четверг, 16 октября 2008 г.

Знакомство с Android. Часть 4: Использование GridView

Итак, в нашем приложении осталось всего ничего: реализовать собственно алгоритм игры Life и отобразить его в GridView. Этим-то мы сейчас и займёмся.

Класс, реализующий логику Life

Добавим в проект новый класс, назовем его LifeModel. Тут у нас будет реализована вся логика Life

LifeModel.java

package demo.android.life;

import java.util.Random;

public class LifeModel
{
    // состояния клетки
    private static final Byte CELL_ALIVE = 1; // клетка жива
    private static final Byte CELL_DEAD = 0; // клетки нет
    
    // константы для количества соседей
    private static final Byte NEIGHBOURS_MIN = 2; // минимальное число соседей для живой клетки
    private static final Byte NEIGHBOURS_MAX = 3; // максимальное число соседей для живой клетки
    private static final Byte NEIGHBOURS_BORN = 3; // необходимое число соседей для рождения клетки
    
    private static int mCols; // количество столбцов на карте
    private static int mRows; // количество строк на карте
    private Byte[][] mCells; // расположение очередного поколения на карте. 
                            //Каждая ячейка может содержать либо CELL_ACTIVE, либо CELL_DEAD
    
    /**
     * Конструктор
     */
    public LifeModel(int rows, int cols, int cellsNumber)
    {
        mCols = cols;
        mRows = rows;
        mCells = new Byte[mRows][mCols];
        
        initValues(cellsNumber);
    }
    
    /**
     * Инициализация первого поколения случайным образом
     * @param cellsNumber количество клеток в первом поколении
     */
    private void initValues(int cellsNumber)
    {
        for (int i = 0; i < mRows; ++i)
            for (int j = 0; j < mCols; ++j)
                mCells[i][j] = CELL_DEAD;
        
        Random rnd = new Random(System.currentTimeMillis());
        for (int i = 0; i < cellsNumber; ++i)
        {
            int cc;
            int cr;
            do
            {
                cc = rnd.nextInt(mCols);
                cr = rnd.nextInt(mRows);
            }
            while (isCellAlive(cr, cc));
            mCells[cr][cc] = CELL_ALIVE;       
        }
    }
    
    /**
     * Переход к следующему поколению
     */
    public void next()
    {
        Byte[][] tmp = new Byte[mRows][mCols];
        
        // цикл по всем клеткам
        for (int i = 0; i < mRows; ++i)
            for (int j = 0; j < mCols; ++j)
            {
                // вычисляем количество соседей для каждой клетки
                int n = 
                    itemAt(i-1, j-1) + itemAt(i-1, j) + itemAt(i-1, j+1) +
                    itemAt(i, j-1) + itemAt(i, j+1) +
                    itemAt(i+1, j-1) + itemAt(i+1, j) + itemAt(i+1, j+1);
                
                tmp[i][j] = mCells[i][j];
                if (isCellAlive(i, j))
                {
                    // если клетка жива, а соседей у нее недостаточно или слишком много, клетка умирает
                    if (n < NEIGHBOURS_MIN || n > NEIGHBOURS_MAX)
                        tmp[i][j] = CELL_DEAD;
                }
                else
                {
                    // если у пустой клетки ровно столько соседей, сколько нужно, она оживает 
                    if (n == NEIGHBOURS_BORN)
                        tmp[i][j] = CELL_ALIVE;        
                }
            }
        mCells = tmp;
    }
    
    /**
     * @return Размер поля
     */
    public int getCount()
    {
        return mCols * mRows;
    }
    
    /**
     * @param row Номер строки
     * @param col Номер столбца
     * @return Значение ячейки, находящейся в указанной строке и указанном столбце
     */
    private Byte itemAt(int row, int col)
    {
        if (row < 0 || row >= mRows || col < 0 || col >= mCols)
            return 0;
            
        return mCells[row][col];
    }
    
    /**
     * @param row Номер строки
     * @param col Номер столбца
     * @return Жива ли клетка, находящаяся в указанной строке и указанном столбце
     */
    public Boolean isCellAlive(int row, int col)
    {
        return itemAt(row, col) == CELL_ALIVE;
    }

    /**
     * @param position Позиция (для клетки [row, col], вычисляется как row * mCols + col)
     * @return Жива ли клетка, находящаяся в указанной позиции
     */
    public Boolean isCellAlive(int position)
    {
        int r = position / mCols;
        int c = position % mCols;

        return isCellAlive(r,c);
    }
}

GridView. Отображение первого поколения клеток

Модифицируем разметку run.xml так, чтобы она выглядела следующим образом:

run.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <GridView
        android:id="@+id/life_grid" 
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        
        android:padding="1dp"
        android:verticalSpacing="1dp"
        android:horizontalSpacing="1dp"
        android:columnWidth="10dp"

        android:gravity="center"
    />
    <Button
        android:id="@+id/close"
        android:text="@string/close"
        android:textStyle="bold"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
    />
</LinearLayout>
Теперь нам надо отобразить в этом GridView данные. Думаю, вполне логичным для данной задачи было бы отображение клеток в виде графических файлов. Создаем два графических файлика, на одном изображаем черный квадратик, на другом - зелёный. Первый назовём empty.png и он будет обозначать пустую клетку, второй - cell.png, и он будет изображать живую клетку. Оба файлика положим в папку /res/drawable
Нам нужно знать, что именно отображать в гриде. Для этого нужно создать для грида поставщик данных (Adapter). Есть стандартные классы для адаптеров (ArrayAdapter и др.), но нам будет удобнее написать свой, унаследованный от BaseAdapter. Дабы не плодить файлов (да и не нужен он больше никому), поместим его внутрь класса RunActivity. А напишем там следующее:

RunActivity.java

class LifeAdapter extends BaseAdapter
{
    private Context mContext;

    private LifeModel mLifeModel;

    public LifeAdapter(Context context, int cols, int rows, int cells)
    {
        mContext = context;
        mLifeModel = new LifeModel(rows, cols, cells);
    }

    public void next()
    {
        mLifeModel.next();
    }

    /**
     * Возвращает количество элементов в GridView 
     */
    public int getCount()
    {
        return mLifeModel.getCount();
    }

    /**
     * Возвращает объект, хранящийся под номером position
     */
    public Object getItem(int position)
    {
        return mLifeModel.isCellAlive(position);
    }

    /**
     * Возвращает идентификатор элемента, хранящегося в под номером position
     */
    public long getItemId(int position)
    {
        return position;
    }

    /**
     * Возвращает элемент управления, который будет выведен под номером position
     */
    public View getView(int position, View convertView, ViewGroup parent)
    {
        ImageView view; // выводиться у нас будет картинка
        
        if (convertView == null)
        {
            view = new ImageView(mContext);

            // задаем атрибуты
            view.setLayoutParams(new GridView.LayoutParams(10, 10));
            view.setAdjustViewBounds(false);
            view.setScaleType(ImageView.ScaleType.CENTER_CROP);
            view.setPadding(1, 1, 1, 1);
        }
        else
        {
            view = (ImageView)convertView;
        }
        
        // выводим черный квадратик, если клетка пустая, и зеленый, если она жива
        view.setImageResource(mLifeModel.isCellAlive(position) ? R.drawable.cell : R.drawable.empty);
        
        return view;
    }
}
Теперь добавим в RunActivity поля:

RunActivity.java

private GridView mLifeGrid;
private LifeAdapter mAdapter;
и модифицируем onCreate:

RunActivity.java

public void onCreate(Bundle savedInstanceState)
{
    super.onCreate(savedInstanceState);
    setContentView(R.layout.run);
    
    mCloseButton = (Button) findViewById(R.id.close);
    mCloseButton.setOnClickListener(this);

    Bundle extras = getIntent().getExtras();
    int cols = extras.getInt(EXT_COLS);
    int rows = extras.getInt(EXT_ROWS);
    int cells = extras.getInt(EXT_CELLS);
    mAdapter = new LifeAdapter(this, cols, rows, cells);
    
    mLifeGrid = (GridView)findViewById(R.id.life_grid);
    mLifeGrid.setAdapter(mAdapter);
    mLifeGrid.setNumColumns(cols);
    mLifeGrid.setEnabled(false);
    mLifeGrid.setStretchMode(0);
}
Запускаем и видим:
Начальное поколение клеток

Отображение последующих поколений

Вот мы и добрались почти до самого конца. Осталось отобразить ход игры.
Каждую секунду нам нужно отправлять кому-то команду о том, что нужно обновить модель и UI. Для этого лучше всего подходит класс Handler. Назначение и поведение этого класса достойны отдельной статьи, но вкратце можно сказать, что он, ассоциировавшись с неким потоком и очередью сообщений, может отправлять туда на выполнение всякие Runnables и Messages. Одно из главных применений класса Handler — запуск Runnable по расписанию. Для этого в нем имеются методы вроде post, postDelayed и postAtTime
Итак, для отображения последующих поколений клеток модифицируем класс RunActivity следующим образом:

RunActivity.java

public class RunActivity extends Activity implements OnClickListener
{
    ...
    
    private Handler mHandler;

    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.run);
        
        mCloseButton = (Button) findViewById(R.id.close);
        mCloseButton.setOnClickListener(this);

        Bundle extras = getIntent().getExtras();
        int cols = extras.getInt(EXT_COLS);
        int rows = extras.getInt(EXT_ROWS);
        int cells = extras.getInt(EXT_CELLS);
        mAdapter = new LifeAdapter(this, cols, rows, cells);
        
        mLifeGrid = (GridView)findViewById(R.id.life_grid);
        mLifeGrid.setAdapter(mAdapter);
        mLifeGrid.setNumColumns(cols);
        mLifeGrid.setEnabled(false);
        mLifeGrid.setStretchMode(0);

        mHandler = new Handler();
        mHandler.postDelayed(mUpdateGeneration, 1000);
    }

    private Runnable mUpdateGeneration = new Runnable()
    {
        public void run()
        {
            mAdapter.next();
            mLifeGrid.setAdapter(mAdapter);

            mHandler.postDelayed(mUpdateGeneration, 1000);
        }
    }; 
    ...
Теперь, запустив Life, можно увидеть, например, следующее
Результат работы приложения

Заключение

Итак, мы написали первое приложение для Android, которое уже и не совсем "Hello, World". Лично мне писать для Android понравилось куда больше, чем классические мидлеты. Остался, правда, ряд претензий к Eclipse, но, возможно, это от недостатка опыта.
Спасибо, если кто осилил. Замечания приветствуются.
Исходники примера

30 комментариев:

Анонимный комментирует...

Приятно, что девушки тоже интересуются Android :)

Анонимный комментирует...

Разместил на тебя ссылку, надеюсь ты не против :)

http://www.maximyudin.com/2008/10/25/android/zhenskij-blog-pro-android/

build_your_web комментирует...

в мемориз. очень доходчиво.

как раз недавно поставил на коммуникатор "портированный" андроид. попользовал - оказалось удобнее WM.

Андрей комментирует...

Одна из лучших статей по Android

Анонимный комментирует...

мне тоже понравилось. позволяет быстро ознакомиться с многими аспектами как java, так и android человеку, который программировал много, но не в этом направлении :) спасибо.

Анонимный комментирует...

Анонимный, вы меня извините, конечно, но научиться писать на java под android и говорить, что умеешь писать на java -- это как наступить в лужу и говорить, что можешь переплыть океан.

Анонимный комментирует...

А статьи очень хорошие, спасибо.

Unknown комментирует...
Этот комментарий был удален автором.
Анонимный комментирует...

Привет. Зачитался :) Спасибо за разжевывание андроида для начинающих.

Нельзя ли заказать урок по параметрам разметки?

Никак не могу уловить закономерность всех этих длинн, ширин и прочего - вроде ставишь по логике, не работает.
Вероятно там какая то хитрая система контейнеров и относительных величин. Или андроид не подразумевает форматирование как в HTML CSS и кладет все так на экране как считает нужным?
Ну и хотелось бы про grid и tablelayout..

darja комментирует...

2 Antabis:
Честно говоря, не совсем поняла, в чем Ваша проблема. У контролов есть атрибуты width и height, с помощью которых задаются абсолютные размеры, и атрибуты layout_width и layout_height, задающие размеры относительно контейнера. Относительно таблиц: в TableRow можно задать width у какого-нибудь контрола, и весь столбец, в котором он содержится, станет такой ширины.
А вообще мысль интересная, может и действительно стоит написать подробную статью про Layout-ы. Правда, вряд ли получится это сделать в ближайшее время.

Анонимный комментирует...

Дарья
спасибо за комментарий.

Понимаете в чем проблема. Все сэмплы которые я видел в сети (особенно для нового СДК) - какие-то укушенные. То есть построены по принципу "делай так и будет тебе счастье!". И шаг влево, шаг вправо для новичка приводит к невероятным последствиям.

Вот, к примеру, пока нашел что же за зверь такой - колонки в таблице и как с ними обращаться, весь поседел.

Я сам - веб-программист и по моей логике колонки должны обявляться как контейнер ХМЛ, ну то есть парой ' col -- /col' как то так.

А в андроиде такого нет и в доке тоже не особо разбежались писать, КАК ЭТО РАБОТАЕТ. Аналогично фиг знает сколько времени искал принцип действия автоматического расширения колонок. Пока не понял, что надо просто перечислить номера в параметре - и будет счастье.

Проблемы возникают именно из-за предыдущего опыта, конепция разметки андроида как-то не вяжется ни с чем, что я знаю.

Опять же только через неделю сообразил что архитектура скорее напоминает ASP.NET чем Жаву. Своими адаптерами и прочим.

В данный момент очередной раз озадачен толкованием понятия spiner. По жизни спинер был числовым полем с кнопочками. Значение поля увеличивалось при нажатии на одну и уменьшалось при нажатии на другую. А в андроиде спинер - это же комбобокс, ну никак не спинер :) Какое-то зазеркалье сплошное.

Кстати, вы не подскажете, в каком классе искать настоящий спинер? Я видел что его используют, но не могу найти :(

Вот такие неприятности к примеру у меня, как у человека начавшего осваивать ЭТО :)

Спасибо за помощь!

darja комментирует...

Мда, понятие spinner-а действительно своеобразное. Я как-то искала нормальный spinner и не нашла, пришлось использовать TextEdit. А вы точно его отдельно видели? Это не DatePicker был?
Зря Вы так уж ругаетесь. После j2me, где разметок никаких не предусмотрено, и контролы надо добавлять прямо в коде, здесь все очень даже человечно. А разметка действительно немного похожа на aspx.

Анонимный комментирует...

Ну, я не уверен, что это несамодельный спинер был, то что я видел. Очень может быть и EditBox с двумя кнопками :)

Мне сложно судить о том насколько хорош Андроид для разработчика - это мой первый мобильный опыт...
Я то смотрю с точки зрения обычных приложений.

Роман Рахман комментирует...

А еще было бы здорово сделать поле игры замкнутым т.е. имитирующим развертку сферы. Например соседом слева у ячеек нулевого столбца считаются ячейки последнего столбца, аналогично -- со строками.

Для этого нужно всего то подправить функцию itemAt()

Понятно, что к программированию под Androind это мало относится, но глайдеры пусть стало бы сразу веселее.

Nick Pepper комментирует...

отличная статья. спасибо!

P.S.: а ряд претензий к Eclipse и в самом деле остался, причём очень длинный такой ряд...

Анонимный комментирует...

It was rather interesting for me to read the article. Thanks for it. I like such topics and anything connected to this matter. I would like to read a bit more on that blog soon.
Alex
Mobile phone blocker

Sergii Getman комментирует...

Прошу прощения, Дарья, но я так и не понял, каким образом мы задаем кол-во строк нашему GridView(по аналогии с numColumns, через какой атрибут)?

Rat комментирует...

Дарья, доброго времени суток!
Спасибо за Ваши тьюториалы, для Android в Рунете их можно пересчитать по пальцам.
Не могли бы Вы уделить внимание, почему не работает мой код - не обновляется GridView при вызове функции adapter.NotifyDataSetChanged(). Буду премного благодарен за экспертный анализ =)

Alexey Tagarov комментирует...

Девушка вы меня покорили своей статьей, просто шикарно!!!

Анонимный комментирует...

Отличная серия! По Android так не хватает русскоязычной документации. Спасибо Вам огромное.

Анонимный комментирует...

Спасибо, Дарья!
У меня, правда, есть ряд вопросов, так как писать под Андроид начал недавно и не знаю, как реализовать некоторые элементы программы.
Как с вами можно связаться для небольшого разговора на эту тему?

darja комментирует...

Контакты в профиле. Но рекомендую с вопросами сначала ходить в гугл. Я уже год как почти не занималась андроидом.

Анонимный комментирует...

Спасибо за столь оперативный ответ, не ожидал, если честно.
Да дело в том, что мне даже тяжело определиться, какие лэйоты использовать в моём случае. Так что вопросы будут простые!)

Анонимный комментирует...

Спасибо за этот пример. Всё же, когда читаешь объяснение по-русски понимаешь намного больше.

А у тебя приложение при размерах 25х25 тоже немного медленно в симуляторе выполняется / выполнялось?

Анонимный комментирует...

Агромадное спасибо Дарье За уроки.

Не знаю, чей это глюк - может, движок сайта, Но кнопки "Следующее" и "Предыдущее" внизу страницы перепутаны...

Анонимный комментирует...

Мегаспасибо автору!

Вопрос такой - в ресурсах есть иконка, и в манифесте она прописана.
А Life на экране изображает стандартную гугловскую картинку.

И еще - нигде вроде бы не прописаны запрашиваемые привилегии. А при установке просит разрешить телефонные вызовы и память(SD-карту)

Анонимный комментирует...

Огромное СПАСИБО!
Всё очень наглядно и доступно!
Респект Автору!

Igor Bogoslavskyi комментирует...

присоединяюсь к благодарным возгласам) Очень помогает при первых шагах на андроиде. )

Анонимный комментирует...

Очень!!! хорошая статья, все понятно и не вызывает вопросов, Огромное спасибо автору.

Сергей комментирует...

Огромное спасибо, наконец то разобрался с GridView. Все понятно, все по полочкам.