Сайт разработчика Александр Климова

/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000

Memoria - игра для тренировки памяти. Часть 1

Напишем хорошо известную игру для тренировки памяти. На игровом поле расположены перевернутые картинки. Переворачивая по две, необходимо найти среди них одинаковые. Найденная пара исчезает с поля.

По задумке в программе должно быть:

  1. Начальный экран
  2. Настройки: выбор цвета фона и набора картинок
  3. Игровое поле 6х6 с картинками
  4. Учет количество ходов (или времени)
  5. Просмотр таблицы рекордов

Игровое поле

Создаем новый проект Memoria.

Для игрового поля будем использовать элемент GridView. GridView - это представление данных (то есть их отображение на экране). А сами данные хранятся и передаются в View через адаптер (BaseAdapter), который связан с этим GridView. Стандартный адаптер нам не подойдет, поэтому будем писать свой. Им будет класс GridAdapter, унаследованный от стандартного класса адаптеров BaseAdapter.

main.xml


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <GridView
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/field"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:gravity="center" />

</LinearLayout>

Создаем в проекте файл GridAdapter.java. Создаем в нем класс GridAdapter. Унаследованные от BaseAdapter классы должны определять четыре метода:

  • int getCount() - возвращает количество элементов в GridView
  • Object getItem(int position) - возвращает объект, хранящийся под номером position
  • long getItemId(int position) - возвращает идентификатор элемента, хранящегося в под номером position
  • View getView(int position, View convertView, ViewGroup parent) - возвращает View, который будет выведен в ячейке под номером position. Каждая ячейка идентифицируется не двумя цифрами (столбец и строка), как можно было ожидать, а одной. Нумеруются ячейки сверху вниз, слева направо.

GridAdapter.java


package ru.alexanderklimov.memoria;

import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;

public class GridAdapter extends BaseAdapter {
	private Context mContext;
	private Integer mCols, mRows;

	public GridAdapter(Context context, int cols, int rows) {
		mContext = context;
		mCols = cols;
		mRows = rows;
	}

	@Override
	public int getCount() {
		return mCols * mRows;
	}

	@Override
	public Object getItem(int position) {
		return null;
	}

	@Override
	public long getItemId(int position) {
		return 0;
	}

	@Override
	public View getView(int position, View convertView, ViewGroup parent) {

		ImageView view; // для вывода картинки

		if (convertView == null)
			view = new ImageView(mContext);
		else
			view = (ImageView) convertView;

		view.setImageResource(R.drawable.ic_launcher);

		return view;
	}
}

Теперь в основном классе связываем наш адаптер с таблицей:


package ru.alexanderklimov.memoria;

import android.app.Activity;
import android.os.Bundle;
import android.widget.GridView;

public class MemoriaActivity extends Activity {
	
	private GridView mGrid;
	private GridAdapter mAdapter;
	  
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        mGrid = (GridView)findViewById(R.id.field);
        mGrid.setNumColumns(6);
        mGrid.setEnabled(true);
     
        mAdapter = new GridAdapter(this, 6, 6);
        mGrid.setAdapter(mAdapter);
    }
}

Запускаем проект и получаем таблицу 6*6 с одинаковой картинкой (обратная сторона) в каждой ячейке.

Игровое поле

Нам нужны разные картинки, причем каждая картинка должна встречаться два раза. Так как наборы картинок можно будет менять, для удобства в каждом таком наборе назовем файлы следующим образом: animal0.png, animal1.png, …. animal17.png или people0.png, people1.png, …. people17.png. Теперь наборы картинок отличаются префиксом, а внутри набора цифрой. Для обращения к картинке используется его идентификатор, например R.drawable.picture. Для того чтобы выбирать картинку динамически, воспользуемся классом Resources. Для инициализации экземпляра класса используется метод getResources() из класса Context:


getIdentifier(String name, String defType, String defPackage)

где:

  • name — название ресурса
  • defType — его тип (drawable, string, anim)
  • defPackage — имя пакета приложения. Имя пакета можно получить с помощью метода getPackageName() класса Context.

Таким образом записи:

Integer identifierID = R.drawable.picture;

и

Resources mRes = mContext.getResources();
Integer identifierID = mRes.getIdentifier("picture", "drawable", mContext.getPackageName());

по сути одинаковые и возвращают идентификатор файла picture.png из директории drawable.

Картинки в таблице должны располагаться в случайном порядке, для этого удобно использовать метод shuffle() класса ArrayList. ArrayList — массив для хранения данных любого типа. Добавим в массив название каждой картинки по два раза, а затем вызывем метод shuffle(), который перемешает все элементы.


package ru.alexanderklimov.memoria;

import java.util.ArrayList;
import java.util.Collections;

import android.content.Context;
import android.content.res.Resources;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;

public class GridAdapter extends BaseAdapter {
	private Context mContext;
	private Integer mCols, mRows;

	private ArrayList<String> arrPict; // массив картинок
	private String PictureCollection; // Префикс набора картинок
	private Resources mRes; // Ресурсы приложени

	public GridAdapter(Context context, int cols, int rows) {
		mContext = context;
		mCols = cols;
		mRows = rows;
		
		arrPict = new ArrayList<String>();
	    // Пока определяем префикс так, позже он будет браться из настроек
	    PictureCollection = "animal";
	    // Получаем все ресурсы приложения
	    mRes = mContext.getResources();
	 
	    // Метод заполняющий массив vecPict
	    makePictArray();
	}
	
	private void makePictArray () {
	    // очищаем вектор
	    arrPict.clear();
	    // добавляем
	    for (int i = 0; i < ((mCols * mRows) / 2); i++)
	    {
	      arrPict.add (PictureCollection + Integer.toString (i));
	      arrPict.add (PictureCollection + Integer.toString (i));
	    }
	    // перемешиваем
	    Collections.shuffle(arrPict);
	  }

	@Override
	public int getCount() {
		return mCols * mRows;
	}

	@Override
	public Object getItem(int position) {
		return null;
	}

	@Override
	public long getItemId(int position) {
		return 0;
	}

	@Override
	public View getView(int position, View convertView, ViewGroup parent) {

		ImageView view; // для вывода картинки

		if (convertView == null)
			view = new ImageView(mContext);
		else
			view = (ImageView) convertView;
		
		// Получаем идентификатор ресурса для картинки,
	    // которая находится в векторе vecPict на позиции position
		Integer drawableId = mRes.getIdentifier(arrPict.get(position), "drawable", mContext.getPackageName());

		//view.setImageResource(R.drawable.ic_launcher);
        view.setImageResource(drawableId);
		return view;

	}
}

Запускаем. Видим вот такой вот веселенький зоопарк :)

Картинки

Каждая ячейка может быть в одном из трех состояний: открытая, закрытая и удаленая (если две открытые картинки совпали, они убираются с поля). Для удобства добавим в GridAdapter константы для этих состояний:

private static final int CELL_CLOSE = 0;
private static final int CELL_OPEN = 1;
private static final int CELL_DELETE = -1;

Мы сделаем их в виде enum (он красивее смотрится). В адаптере создадим массив для хранения состояния каждой ячейки, вначале игры всем ячейкам проставляется статус закрыта (CELL_CLOSE). И немного исправим метод getView чтобы картинка рисовалась в зависимости от статуса:


private static enum Status {CELL_OPEN, CELL_CLOSE, CELL_DELETE};
private ArrayList<Status> arrStatus; // состояние ячеек

	public GridAdapter(Context context, int cols, int rows) {
	mContext = context;
	mCols = cols;
	mRows = rows;

	arrPict = new ArrayList<String>();
	// Пока определяем префикс так, позже он будет браться из настроек
	PictureCollection = "animal";
	// Получаем все ресурсы приложения
	mRes = mContext.getResources();

	// Метод заполняющий массив vecPict
	makePictArray();
	// Метод устанавливающий всем ячейкам статус CELL_CLOSE
	closeAllCells();
}

private void closeAllCells() {
	arrStatus.clear();
	for (int i = 0; i < mCols * mRows; i++)
		arrStatus.add(Status.CELL_CLOSE);
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {

	ImageView view; // для вывода картинки

	if (convertView == null)
		view = new ImageView(mContext);
	else
		view = (ImageView) convertView;

	switch (arrStatus.get(position)) {
	case CELL_OPEN:
		// Получаем идентификатор ресурса для картинки,
		// которая находится в векторе vecPict на позиции position
		Integer drawableId = mRes.getIdentifier(arrPict.get(position),
				"drawable", mContext.getPackageName());
		view.setImageResource(drawableId);
		break;
	case CELL_CLOSE:
		view.setImageResource(R.drawable.close);
		break;
	default:
		view.setImageResource(R.drawable.none);
	}
	return view;
}

Теперь надо обработать нажатие на ячейку. По нажатию будем делать следующее:

  • Проверяем, есть ли уже открытые ячейки. Если открыты две ячейки, то сравниваем их и если они одинаковые удаляем, если разные — закрываем.
  • Открываем выбранную ячейку.
  • Проверяем, остались ли открытые ячейки, если нет, то заканчиваем игру

Можно проверять открытые картинки после нажатия, но тогда они будут закрываться (или удаляться) слишком быстро, что не удобно.

Нажатие на ячейку в таблице обрабатывает метод setOnItemClickListener класса GridView:


mGrid.setOnItemClickListener(new OnItemClickListener() {
	@Override
	public void onItemClick(AdapterView<?> parent, View v,
			int position, long id) {

		mAdapter.checkOpenCells();
		mAdapter.openCell(position);

		if (mAdapter.checkGameOver())
			Toast.makeText(getApplicationContext(), "Игра закончена",
					Toast.LENGTH_SHORT).show();
	}
});

В методе setOnItemClickListener мы дергаем три метода из GridAdapter, они достаточно банальны:


public void checkOpenCells() {
    int first = arrStatus.indexOf(Status.CELL_OPEN);
    int second = arrStatus.lastIndexOf(Status.CELL_OPEN);
    if (first == second)
      return;
    if (arrPict.get(first).equals (arrPict.get(second)))
    {
      arrStatus.set(first, Status.CELL_DELETE);
      arrStatus.set(second, Status.CELL_DELETE);
    }
    else
    {
      arrStatus.set(first, Status.CELL_CLOSE);
      arrStatus.set(second, Status.CELL_CLOSE);
    }
    return;
  }
 
public void openCell(int position) {
  if (arrStatus.get(position) != Status.CELL_DELETE)
    arrStatus.set(position, Status.CELL_OPEN);

  notifyDataSetChanged(); 
  return;
}

public boolean checkGameOver() {
  if (arrStatus.indexOf(Status.CELL_CLOSE) < 0)
    return true;
  return false;
}

Метод notifyDataSetChanged() класса BaseAdapter (мы его вызываем в public void openCell(int position)) сообщает GridView, что данные изменились и таблица перерисовывается. Казалось бы, что notifyDataSetChanged() надо вызвать и в checkOpenCells(), потому что там тоже меняется статус ячеек, но смысла в этом мало, потому что после checkOpenCells() всегда будет вызываться openCell() в которой таблица и перерисуется.

Все! Можно запускать и играть. Пока игра не ведет счета и после окончания игры просто показывает черный экран без возможности запуска новой игры. Все это мы сделаем в следующий раз.

Источник: Программирование для android - Игра для тренировки памяти

Реклама