|
Итак, в нашем приложении осталось всего ничего: реализовать собственно
алгоритм игры Life и отобразить его в GridView. Этим-то мы сейчас и
займёмся.
Класс, реализующий логику Life
Добавим в проект новый класс, назовем его LifeModel. Тут у нас будет реализована вся логика Life
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 так, чтобы она выглядела следующим образом:
<?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 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 поля:
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 следующим образом:
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, но, возможно, это от недостатка опыта.
Спасибо, если кто осилил. Замечания приветствуются.
Исходники примера
Об авторе: darja живет Владивостоке, работает программистом, интересуется разными технологиями. Цикл статей перекликается на сайте www.mobilab.ru с согласия автора.
|