среда, 18 апреля 2012 г.

Игра для тренировки памяти (часть 4). Хранение данных

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

Осталось только сохранять кол-во очков и время в конце каждой игры и выводить 5 лучших результатов по нажатию на кнопку «Рекорды». Окно результатов будет с двумя табами, в одной соответственно время, во второй — очки. Сами результаты будем хранить в файле во внутренней памяти телефона.


1. Создадим новый класс Records (в Records.java) унаследованный от TabActivity и разметку окна records.xml (в /res/layout). Разметка окна будет такая:
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <TabHost xmlns:android="http://schemas.android.com/apk/res/android"
  3.  android:id="@android:id/tabhost"
  4.  android:layout_width="fill_parent"
  5.  android:layout_height="fill_parent">
  6.  <LinearLayout
  7.   android:orientation="vertical"
  8.   android:layout_width="fill_parent"
  9.   android:layout_height="fill_parent">
  10.  
  11.   <TabWidget
  12.    android:id="@android:id/tabs"
  13.    android:layout_width="fill_parent"
  14.    android:layout_height="wrap_content"
  15.   />
  16.  
  17.   <FrameLayout
  18.    android:id="@android:id/tabcontent"
  19.    android:layout_width="fill_parent"
  20.    android:layout_height="fill_parent" />
  21.  
  22.   </LinearLayout>
  23. </TabHost>

TabHost - контейнер для окна с вкладками. У него обязательно должны быть два дочерних элемента: TabWidget и FrameLayout.
TabWidget — список ярлыков вкладок, при нажатии на один из ярлыков в FrameLayout открывается соответствующая вкладка.

В классе Records напишем:
  1. public class Records extends TabActivity {
  2.     /** Called when the activity is first created. */
  3.     @Override
  4.     public void onCreate(Bundle savedInstanceState) {
  5.         super.onCreate(savedInstanceState);
  6.         setContentView(R.layout.records);
  7.  
  8.         TabHost tabHost = getTabHost();
  9.  
  10.         // Вкладка Время
  11.         TabSpec timetab = tabHost.newTabSpec("Time");
  12.         // устанавливаем заголовок и иконку
  13.         timetab.setIndicator("по времени", getResources().getDrawable(R.drawable.time));
  14.         // устанавливаем окно, которая будет показываться во вкладке
  15.         Intent timeIntent = new Intent(this, RecordTime.class);
  16.         timetab.setContent(timeIntent);
  17.  
  18.         // Вкладка Очки
  19.         TabSpec pointtab = tabHost.newTabSpec("Point");
  20.         pointtab.setIndicator("по очкам", getResources().getDrawable(R.drawable.point));
  21.         Intent pointIntent = new Intent(this, RecordPoint.class);
  22.         pointtab.setContent(pointIntent);
  23.  
  24.         // Добавляем вкладки в TabHost
  25.         tabHost.addTab(timetab); 
  26.         tabHost.addTab(pointtab); 
  27.     }
  28. }

2. В качестве иконки вкладки мы написали «R.drawable.time», что обычно соответствует файлу /res/drawable/time.png. Но мы хотим сделать, чтобы рисунок активной вкладки отличался от рисунка неактивной. Например, у активной вкладки изображение будет красным (time_red.png), а у неактивной — серым (time_grey.png). Это легко сделать с помощью StateListDrawable — объект drawable определённый в xml, который позволяет использовать несколько изображений в зависимости от состояния объекта. Создадим в /res/drawable файл time.xml и напишем в него
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <selector xmlns:android="http://schemas.android.com/apk/res/android">
  3.  <item android:drawable="@drawable/time_red"
  4.    android:state_selected="true" />
  5.  <item android:drawable="@drawable/time_grey" />
  6. </selector>
Аналогично сделаем с картинками point_grey.png и point_red.png.

3. Добавим еще два класса RecordTime (в RecordTime.java) и RecordPoint (в RecordPoint.java) и соответственно две разметки к ним time_tab.xml и point_tab.xml. Добавим все новые классы в AndroidManifest.xml и по кнопке «Результаты» будем открывать окно Records. Все это мы уже делали, так что еще раз писать код не буду, его можно посмотреть в части 2.
4. Запускаем:


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

6. Для того, чтобы сохранить данные в файле нам понадобятся два класса:
FileOutputStream — входной поток, который записывает данные в файл. Класс инициализируется методом openFileOutput(String name, int mode) (класса Context), где name — имя файла, который надо открыть, mode — режим, по умолчанию используется MODE_PRIVATE (создается файл доступный только из данного приложения). Также может быть MODE_APPEND (если файл уже существует, то данные добавляются в конец файла), MODE_WORLD_READABLE и MODE_WORLD_WRITABLE (файлы которые может читать или записывать сторонее приложение).

ObjectOutputStream — класс для сериализации любых объектов.

  1. SomeObject obj;
  2. FileOutputStream fos = openFileOutput(«FileName.txt», Context.MODE_PRIVATE);
  3. ObjectOutputStream os = new ObjectOutputStream(fos);
  4. os.writeObject(obj);
  5. os.close();

Мы сериализуем и записываем в файл FileName.txt, объект типа MyObject. Объект может быть любого типа, например String, Integer, ArrayList.

Для чтения файлов используются классы FileInputStream и ObjectInputStream. Работа с ними аналогична

  1. FileInputStream fis = openFileInput(«FileName.txt»);
  2. ObjectInputStream is = new ObjectInputStream(fis);
  3. SomeObject s1 = (SomeObject) is.readObject();
  4. is.close();

Метод Object readObject () возвращает значение типа Object — базовый класс языка Java. Все не примитивные типы данных являются наследниками этого класса, поэтому мы можем выполнить операцию по приведению типа.

Записать в один файл можно несколько объектов (несколько раз вызвав метод writeObject()). Соответственно читать их надо будет в том же порядке.

7. Если вставить эти четыре строчки в код, то eclipse их подчеркнет и напишет кучу ошибок типа «Unhandled exception type FileNotFoundException» (IOException или ClassNotFoundException). Этот код потенциально может вызывать исключения и поэтому его надо окружить операторами try/catch. По хорошему надо обработать каждое исключение и написать код, что делать в случае его возникновения. Но все эти виды исключений являются наследниками класса Exception, поэтому все их можно отловить в одном catch (Exception e) {}. В нашей программе мы так и поступим, в случае любой ошибки будем писать сообщение пользователю (Toast).

8. С рекордами удобно работать в ArrayList, так как во-первых, в нем их можно сортировать и искать, а во-вторых, его легко можно вывести в ListView. Значит в нашем файле будет хранится (в сериализованном виде) два массива — рекорды по времени и рекорды по очкам. По окончании игры, считаем массивы, добавим в них значения результатов текущей игры, отсортируем и первые 5 элементов массива запишем обратно в файл. Чтобы не засорять код класса MemoriaActivity напишем всю работу с файлами в отдельном классе RecordAdapter (файл RecordAdapter.java). Это будет просто класс java не от чего не унаследованный.

  1. class RecordAdapter 
  2. {
  3.   // Название файла в котором хранятся данные
  4.   private static String FILE_RECORDS = "memoria-records";
  5.  
  6.   // Два массива для рекордов
  7.   ArrayList<String> recTime;
  8.   ArrayList<Integer> recPoint;
  9.  
  10.   Context mContext;
  11.  
  12.   public RecordAdapter(Context context)
  13.   {
  14.     recTime = new ArrayList<String> ();
  15.     recPoint = new ArrayList<Integer> ();
  16.  
  17.     mContext = context;
  18.     // читаем из файла два массива с рекордами
  19.     try {
  20.       FileInputStream fis = mContext.openFileInput(FILE_RECORDS);
  21.       ObjectInputStream is = new ObjectInputStream(fis);
  22.       recPoint = (ArrayList<Integer>) is.readObject();
  23.       recTime = (ArrayList<String>) is.readObject();
  24.       is.close();
  25.     } catch (Exception e) {
  26.       Toast.makeText(mContext, "Произошла ошибка чтения таблицы рекордов", Toast.LENGTH_LONG);
  27.     }
  28.   }
  29.  
  30.   public void WriteRecords ()
  31.   {
  32.     // записываем в файл массивы с рекордами
  33.     try {
  34.       FileOutputStream fos = mContext.openFileOutput(FILE_RECORDS, Context.MODE_PRIVATE);
  35.       ObjectOutputStream os = new ObjectOutputStream(fos);
  36.       os.writeObject(recPoint);
  37.       os.writeObject(recTime);
  38.       os.close();
  39.     } catch (Exception e) {
  40.       Toast.makeText(mContext, "Произошла ошибка записи в таблицу рекордов", Toast.LENGTH_LONG);
  41.     }
  42.     return;
  43.   }
  44.  
  45.   public void addTime (String str)
  46.   {
  47.     // Добавляем новое значение времени,
  48.     // если такого еще нет в массиве
  49.     if (!recTime.contains(str))
  50.       recTime.add(str);
  51.  
  52.     // сортируем
  53.     Collections.sort(recTime);
  54.  
  55.     // оставляем в массиве только 5 элементов
  56.     for (int i = 5; i < recTime.size(); i++)
  57.       recTime.remove(i);
  58.  
  59.     return;
  60.   }
  61.  
  62.   public void addPoint (Integer num)
  63.   {
  64.     if (!recPoint.contains(num))
  65.       recPoint.add(num);
  66.  
  67.     Collections.sort(recPoint);
  68.  
  69.     for (int i = 5; i < recPoint.size(); i++)
  70.       recPoint.remove(i);
  71.  
  72.     return;
  73.   }
  74.  
  75.   public ArrayList<String> getRecTime()
  76.   {
  77.     // возвращаем массив рекордов по времени
  78.     return recTime;
  79.   }
  80.  
  81.   public ArrayList<String> getRecPoint()
  82.   {
  83.     // переделываем массив целых чисел в массив строк
  84.     ArrayList<String> arr = new ArrayList<String>();
  85.     for (Integer temp : recPoint)
  86.       arr.add(temp.toString());
  87.  
  88.     // возвращаем массив рекордов по очкам
  89.     return arr;
  90.   }
  91. }

9. Теперь напишем в методе ShowGameOver() класса MemoriaActivity, код для сохранения результатов игры:
  1. private void ShowGameOver () {
  2.  
  3.      String time = mTimeScreen.getText().toString();
  4.  
  5.      // Читаем файл с рекордами
  6.      RecordAdapter ra = new RecordAdapter (this);
  7.      // Добавляем новые значения
  8.      ra.addPoint(StepCount);
  9.      ra.addTime(time);
  10.      // Записываем рекорды в файл
  11.      ra.WriteRecords();
  12.  
  13.      // Диалоговое окно
  14.       ...
  15. }

10. А в классах RecordPoint и RecordTime выведем список рекордов. Как вывести список ArrayList я писала в статье про списки. Пишем в point_tab.xml и time_tab.xml:
  1. <LinearLayout
  2.   xmlns:android="http://schemas.android.com/apk/res/android"
  3.   android:orientation="vertical"
  4.   android:layout_width="fill_parent"
  5.   android:layout_height="fill_parent">
  6.  
  7. <ListView  
  8.     android:id="@android:id/list"
  9.     android:layout_width="fill_parent" 
  10.     android:layout_height="wrap_content" 
  11.     />
  12.  
  13. </LinearLayout>

А в классы:
  1. public class RecordPoint extends ListActivity {
  2.   @Override
  3.   public void onCreate(Bundle savedInstanceState) {
  4.     super.onCreate(savedInstanceState);
  5.     setContentView(R.layout.point_tab);
  6.  
  7.     RecordAdapter ra = new RecordAdapter (this);
  8.     ArrayList<String> arr = ra.getRecPoint();
  9.  
  10.     ArrayAdapter<String> mAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, arr);  
  11.     setListAdapter(mAdapter);
  12.   }
  13. }
Вместо android.R.layout.simple_list_item_1 можно написать R.layout.item, а в файле /res/layout/item.xml описать как должна выглядеть каждая строка таблицы.

11. Запускаем. Теперь в конце игры результаты сохраняются и выводятся в списках по кнопкам «Рекорды».


Все, игра готова :)

Исходники: Memoria-4.zip


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

2 комментария: