Пишем игру для 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

, все кнопки работают.

Настройки

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

Сама форма с настройками делается достаточно просто. Есть специальный наследник класса

Activity

PreferenceActivity

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

Разметка

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

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

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