Итак, в нашем приложении осталось всего ничего: реализовать собственно алгоритм игры 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);
}
Запускаем и видим:
Отображение последующих поколений
Вот мы и добрались почти до самого конца. Осталось отобразить ход игры. Тут стоит воспользоваться таймером. Таймер будет каждую секунду вызывать обработчик, в котором данные в адаптере будут пересчитываться. Сначала добавим в 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, можно увидеть, например, следующее
Ссылки
Заключение
Итак, мы написали первое приложение для Android, которое уже и не совсем "Hello, World". Лично мне писать для Android понравилось куда больше, чем классические мидлеты. Остался, правда, ряд претензий к Eclipse, но, возможно, это от недостатка опыта.
Спасибо, если кто осилил. Замечания приветствуются.
Комментариев нет:
Отправить комментарий