Перейти к содержимому


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


Сообщений в теме: 3

#1 agentru

    Администратор

  • Главные администраторы
  • PipPipPip
  • 78 сообщений

Отправлено 25 Октябрь 2008 - 10:25

Итак, в нашем приложении осталось всего ничего: реализовать собственно алгоритм игры 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_init.PNG (8,02К)
Количество загрузок:: 370

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

Вот мы и добрались почти до самого конца. Осталось отобразить ход игры. Тут стоит воспользоваться таймером. Таймер будет каждую секунду вызывать обработчик, в котором данные в адаптере будут пересчитываться. Сначала добавим в 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.gif (477,64К)
Количество загрузок:: 344

Заключение

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

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

Исходники: Прикрепленный файл  Life_part4.zip (52,01К)
Количество загрузок:: 158

#2 oldcyber

    Новичок

  • Пользователи
  • Pip
  • 2 сообщений

Отправлено 17 Март 2010 - 12:32

Захватывающе.

Жаль, что подробность изложения упала к концу с появлением более тяжёлых кусков кода :blink:

Посмотрел результат и сразу захотелось добавить возможность изменения времени в таймере в StartScreen и чтобы кнопка close в RunScreen просто возвращала к окну с вводом исходных параметров, а уже там можно было навсегда покинуть приложение :)

Спасибо за такие примеры т.к. я например не программист по основной деятельности и некоторые моменты мне иногда не понятны почему именно так. Просто пишу шаблонами :blink: Понимаю что делает тот или иной кусок программы, но без основы так сказать.

#3 egosum

    Новичок

  • Пользователи
  • Pip
  • 3 сообщений

Отправлено 16 Март 2011 - 05:36

Как сделать чтоб в GridView отображалист в каждой ячейке ImageView и TextView уже несколько дней бьюсь с этим :D
Нашел решение с помощью Layout Inflater

#4 KalinVl

    Новичок

  • Пользователи
  • Pip
  • 1 сообщений

Отправлено 10 Июнь 2011 - 23:46

Автору большой респект)
Я не профи (пока :rolleyes: ) , потихоньку разбираюсь... пишу в IntellIj Idea 10 - всем рекомендую.

Единственное, при копипасте с сайта в идею появляется бяка - табы или пробелы, они в идее не читаются, выделяются красным, а в .xml - файле их вообще не видно( решить проблему - не сложно - просто поудалять их нафиг.
Лично я, т.к. ленив безмерно, написал программку, которая эту фигню исправляет)

второе - в некоторых местах ругается на @Override. Тоже поубирал где не надо.

А в целом - всё мега-отлично) Код красивый, всё достаточно доступно. Спасибо




Ответить



  


Количество пользователей, читающих эту тему: 1

0 пользователей, 1 гостей, 0 анононимных