четверг, 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 комментариев:

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

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

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

    ОтветитьУдалить
  3. в мемориз. очень доходчиво.

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

    ОтветитьУдалить
  4. Одна из лучших статей по Android

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

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

    ОтветитьУдалить
  7. А статьи очень хорошие, спасибо.

    ОтветитьУдалить
  8. Этот комментарий был удален автором.

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

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

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

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

    ОтветитьУдалить
  11. Дарья
    спасибо за комментарий.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    ОтветитьУдалить
  15. отличная статья. спасибо!

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

    ОтветитьУдалить
  16. 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

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

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

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

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

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

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

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

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

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

    ОтветитьУдалить
  25. Агромадное спасибо Дарье За уроки.

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

    ОтветитьУдалить
  26. Мегаспасибо автору!

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

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

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

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

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

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

    ОтветитьУдалить

Пожалуйста, пишите содержательные комментарии и соблюдайте вежливость.