пятница, 27 марта 2009 г.

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

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

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

В одной из прошлых статей подробно рассматривался вопрос, как создавать формы в приложении для 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

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

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

сергей комментирует...

Вы - молодец, Даша! :)

lostop комментирует...

Даша, я поддерживаю сергея.
Считаю Вам на эту тему нужно развивать

Анонимный комментирует...

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

Анонимный комментирует...

Даша, вы сделали очень полезный и важный труд, для развитии программирования под андроид в русскоязычном инете. Просим продолжения блога! В особенности считаю, что не у каждого опытного программиста хватит терпения и целеустремленности так оформить и объяснить код.

mikich комментирует...

Спасибо огромное Даша!
За развитие Андроида в руНете!
За проделанную работу
Очень надеюсь на продолжение серии статей по теме.

Broderix комментирует...

Отличная работа!

Анонимный комментирует...

Я только начал окучивать направление Java и Android, но с С/С++ перенастроиться на Java, а с PocketPC на Android довольно тяжело. Пока я ломал голову над простенькой начальной игрушкой и над проблемами слоев в памяти/на экране и столкновения спрайтов, тут все уже за меня сделано. Спасибо.
Сделайте, плз, теперь что-нибудь многоэкранное :-).

darja комментирует...

2 Анонимный
Многоэкранные приложения довольно подробно рассмотрены в прошлой серии статей. Или Вы какую многоэкранность имеете в виду?

Анонимный комментирует...

Да, Life я видел. Я имел в виду режим плавного скроллинга, когда игровое поле больше ширины экрана. Но это уже сложная штука, согласен. Лично мне бы с нынешним 2D разобраться :-). И кроме как на таких примерах, негде (SDK и книга DiMarzio не катят). Так что на вас, Даша, вся наша надежда ;-). Всяческих вам регардов. Николай.

Yevgeniy комментирует...

Спасибо, огромное за проделанную работу. Первый блог в рунете на который я встретил с описанием 2D игры. Интересно было бы прочитать про моделирование игры с игровым полем сверх размеров экрана.

Vermus комментирует...

Потрясающая серия. Впечатлен. :)

Skiif комментирует...

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

Можно использовать получение EditText из EditTextPreference и вешать на него обработчики или указывать тип ввода. Например:
((EditTextPreference)this.findPreference(getString(R.string.pref_phone))).getEditText().setInputType(InputType.TYPE_CLASS_NUMBER);

Anton Rymarchuk комментирует...

Wow! Bravo!
Так подробно и четко описали все шаги и трудности в процессе разработки...
Очень хороший пример, спасибо.

Galina комментирует...

Спасибо, добрый человек. Весьма полезно.

hkvd комментирует...

Спасибо большое за статьи! Жду продолжения :)

Nick Pepper комментирует...

да, статья очень подробная, большое спасибо! ждём-с продолжений

а кто-нибудь понял, отчего возникает такая бага (и как её пофиксить) - мячик как бы "зависает", "прилипает" к ракетке иногда?

Boris Yu. комментирует...

Спасибо.

Анонимный комментирует...

а предусмотрена ли для андроида возможность "резать" png-шки?
например, знаю что на С++ для создания анимашки считываем файлик, который условно порезан на кадры. потом во время рисования выводим тот кадр, который необходим. пересмотрел уже много приложений под андроид, и нигде с подобным не стыкался. или все сделано так что достатоочно 1 кадрика или порезано на кучу маленьких кусочков. потом попробуй разберись в тех ошметках что надо выводить...

извините, если туплю просто под андроидом не работал еще. и не знаю разумно ли "резать" файлы. или мы там наткнемся на какие-то проблемы...

наталья комментирует...

Даша,спасибо огромное за понятное объяснение всех программ для андроида.есть маленькая просьба.если не трудно,нужен пример как брать данные из json file в андроиде.например брать координаты с json file и по ним нарисовать треугольник.пыталась сделать сама,но столкнулась с проблемой занесения данных в буфер.прошу помощи.спасибо заранее!!!!

Станислав Логинов комментирует...

Спасибо! Все наглядно и толково! Хочется продолжения, а потом можно и книжку написать!

Alexey комментирует...

Даешь книгу?!?

"Android. Программирование приложений." автор Дарья Ряжских

Анонимный комментирует...

очень подробно написано. только как заменять изображение ракетки при ударе по мячу. пробовал через mImage, но начинает тормозить.

Константин комментирует...

Дарья, Вы большой молодец, спасибо Вам. Получилось решить проблему android:defaultValue для ListPrefernce

http://paste.org/pastebin/view/23309

Victorio комментирует...

Джава огромен. Изучать его с нуля трудно. Эти уроки мне очень помогли. Перечитываю их опять и опять. И, конечно, жду продолжения банкета! :-)

PS Поздравляю с успешным завершением путешествия! :-)

Анонимный комментирует...

интересно, а можно ли в событии onPreferenceChange узнать о значениях других параметров и изменить их?

Роман комментирует...

Спасибо за мануал, Дарья! Очень пригодится.

blekel комментирует...

молодец, Дарья! респект )

Макс комментирует...

Дарья, большое спасибо!
Отличные статьи, очень аккуратно, внятно и доходчиво написано!

OnDroid комментирует...

И в этой статье опечаточки....

«Как видно даже не прошлой картинке»
Не «не» а «на».

«если флажок не установлен, но эдитор будет неактивен.»
Не «но» а «то».

Анонимный комментирует...

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

Анонимный комментирует...

С удовольствие прочитал Вашу статью. Кто может подсказать, как определить координаты клика на экране? По сенсору.

Анонимный комментирует...

"Анонимный комментирует...

С удовольствие прочитал Вашу статью. Кто может подсказать, как определить координаты клика на экране? По сенсору."

Сам нашел ответ на свой вопрос. Реализовал так:
public class GameSurface (тут GameView) extends SurfaceView implements SurfaceHolder.Callback, OnTouchListener

в конструктор добавляем такую строчку:
findViewById(R.id.game).setOnTouchListener(this);

дальше в этом классе добавляем метод:

public boolean onTouch(View v, MotionEvent event)
{
int action = event.getAction();
int x = (int) event.getX();
int y = (int) event.getY();

return mThread.onTouch(v, event);
}

теперь можно выполнять действия по прикосновению на экран (onTouch) или сделать Х и У public и получать доступ из других классов.

UnChanter комментирует...

Скачал исходники, импортировал в эклипс, импортировал, не компилирует - пишет три ошибки:


Description Resource Path Location Type
Project has no default.properties file! Edit the project properties to set one
The project was not built since its build path is incomplete. Cannot find the class file for java.lang.Object. Fix the build path then try building this project
The type java.lang.Object cannot be resolved. It is indirectly referenced from required .class files

подскажие что делать

TIgor комментирует...

Отличный мануал! Спасибо!

Анонимный комментирует...

Согласна, очень хороший мануал для начинающего. Мне очень помогло. Сижу радуюсь "результатам", вроде сама сделала :)
Дальше уже как-то проще должно быть, есть от чего отталкиваться. А то не знала с какой стороны ко всему этому подходить.
Спасибо большое.

Анонимный комментирует...

Большое спасибо за статью! Читал с интересом, очень классно написано :)

Анонимный комментирует...

У Вас талант, Даша. Советую вам развиватсья в этом направлении.Многие скажут, что талантливые люли талантливы во всём, но я думаю, что это особый случай. БОЛЬШОЕ СПАСИБО.

Georg комментирует...

спасибо вам огромное, Даша:)

Анонимный комментирует...

Огромное спасибо, Дарья за труд. Но я наткнулся на одну ошибку, характерную в java после C# - сравнение строк.

метод
private void setDifficulty(String difficulty)
{
if (difficulty == "Easy")
{

А правильно сравнение строк работает вот так difficulty.equals("Easy")

Леша комментирует...

Помогите, пжл, новичку. Я собрал проект используя ваши статьи, но вот беда - мяч иногда (когда касается угла ракетки) как-бы... начинает через ракетку компьютера проходить... То есть ракетка компьютера загоняет мячик в угол поля, он начинает дергаться и потом проходит через саму ракету... Вообщем, очень страшно выходит... Возможно в логике ракетки что-то не так?

Леша комментирует...

И еще одна проблема. По-моему какой бы режим игры не ставь, параметры не меняются. То есть:
mBall.setSpeed(2);
mUs.setSpeed(3);
mThem.setSpeed(3);
Что так:
mBall.setSpeed(3);
mUs.setSpeed(1);
mThem.setSpeed(1);
Видимо в конструкторе Ball перекрывается: mSpeed = DEFAULT_SPEED; // задали скорость по умолчанию

Леша комментирует...

Согласен с верхним комментарием. private void setDifficulty(String difficulty)
{
if (difficulty == "Easy")
{

difficulty.equals("Easy")

Тогда работает