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

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

16.10.2008

Источник: megadarja.blogspot.com

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

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

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

package com.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 так, чтобы она выглядела следующим образом:

<?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/LifeGrid"
????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/CloseButton"
????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. Дабы не плодить файлов (да и не нужен он больше никому), поместим его внутрь класса RunScreen. А напишем там следующее:

public 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
??*/
??@Override
??public int getCount()
??{
????return mLifeModel.getCount();
??}

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

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

??/**
??* Возвращает элемент управления, который будет выведен под номером position
??*/
??@Override
??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;
??}
}

Теперь добавим в класс поля:

private GridView mLifeGrid;
private LifeAdapter mAdapter;

и модифицируем onCreate:

public void onCreate(Bundle savedInstanceState)
{
??super.onCreate(savedInstanceState);
??setContentView(R.layout.run);
??
??mCloseButton = (Button) findViewById(R.id.CloseButton);
??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.LifeGrid);
??mLifeGrid.setAdapter(mAdapter);
??mLifeGrid.setNumColumns(cols);
??mLifeGrid.setEnabled(false);
??mLifeGrid.setStretchMode(0);

}

Запускаем и видим:

Life для Android: первое поколение

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

Вот мы и добрались почти до самого конца. Осталось отобразить
ход игры. Тут стоит воспользоваться таймером. Таймер будет каждую
секунду вызывать обработчик, в котором данные в адаптере будут
пересчитываться. Сначала добавим в RunScreen поле:

private Timer mTimer;

а в onCreate - такой код:

mTimer = new Timer("LifeTimer");
mTimer.scheduleAtFixedRate(new SendMessageTask(), 0, 500);

SendMessageTask - это класс-обработчик таймера. Мы определим его прямо в классе RunScreen следующим образом:

public class SendMessageTask extends TimerTask
{
??/**
??* @see java.util.TimerTask#run()
??*/
??@Override
??public void run()
??{
????Message m = new Message();
????RunScreen.this.updateGridHandler.sendMessage(m);
??}
}

В RunScreen же добавим такую конструкцию:

Handler updateGridHandler = new Handler()
{
??public void handleMessage(Message msg)
??{
????mAdapter.next();
????mLifeGrid.setAdapter(mAdapter);

????super.handleMessage(msg);
??}
};

Таким образом, по таймауту мы посылаем нашей же форме
сообщение, что пора обновиться, и в только обработчике этого сообщения
обновляемся. Спрашивается, почему нельзя передать mLifeGrid и mAdapter
в класс-обработчик таймера и обновить их там? Ответ - таймер работает в
другом потоке, а андроид разрешает модифицировать элементы управления
только в том потоке, в котором они были созданы.

Теперь, запустив Life, можно увидеть, например, следующее

Кусок Life

Ссылки

Заключение

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

Спасибо, если кто осилил. Замечания приветствуются.

Исходники примера