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

Игра для тренировки памяти (часть 2)

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


1. С количеством ходов все понятно, будем считать количество открытых картинок и выводить это число в TextView. Для таймера есть класс Chronometer, который делает как раз то что нам нужно — отчитывает время. Класс унаследован от TextView, поэтому работать с ним можно как с обычным текстовым полем. Дополнительными являются методы:
void start () - запускает отсчет времени
void stop() - останавливает отсчет
void setFormat (String format) — устанавливает формат в котором будет выводится дата. По умолчанию форматом является MM:SS (или H:MM:SS). Можно задать свой формат, при этом в строке format первое встреченное «%s», будет заменено на «HH:MM». Например: «Time: %s» будет выводить время «Time: 01:30»

2. Добавим в main.xml TextView и Chronometr
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3.   android:layout_width="fill_parent"
  4.   android:layout_height="fill_parent"
  5.   android:orientation="vertical" >
  6.  
  7.   <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  8.     android:layout_width="fill_parent"
  9.     android:layout_height="wrap_content"
  10.     android:orientation="horizontal" >
  11.  
  12.     <TextView xmlns:android="http://schemas.android.com/apk/res/android" 
  13.       android:id="@+id/stepview"
  14.       android:textSize="30dp"
  15.       android:layout_weight="1"
  16.       android:layout_marginLeft="10dip"
  17.       android:layout_width="0dip" 
  18.       android:layout_height="wrap_content"
  19.     />
  20.  
  21.     <Chronometer xmlns:android="http://schemas.android.com/apk/res/android" 
  22.       android:id="@+id/timeview"
  23.       android:layout_width="100dip" 
  24.       android:layout_height="wrap_content"
  25.       android:textSize="30dp"
  26.     />
  27.  
  28.   </LinearLayout>
  29.  
  30.   <GridView xmlns:android="http://schemas.android.com/apk/res/android" 
  31.     android:id="@+id/field"
  32.     android:layout_width="fill_parent" 
  33.     android:layout_height="wrap_content"
  34.     android:gravity="center"
  35.   />
  36. </LinearLayout>
3. В классе MemoriaActivity напишем их обработку:
  1. public class MemoriaActivity extends Activity {
  2.  
  3.   ...
  4.   private TextView mStepScreen;
  5.   private Chronometer mTimeScreen;
  6.  
  7.   private Integer StepCount; // кол-во ходов
  8.  
  9.   @Override
  10.   public void onCreate(Bundle savedInstanceState) {
  11.  
  12.     ...
  13.  
  14.     mTimeScreen = (Chronometer) findViewById(R.id.timeview);
  15.     mStepScreen = (TextView)findViewById(R.id.stepview);
  16.  
  17.     // шрифт
  18.     Typeface type = Typeface.createFromAsset(getAssets(),"my-font.ttf"); 
  19.     mTimeScreen.setTypeface(type);
  20.     mStepScreen.setTypeface(type);
  21.  
  22.     StepCount = 0;
  23.     mStepScreen.setText (StepCount.toString());
  24.  
  25.     mTimeScreen.start();
  26.  
  27.     ...
  28.  
  29.     mGrid.setOnItemClickListener(new OnItemClickListener() {
  30.       @Override
  31.       public void onItemClick(AdapterView<?> parent, View v,int position, long id) {
  32.  
  33.         mAdapter.checkOpenCells ();
  34.         mAdapter.openCell (position);
  35.  
  36.         StepCount ++;
  37.         mStepScreen.setText (StepCount.toString());
  38.         if (mAdapter.checkGameOver())
  39.         {
  40.           mTimeScreen.stop();
  41.           String time = mTimeScreen.getText().toString();
  42.           String TextToast = "Игра закончена nХодов: " + StepCount.toString() + "nВремя: " + time;
  43.           Toast.makeText (getApplicationContext(), TextToast, Toast.LENGTH_SHORT).show();
  44.         }
  45.       }
  46.     });
  47.   }
  48. }
4. Запускаем:


5. Тут, кстати, обнаружилась небольшая ошибка в игре: нажатие на уже удаленную картинку считается ходом, что не правильно. Переделаем метод openCell() в классе GridAdapter, он должен проверять является ли карточка закрытой и только тогда ее открывать.
  1. public boolean openCell(int position) {
  2.   if (arrStatus.get(position) == Status.CELL_DELETE || arrStatus.get(position) == Status.CELL_OPEN)
  3.     return false;
  4.  
  5.   if (arrStatus.get(position) != Status.CELL_DELETE)
  6.     arrStatus.set(position, Status.CELL_OPEN);
  7.  
  8.   notifyDataSetChanged(); 
  9.   return true;
  10. }
А в классе MemoriaActivity, будем правильно считать ходы
  1.  mGrid.setOnItemClickListener(new OnItemClickListener() {
  2.   @Override
  3.   public void onItemClick(AdapterView<?> parent, View v,int position, long id) {
  4.  
  5.     mAdapter.checkOpenCells ();
  6.     if (mAdapter.openCell (position)) {
  7.       StepCount ++;
  8.       mStepScreen.setText (StepCount.toString());
  9.     }
  10.  
  11.     ...
  12.   }
  13. });
6. В коде есть такие строчки:
Typeface type = Typeface.createFromAsset(getAssets(),"my-font.ttf");
mTimeScreen.setTypeface(type);


Таким способом можно установить свой шрифт для TextView. Берем файл шрифта (у меня это 'my-font.ttf'), кладем его в директорию /assets.
Typeface — класс стилей шрифта.
createFromAsset(AssetManager mgr, String path) — создает экземпляр класса Typeface, используя файл из директории /assets.
setTypeface(Typeface) — устанавливает шрифт в TextView.

7. В xml и у TextView и у Chronometer также описаны параметры шрифта
android:textSize="30dp"
android:textColor="#FFFFFF"


Две строчки, конечно, можно и скопировать в два места, но если или строчек или мест будет много, то это неудобно, да и места много занимает. Создадим стиль шрифта. Добавим файл style.xml в директорию /res/values/. Напишем туда:
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <resources>
  3.   <style name="MyText">
  4.     <item name="android:textColor">#FFFFFF</item>
  5.     <item name="android:textSize">30dp</item>
  6.   </style>
  7. </resources>
А в main.xml заменим эти строчки у TextView и Chronometer на:
style="@style/MyText"

Теперь чтобы изменить стиль шрифта и в TextView и в Chronometer, надо изменить только файл style.xml, например добавим тень к тексту:

  1.   <style name="MyText">
  2.     <item name="android:textColor">#FFFFFF</item>
  3.     <item name="android:textSize">30dp</item>
  4.     <item name="android:shadowColor">#FF0000</item>
  5.     <item name="android:shadowDx">2</item>
  6.     <item name="android:shadowDy">2</item>
  7.     <item name="android:shadowRadius">3</item>
  8.   </style>
  9.  
8. Попробуем повернуть экран горизонтально (на эмуляторе это можно сделать нажав Ctrl + F11). Все повернулось, но выглядит теперь не очень хорошо. Здесь есть три решения.
- Первое запретить поворот зкрана, сам-то телефон конечно крутить можно, но изображение от этого меняться не будет. Для этого добавим строку android:screenOrientation="portrait" в файл AndroidManifest.xml:
  1. <activity
  2.   android:name=".MemoriaActivity"
  3.   android:label="@string/app_name" 
  4.   android:screenOrientation="portrait">
- Второй вариант написать другую разметку xml для горизонтальной ориентации экрана. Создадим директорию /res/layout-land, а в ней файл main.xml с новой разметкой. Более подробно про спецификаторы ресурсов я писала тут.

- И наконец третий способ, отловить поворот экрана программно, для этого добавим в AndroidManifest.xml за каким изменением конфигурации мы хотим следить
  1. <activity
  2.   android:name=".MemoriaActivity"
  3.   android:label="@string/app_name" 
  4.   android:configChanges="orientation">
А в класс MemoriaActivity добавим метод void onConfigurationChanged(Configuration newConfig), который вызывается каждый раз когда конфигурация аппарата меняется.
  1. public class MemoriaActivity extends Activity {
  2.  
  3. ...    
  4.  
  5.   @Override
  6.   public void onConfigurationChanged(Configuration newConfig) {
  7.     super.onConfigurationChanged(newConfig);
  8.     if (newConfig.orientation ==  Configuration.ORIENTATION_LANDSCAPE)
  9.       mGrid.setNumColumns(9);
  10.     if (newConfig.orientation ==  Configuration.ORIENTATION_PORTRAIT)
  11.       mGrid.setNumColumns(6);
  12.   }  
  13. }
Я в вертикальном расположении экрана рисую поле 6х6, а в горизонтальном 9х4.


9. Теперь сделаем начальный экран, у меня это будет четыре кнопки (Старт, Настройки, Рекорды, Выход) на каком-нибудь фоне. Создаем файл MemoriaStart.java с классом MemoriaStart (унаследованным от Activity) и разметку start.xml. Я сделаю две разметки для вертикальной и горизонтальной ориентации.


10. Если после этого запустить программу, то откроется окно MemoriaActivity, для того чтобы первым открывалось MemoriaStart изменим AndroidManifest.xml:
  1. <application
  2.   android:icon="@drawable/ic_launcher"
  3.   android:label="@string/app_name" >
  4.   <activity
  5.     android:name=".MemoriaStart"
  6.     android:label="@string/app_name">
  7.     <intent-filter>
  8.       <action android:name="android.intent.action.MAIN" />
  9.       <category android:name="android.intent.category.LAUNCHER" />
  10.     </intent-filter>
  11.   </activity>
  12.   <activity android:name=".MemoriaActivity" 
  13.     android:label="@string/app_name"
  14.     android:configChanges="orientation">
  15.   </activity>
  16. </application>
11. На нажатие кнопок «Настройка» и «Рекорды» пока никак реагировать не будем, а «Старт» и «Выход» сделаем:
  1. public class MemoriaStart extends Activity   {
  2.  
  3.   Button mStart;
  4.   Button mExit;
  5.  
  6.   @Override
  7.   public void onCreate(Bundle savedInstanceState) {
  8.     super.onCreate(savedInstanceState);
  9.     setContentView(R.layout.start);
  10.  
  11.     mStart = (Button)findViewById(R.id.butStart);
  12.     mExit = (Button)findViewById(R.id.butExit);
  13.  
  14.     mStart.setOnClickListener (new OnClickListener() {
  15.       @Override
  16.       public void onClick(View v) {
  17.         startGame();
  18.       }
  19.     });
  20.  
  21.     mExit.setOnClickListener (new OnClickListener() {
  22.       @Override
  23.       public void onClick(View v) {
  24.         finish();
  25.       }
  26.     });
  27.   }
  28.  
  29.   private void startGame () {
  30.     Intent i = new Intent(this, MemoriaActivity.class);
  31.     startActivity (i);
  32.   }
  33. }
Для того чтобы открыть другое окно программы используется метод void startActivity (Intent intent), которому передается экземпляр класса Intent.
Intent (намерение) — структура данных, содержащая информацию о том, какое действие необходимо выполнить. В данном случае, мы вызываем окно MemoriaActivity.

12. Запускаем, теперь при запуске программы, вначале показывается окно с кнопками, при нажатии на «Старт» запускается игра, а кнопка «Выход» закрывает приложение. Чтобы из игры вернуться на начальный экран можно нажать кнопку «Назад» на телефоне, но для наглядности после завершения игры будем показывать не просто всплывающее сообщение, а диалоговое окно с кнопкой, по нажатию на которое будем возвращаться назад.
В классе MemoriaActivity при завершении игры вызовем функцию ShowGameOver()
  1. public class MemoriaActivity extends Activity {
  2. ...       
  3.   mGrid.setOnItemClickListener(new OnItemClickListener() {
  4.     @Override
  5.     public void onItemClick(AdapterView<?> parent, View v,int position, long id) {
  6.  
  7.       ...
  8.       if (mAdapter.checkGameOver())
  9.       {
  10.         mTimeScreen.stop();
  11.         ShowGameOver();
  12.       }
  13.     }
  14.   });
  15. }
И напишем саму функцию
  1. public class MemoriaActivity extends Activity {
  2.   ...
  3.  
  4.   private void ShowGameOver () {
  5.  
  6.     // Диалоговое окно
  7.     AlertDialog.Builder alertbox = new AlertDialog.Builder(this);
  8.  
  9.     // Заголовок и текст
  10.     alertbox.setTitle("Поздравляем!");
  11.     String time = mTimeScreen.getText().toString();
  12.     String TextToast = "Игра закончена nХодов: " + StepCount.toString() + "nВремя: " + time;
  13.     alertbox.setMessage(TextToast);
  14.  
  15.     // Добавляем кнопку 
  16.     alertbox.setNeutralButton("Ok"new DialogInterface.OnClickListener() {
  17.       public void onClick(DialogInterface arg0, int arg1) {
  18.         // закрываем текущюю Activity
  19.         finish();
  20.       }
  21.     });
  22.     // показываем окно
  23.     alertbox.show();
  24.   }
  25. }
AlertDialog — класс диалоговых окон с одной, двумя или тремя кнопками или вообще без кнопок.
AlertDialog.Builder — вспомогательный класс, который создает диалоговое окошко.

Методы setTitle(CharSequence title) и setMessage(CharSequence message) задают соответственно заголовок и текст сообщения.

setNeutralButton(CharSequence text, DialogInterface.OnClickListener listener) — создает в окне кнопку и устанавливает обработчик нажатия на кнопку.

show() - создает и показывает созданное окно.



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

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

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

1 комментарий:

  1. Счетчик ходов работает некорректно, одно нажатие выдает за 2 и еще при нажатии на пустое поле тоже засчитываеь как ход, или нажатие на открытую картинку...

    ОтветитьУдалить