воскресенье, 26 октября 2008 г.

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

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

Итак, в нашем приложении осталось всего ничего: реализовать собственно алгоритм игры 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

Ссылки

Заключение

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

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

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

Автор: darja на 14:11

Комментариев нет: