Пишем игру для Android. Часть 5. Хранение настроек

Пишем игру для Android. Часть 5. Хранение настроек

27.03.2009

Источник: megadarja.blogspot.com

Вот мы и добрались до конца. Осталось сделать только главное меню
приложения, а также сделать игре настройки (собственно, меню только для
того и нужно, чтобы было откуда настройки вызывать). Ну первое мы еще с
прошлой статьи умеем, так что особых сложностей быть не должно. А вот
второе следует рассмотреть подробнее. Итак, приступим.

Окно приветствия

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

На нашей форме приветствия будет
какая-нибудь картинка и три кнопки: "Начать игру", "Настройки" и
"Выход". Картинку в формате png, которую мы назовем start.png нужно положить в папку /res/drawable. Названия кнопок нужно вынести в strings.xml, добавив следующие строки:

res/values/strings.xml

<resources>
<string name="app_name">PingPong</string>
<string name="start_title">Start Game</string>
<string name="settings_title">Settings</string>
<string name="exit_title">Exit</string>
</resources>

Тогда разметка для новой формы будет выглядеть так:

res/layout/start.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"
android:gravity="bottom"
android:background="@drawable/start"
android:padding="8dip">

<Button android:id="@+id/StartButton"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textStyle="bold"
android:text="@string/start_title" />

<Button android:id="@+id/SettingsButton"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textStyle="bold"
android:text="@string/settings_title" />

<Button android:id="@+id/ExitButton"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textStyle="bold"
android:text="@string/exit_title" />
</LinearLayout>

Фоновое изображение можно задать экрану с помощью полезного свойства android:background. Собственно, так можно задавать фон и кнопкам, и вообще чему угодно. Получили вот такую разметку:

Экран приветствия

Добавим соответствующий этой разметке класс StartScreen.java. Сразу обработаем нажатия всех кнопок:

StartScreen.java

public class StartScreen extends Activity implements OnClickListener
{
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.start);

// Кнопка "Start"
Button startButton = (Button)findViewById(R.id.StartButton);
startButton.setOnClickListener(this);

// Кнопка "Exit"
Button exitButton = (Button)findViewById(R.id.ExitButton);
exitButton.setOnClickListener(this);

// Кнопка "Settings"
Button settingsButton = (Button)findViewById(R.id.SettingsButton);
settingsButton.setOnClickListener(this);
}

/** Обработка нажатия кнопок */
public void onClick(View v)
{
switch (v.getId())
{
case R.id.StartButton:
{
Intent intent = new Intent();
intent.setClass(this, GameScreen.class);
startActivity(intent);
break;
}

case R.id.SettingsButton:
{
break;
}

case R.id.ExitButton:
finish();
break;

default:
break;
}
}
}

По нажатию на кнопку Start происходит переход на экран с игрой. Обратите внимание, что StartScreen при этом не закрывается. Это значит, что, когда закроется StartScreen, мы попадем обратно на экран приветствия. По нажатию на Settings пока что ничего не происходит, по Exit — приложение закрывается.

Осталось только зарегистрировать эту форму в приложении и сделать ее главной. Для этого идем в AndroidManifest.xml:

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.pingpong"
android:versionCode="1"
android:versionName="1.0.0">
<application android:icon="@drawable/icon" android:label="@string/app_name">
<activity android:name=".GameScreen"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.SAMPLE_CODE" />
</intent-filter>
</activity>
<activity android:name=".StartScreen">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

Теперь приложение начинается с StartScreen, все кнопки работают.

Настройки

Я
сделаю в настройках два параметра — максимальное количество очков и
сложность. Сложность игры будем менять, варьируя скорость мячика и
ракеток.

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

Разметка

Разметка для формы с настройками выглядит несколько необычно:

res/xml/preferences.xml

<?xml version="1.0" encoding="UTF-8"?>
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android" >
<PreferenceCategory
android:title="@string/prefs_title">

<ListPreference android:key="@string/pref_difficulty"
android:title="@string/difficulty_title"
android:entries="@array/difficulty"
android:entryValues="@array/difficulty"
android:defaultValue="1"
/>

<EditTextPreference android:key="@string/pref_max_score"
android:title="@string/score_title"
android:defaultValue="10"
/>
</PreferenceCategory>
</PreferenceScreen>

Настолько необычно, что, если поместить этот XML в папку
layout, eclipse начнет ругаться, что не может разрезолвить такие
классы. Собственно, это не просто разметка: там также содержатся ключи
настроек и значения по умолчанию. Поэтому мы создадим отдельную папку xml, и поместим этот файл туда. А теперь обо всем по порядку.

PreferenceCategory

Ну, PreferenceScreen все понятно, а что такое PreferenceCategory?
Как ни удивительно, это категория настроек. Например, у какой-нибудь
игры могут быть настройки графики, настройки звука, настройки сети и
т.д.. Удобно отобразить их сгруппированными, вот так:

Пример формы с настройками

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

Какие можно сделать настройки

Как видно
даже не прошлой картинке, настройки могут быть разными. И флажки, и
текстовые поля, и списки. Все они происходят от одного класса Preference, и наследуют от него всякие полезные атрибуты, которые можно задавать в разметке, как то:

android:title
Заголовок настройки или контейнера настроек.
android:summary
Краткое описание. Проще говоря, это то, что пишется под заголовком мелким шрифтом.
android:defaultValue
Значение по умолчанию
android:key
Ключ, с которым данная настройка будет храниться и с которым можно будет к ней обращаться.
android:dependency
Задает
зависимость от другого контрола. Например, можно поставить эдитору
зависимость от флажка, и тогда, если флажок не установлен, но эдитор
будет неактивен.

Ну и еще кое-что. Рассмотрим некоторые конкретные виды настроек.

CheckBoxPreference

Вот такой флажок:

Флажок
EditTextPreference

Редактор текста.

Редактор текста

Честно
говоря, у редактора текста очень хотелось бы валидатор, а еще лучше
маску. Например, IP-адрес имеет специфический формат, и хотелось бы
запрещать пользователю вводить там что попало. Но мне такую
функциональность так и не удалось обнаружить.

ListPreference

Своеобразная реализация Dropdown-а. Хотя, на телефоне наверно и вправду так удобнее.

Список

На
этом контроле хотелось бы остановиться подробнее. А конкретнее,
рассказать, откуда он, собственно, берет элементы списка. А берет он их
из ресурсов с помощью таких атрибутов.

android:entries

Здесь
хранится ссылка на ресурс, в котором хранятся отображаемые элементы
списка. Все значения, которые хочется вынести в ресурсы, хранятся в
папке values. До сих пор там была только одна XML-ка — strings.xml. Но теперь надо добавить еще одну — arrays.xml. И добавить в узел resources следующее:

<string-array name="performance">
<item>Best performance</item>
<item>Normal performance and appearance</item>
<item>Best appearance</item>
</string-array>

После этого можно смело указывать в android:entries этот ресурс, список будет загружен.

Кстати говоря, в item-ах может быть не непосредственно строка, а ссылка на строку в strings.xml. Например, в нашем случае будет так (разумеется, стоит добавить соответствующие значения в strings.xml):

res/values/arrays.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="difficulty">
<item>@string/difficulty_easy</item>
<item>@string/difficulty_normal</item>
<item>@string/difficulty_hard</item>
</string-array>
</resources>
android:entryValues
Список действительных
значений. Также ссылка на ресурс, и задавать можно так же. Если кодов
будет меньше, чем значений, приложение будет валиться с исключением.
Если больше — не будет. В нашем случае можно в entries и entryValues
смело задавать одно и то же, но бывает, когда имеет смысл их разделять.

А еще мне никак не удалось победить у ListPreference атрибут adnroid:defaultValue. Не работает и все.

RingtonePreference

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

Рингтон

Класс формы

Класс для формы с настройками будет выглядеть так:

SettingsScreen.java

public class SettingsScreen extends PreferenceActivity
{
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);

// Настройки и их разметка загружаются из XML-файла
addPreferencesFromResource(R.xml.preferences);
}
}

Кстати, настройки необязательно загружать из XML-ки,
можно добавлять все эти настройки прямо в коде конструктора. В сэмплах,
которые идут с Android SDK, такие примеры есть.

Добавляем в StartScreen код для открытия формы настроек, прописываем SettingsScreen в AndroidManifest.xml. (Все выглядит точно так же, как и для GameScreen, так что листинги не привожу). И увидим мы следующее:

Форма с настройками

Не
знаю, кому как, а мне нравится, когда рядом с названием опции написано
ее значение. Но как это сделать автоматически, я так и не нашла,
пришлось все делать руками, используя для этого поле summary. Итак, summary настройки должно обновляться при изменении значения. По счастью, есть такое событие OnPreferenceChange. Итак, пишем:

SettingsScreen.java

public class SettingsScreen extends PreferenceActivity
 implements Preference.OnPreferenceChangeListener
{
/* Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);

// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.preferences);
ListPreference difficulty =
(ListPreference)this.findPreference("pref_difficulty");
difficulty.setSummary(difficulty.getEntry());
difficulty.setOnPreferenceChangeListener(this);

EditTextPreference maxScore =
(EditTextPreference)this.findPreference("pref_max_score");
maxScore.setSummary(maxScore.getText());
maxScore.setOnPreferenceChangeListener(this);
}
public boolean onPreferenceChange(Preference preference, Object newValue)
{
preference.setSummary((CharSequence)newValue);

return true;
}
}

Думаю, все понятно без слов. Теперь мы видим такую картину:

Настройки и их значения

И, если мы будем менять настройки, изменения сразу же будут отображаться в summary.

Использование настроек в других формах

Ну
все, настройки мы сделали, они как-то сами где-то сохранились. Теперь
возникла необходимость их прочитать и что-нибудь с ними сделать. Читать
и делать мы будем в классе GameManager, а конкретно, в конструкторе. Для работы с сохраненными настройками приложения используется класс SharedPreferences. Вся работа по чтению и применению настроек выглядит так:

SettingsScreen.java

public GameManager(SurfaceHolder surfaceHolder, Context context)
{
...

// стили для рисования игрового поля
...

// игровые объекты
...
// применение настроек
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context);

String difficulty = settings.getString(res.getString(R.string.pref_difficulty),
 res.getString(R.string.difficulty_normal));
setDifficulty(difficulty);

int maxScore = Integer.parseInt(settings.getString(
res.getString(R.string.pref_max_score), "10"));
setMaxScore(maxScore);
}

Метод setDifficulty приводить не буду, там ничего особо умного не написано.

Настройки из SharedPreferences можно читать с помощью методов getString, getInt, getBoolean и т.п. Все они принимают два параметра &mdahs; ключ к настройке (то, что мы задавали с помощью атрибута android:key) и значение по умолчанию. Однако, воспользоваться чем-то кроме getString

мне так и не удалось.

Заключение

Итак,
мануал закончен. Получился он огромным, но при этом собственно про
андроид оказалось не так уж и много, что самое-то обидное :( Спасибо,
если кто дочитал до конца. Буду рада услышать любые мнения.

Отдельное спасибо xeye и std.denis

А вот и все исходники