Итак, в нашем приложении осталось всего ничего: реализовать собственно
алгоритм игры Life и отобразить его в GridView. Этим-то мы сейчас и
займёмся.
Добавим в проект новый класс, назовем его LifeModel. Тут у нас будет реализована вся логика Life
package demo.android.life; import java.util.Random; publicclass 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 voidnext() { 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) return0; 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); } }
Модифицируем разметку 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. А напишем там следующее:
class LifeAdapterextends 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 voidnext() { 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 поля:
private GridView mLifeGrid; private LifeAdapter mAdapter;
и модифицируем onCreate:
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 следующим образом:
publicclass RunActivityextends 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, но, возможно, это от недостатка опыта.
Спасибо, если кто осилил. Замечания приветствуются.
Об авторе:darja живет Владивостоке, работает программистом, интересуется разными технологиями. Цикл статей перекликается на сайте www.mobilab.ru с согласия автора.