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

Пишем игру для Android. Часть 4. Игровой процесс

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

Обработка проигрыша

Помнится, мы заводили в классе Racquet поле mScore, в котором собирались хранить количество очков у игрока. Теперь самое время начать использовать это поле.

Итак, в начале игры количество очков у обоих игроков пусто. Когда игрок не успевает отбить мяч, его противнику засчитывается очко, мячик и ракетки возвращаются на исходные позиции. Игра продолжается, пока какой-нибудь из игроков не наберет N очков. N мы пока что объявим константой, а в следующей части вынесем в настройки.

Проверка проигрыша должна осуществляться также в методе updateObjects() GameManager-а. Описанная нами логика запишется так:

GameManager.java

private void updateObjects()
{
    ...
// проверка проигрыша if (mBall.getBottom() < mThem.getBottom()) { mUs.incScore(); reset(); } if (mBall.getTop() > mUs.getTop()) { mThem.incScore(); reset(); }
}

Racquet.incScore() увеличивает на 1 количество очков у игрока:

Racquet.java

/** Увеличить количество очков игрока */
public void incScore()
{
    mScore++;
}

GameManager.reset() расставляет ракетки и мячик на исходные позиции, задает мячику новый случайный угол, а также делает паузу (чтобы игрок успел понять, что произошло).

GameManager.java

private void reset()
{
    // ставим мячик в центр
    mBall.setCenterX(mField.centerX());
    mBall.setCenterY(mField.centerY());
    // задаем ему новый случайный угол
    mBall.resetAngle();

    // ставим ракетки в центр
    mUs.setCenterX(mField.centerX());
    mThem.setCenterX(mField.centerX());

    // делаем паузу
    try
    {
        sleep(LOSE_PAUSE);
    }
    catch (InterruptedException iex)
    {
    }
}

LOSE_PAUSE — это константа класса GameManager, в которой задается длина паузы в миллисекундах (у меня она равна 2000). Метод же resetAngle() класса Ball выглядит следующим образом:

Ball.java

/** Задает новое случайное значение угла */
public void resetAngle()
{
    mAngle = getRandomAngle();
}

Если теперь запустить приложение, то увидим, что, если упустить мячик, то он никуда не улетит, а через некоторое время восстановится в центре. А про очки пока ничего сказать нельзя, потому что они нигде не выводятся. Что ж, будем выводить.

Вывод количества очков

Вывод текста на экран производится с помощью метода drawText(String text, float x, float y, Paint paint) класса Canvas. Как можно заметить, стили текста задаются с помощью экземпляра класса Paint. Где-то в первой части мы создавали в GameManager такое поле mPaint, где хранились стили для рисования игрового поля. Для вывода текста можно использовать это же поле, и при каждой перерисовке экрана задавать ему стили сначала для игрового поля, а потом для текста. А можно завести отдельный экземпляр Paint для хранения стилей текста:

GameManager.java

private Paint mScorePaint;

Инициализировать его в конструкторе:

GameManager.java

public GameManager(SurfaceHolder surfaceHolder, Context context)
{
    mSurfaceHolder = surfaceHolder;
    Resources res = context.getResources();
    mRunning = false;

    // стили для рисования игрового поля
    mPaint = new Paint();
    mPaint.setColor(Color.BLUE);
    mPaint.setStrokeWidth(2);
    mPaint.setStyle(Style.STROKE);
// стили для вывода счета mScorePaint = new Paint(); mScorePaint.setTextSize(20); mScorePaint.setStrokeWidth(1); mScorePaint.setStyle(Style.FILL); mScorePaint.setTextAlign(Paint.Align.CENTER);
// игровые объекты mField = new Rect(); mBall = new Ball(res.getDrawable(R.drawable.ball)); mUs = new Racquet(res.getDrawable(R.drawable.us)); mThem = new Racquet(res.getDrawable(R.drawable.them)); }

А непосредственно вывод счета игры производится в методе, где происходит вся отрисовка текущей игровой ситуации — refreshCanvas

GameManager.java

private void refreshCanvas(Canvas canvas)
{
    // вывод фонового изображения
    canvas.drawBitmap(mBackground, 0, 0, null);

    // рисуем игровое поле
    canvas.drawRect(mField, mPaint);

    // рисуем игровые объекты
    mBall.draw(canvas);
    mUs.draw(canvas);
    mThem.draw(canvas);
// вывод счета mScorePaint.setColor(Color.RED); canvas.drawText(String.valueOf(mThem.getScore()), mField.centerX(), mField.top - 10, mScorePaint); mScorePaint.setColor(Color.GREEN); canvas.drawText(String.valueOf(mUs.getScore()), mField.centerX(), mField.bottom + 25, mScorePaint);
}

Правда, совсем уж без изменения стиля не обошлось. Наши очки мы рисуем зелёным, а очки противника — красным. Запустив, увидим примерно такую картину:

Выводится количество очков

Использование пользовательских шрифтов

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

В нашем проекте есть такая папка assets, там хранятся такие ресурсы, как TrueType-шрифты, возможно, какие-то большие тексты и т.д.. Основное отличие их от ресурсов, которые хранятся в папке res — это то, что используются они гораздо реже, и доставать их оттуда сложнее. Ресурсы из res можно запросто достать с помощью класса R, а assets вытаскиваются с помощью специального класса AssetManager.

Итак, создадим в папке assets папку fonts и кинем туда шрифт под названием Mini.ttf. Теперь, чтобы достать этот шрифт и использовать его для вывода количества очков, достаточно добавить в инициализацию mScorePaint в конструкторе одну строчку:

GameManager.java

mScorePaint.setTypeface(Typeface.createFromAsset(context.getAssets(), "fonts/Mini.ttf"));

context.getAssets() получит менеджер ресурсов (AssetManager) для данного приложения, откуда потом будет можно загрузить шрифт по указанному пути. Стоит обратить внимание, что путь является case-sensitive, т.е. "fonts/mini.ttf" уже ничего не загрузит.

Загружен пользовательский шрифт

Неприятность

И всё бы хорошо, но теперь время от времени стали возникать ситуации, когда в начале игры у одного из игроков выводится не 0 очков, а 1. Я так понимаю, что проблемы возникают в самом начале программы, перед initPositions, когда у игровых объектов координаты еще не заданы, а updateObjects уже вызывается. Чтобы исправить положение, заведем в классе GameManager еще одно булево поле mInitialized, в конструкторе зададим как false, а в initPositions присвоим ему true. Тогда в run можно написать так:

GameManager.java

public void run()
{
    while (mRunning)
    {
        Canvas canvas = null;
        try
        {
            // подготовка Canvas-а
            canvas = mSurfaceHolder.lockCanvas();
            synchronized (mSurfaceHolder)
            {
if (mInitialized) {
updateObjects(); // обновляем объекты refreshCanvas(canvas); // обновляем экран sleep(20); } } } catch (Exception e) { } finally { if (canvas != null) { mSurfaceHolder.unlockCanvasAndPost(canvas); } } } }

Теперь гарантированно не будет происходить никаких проверок, пока не будут инициализированы координаты игровых объектов. Проблема решена.

Обработка окончания игры

Прежде всего, нам следует завести в GameManager переменную, где бы хранилось количество очков, до которого идет игра. Заведем такую переменную и сразу сеттер к ней. Итак:

GameManager.java

/** Максимальное число очков, до которого идет игра */
private static int mMaxScore = 5;

public static void setMaxScore(int value)
{
    mMaxScore = value;
}

Саму проверка на окончание игры можно поместить как в метод updateObjects(), так и прямо в run(). Но, думаю, правильнее именно в updateObjects():

GameManager.java

/** Обновление состояния игровых объектов */
private void updateObjects()
{
    // Обновление состояния игровых объектов
    ...

    // обработка столкновений
    ...

    // проверка проигрыша
    ...

// проверка окончания игры if (mUs.getScore() == mMaxScore mThem.getScore() == mMaxScore) { this.mRunning = false; }
}

Напомню, что метод run выглядит так:

GameManager.java

public void run()
{
    while (mRunning)
    {
        // обновление и отрисовка объектов
    }
}

То есть, когда mRunning станет равным false, поток завершится. Раз он завершился — игра закончена, и надо вывести на экран ее результаты. Так что логично видеть в методе run() что-то вроде:

GameManager.java

public void run()
{
    while (mRunning)
    {
        // обновление и отрисовка объектов
    }
    // рисование GameOver
    ...
}

А теперь разберемся, как это рисование может выглядеть. Как известно, при рисовании мы лочим Canvas, рисуем, и затем разлочиваем. При этом еще нужно отловить возможные исключения. Получается куча кода, которая появляется при каждом рисовании и сильно загромождает текст программы. Естественно, хочется вынести все это в метод-обертку и передавать туда ссылку на функцию, осуществляющую собственно рисование. На C# это выглядело бы так:

delegate void DrawFunction(Canvas canvas);

private void draw(DrawFunction something)
{
    Canvas canvas = null;
    try
    {
        canvas = mSurfaceHolder.lockCanvas();
        synchronized (mSurfaceHolder)
        {
            something(canvas);
        }
    }
    }
    catch (Exception e) { }
    finally
    {
        if (canvas != null)
        {
            mSurfaceHolder.unlockCanvasAndPost(canvas);
        }
    }
}

Но здесь нам не C#, здесь климат иной, и делегатов нет. Однако, как мне подсказал товарищ xeye, подобный код можно написать. Итак, добавим в GameManager такой интерфейс:

GameManager.java

private interface DrawHelper
{
    void draw(Canvas canvas);
}

И такой метод, куда мы вынесем всю работу по подготовке canvas-а:

GameManager.java

private void draw(DrawHelper helper)
{
    Canvas canvas = null;
    try
    {
        // подготовка Canvas-а
        canvas = mSurfaceHolder.lockCanvas();
        synchronized (mSurfaceHolder)
        {
            if (mInitialized)
            {
                helper.draw(canvas);
            }
        }
    }
    catch (Exception e) { }
    finally
    {
        if (canvas != null)
        {
            mSurfaceHolder.unlockCanvasAndPost(canvas);
        }
    }
}

Теперь можно завести конкретные реализации DrawHelper на все случаи жизни. Я добавляю их две:

GameManager.java

/** Хелпер для перерисовки экрана */
private DrawHelper mDrawScreen;

/** Хелпер для рисования результата игры*/
private DrawHelper mDrawGameover;

Инициализирую в конструкторе таким образом:

GameManager.java

public GameManager(SurfaceHolder surfaceHolder, Context context)
{
    ...
   
    // функция для рисования экрана
    mDrawScreen = new DrawHelper()
    {
        public void draw(Canvas canvas)
        {
            refreshCanvas(canvas);
        }
    };

    // функция для рисования результатов игры
    mDrawGameover = new DrawHelper()
    {
        public void draw(Canvas canvas)
        {
            // Вывели последнее состояние игры
            refreshCanvas(canvas);
           
            // смотрим, кто выиграл и выводим соответствующее сообщение
            String message = "";
            if (mUs.getScore() > mThem.getScore())
            {
                mScorePaint.setColor(Color.GREEN);
                message = "You won";
            }
            else
            {
                mScorePaint.setColor(Color.RED);
                message = "You lost";
            }
            mScorePaint.setTextSize(30);
            canvas.drawText(message, mField.centerX(), mField.centerY(), mScorePaint);               
        }
    };
}

После этого метод run() преображается до неузнаваемости:

GameManager.java

/** Действия, выполняемые в потоке */
public void run()
{
    while (mRunning)
    {
        if (mInitialized)
        {
            updateObjects(); // обновляем объекты
            draw(mDrawScreen);
        }
    }
    draw(mDrawGameover);
}

И сразу результат:

Результат игры

Пауза

Тут совсем кратко. Объявим в классе GameManager поле:

GameManager.java

/** Стоит ли приложение на паузе */
private boolean mPaused;

Если приложение на паузе, поток работает "вхолостую", т.е. состояния объектов не меняются и вообще ничего не происходит. Это значит, в методе run() будет следущее:

GameManager.java

public void run()
{
    while (mRunning)
    {
if (mPaused) continue;
if (mInitialized) { updateObjects(); // обновляем объекты draw(mDrawScreen); } } draw(mDrawGameover); }

Будем ставить приложение на паузу, если нажата средняя клавиша джойстика:

GameManager.java

public boolean doKeyDown(int keyCode)
{
    switch (keyCode)
    {
        case KeyEvent.KEYCODE_DPAD_LEFT:
        case KeyEvent.KEYCODE_A:
            mUs.setDirection(GameObject.DIR_LEFT);
            return true;
        case KeyEvent.KEYCODE_DPAD_RIGHT:
        case KeyEvent.KEYCODE_D:
            mUs.setDirection(GameObject.DIR_RIGHT);
            return true;
case KeyEvent.KEYCODE_DPAD_CENTER: mPaused = !mPaused; draw(mDrawPause); return true;
default: return false; } }

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

Итак

У нас уже совсем готовая игра. Можно играть, выигрывать, проигрывать, ставить на паузу.

Исходники примера

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

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

воскресенье, 21 декабря 2008 г.

Еще про log4net

В комментариях к прошлой статье про log4net задал мне товарищ следующий вопрос:

Вопрос: ты знаешь, как заставить log4net создавать лог-файлы для каждого экземпляра интерфейса ILog?
Т.е. есть у меня
static log4net.ILog log = log4net.LogManager.GetLogger("My_mega_logger");
Теперь я хочу, чтобы лог-файл назывался "log_My_mega_logger.log".

Я подумала-подумала, да и придумала решение. Заодно, кажется, разгадала секрет, зачем в конфиге нужен элемент logger. Итак, вот что получилось.

Для примера я создаю консольное приложение, и цепляю в References библиотеку log4net.

App.config

Добавляем в проект конфигурационный файл и пишем в нем, например, такое

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" />
  </configSections>
  <log4net debug="true">
    <appender name="SomeAppender" type="log4net.Appender.RollingFileAppender">
      <lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
      <file value="Logs\some.log" />
      ...
      <layout type="log4net.Layout.PatternLayout">
        <conversionPattern value="%-5p [%d] [%M] %m%n" />
      </layout>
    </appender>
    <appender name="OtherAppender" type="log4net.Appender.RollingFileAppender">
      <lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
      <file value="Logs\other.log" />
      ...
      <layout type="log4net.Layout.PatternLayout">
        <conversionPattern value="%-5p [%d] [%M] %m%n" />
      </layout>
    </appender>
    <root>
      <level value="ALL" />
    </root>
    <logger name="SomeLogger">
      <level value="ALL" />
      <appender-ref ref="SomeAppender" />
    </logger>
    <logger name="OtherLogger">
      <level value="ALL" />
      <appender-ref ref="OtherAppender" />
    </logger>
  </log4net>
  <appSettings>
    <add key="log4net.Internal.Quiet" value="true" />
  </appSettings>
</configuration>

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

В приложении

Класс нашего консольного приложения может выглядеть так:

class Program
{
  private static readonly ILog someLog = LogManager.GetLogger("SomeLogger");
  private static readonly ILog otherLog = LogManager.GetLogger("OtherLogger");

  static void Main(string[] args)
  {
    XmlConfigurator.Configure();

    someLog.Debug("Some message");
    otherLog.Debug("Other message");
  }
}

Немного по-индийски, но, в общем, работает. Когда программа отработает, одно сообщение запишется в один файл, другое - в другой.

вторник, 18 ноября 2008 г.

Доступ к функциям телефона из web-страницы

Например, добавили мы на наш сайт некий платный сервис. Для доступа к нему нужно послать SMS с текстом таким-то на номер такой-то. Однако, на наш сайт ходят люди с телефонов, и им, для того, чтобы этим воспользоваться, надо закрыть браузер, написать SMS (еще вспомнить, что там вообще писать надо), а потом открыть браузер обратно. Получается довольно-таки неудобно.

Однако, есть выход. Оказывается, существует такая полезная вещь, как Trigger phone call. Суть в том, что можно вставлять прямо в HTML-код гиперссылки со специальным URI, и по нажатию на эти ссылки будут отправляться SMS, MMS, совершаться звонки и т.д.

Примеры

Отправка SMS

Ссылка, открывающая редактор SMS с текстом "Hello" и с указанным адресатом:

<a href="sms:+79021234567?body=Hello">Click to send message</a>

Ссылка, открывающая редактор SMS с текстом "Hello" и с указанным списком номеров:

<a href="sms:+79021234567,+79081234567?body=Hello">Click to send message</a>

Пишут, что в URI вместо sms: можно использовать smsto:, SMS: (для телефонов Siemens) и даже com.nokia.sms: (для телефонов Nokia).

Совершение вызова

Ссылка, совершающая вызов:

<a href="tel:+79021234567">Call</a>

Кстати, из Opera Mini, и без ссылок можно звонить на все числа, которые есть на сайте. Для дефаултного браузера же подобная конструкция очень может пригодиться.

Отправка MMS

Ссылка, отправляющая MMS:

<a href="mmsto:+3581234567?subject=Hi?body=Nice%20to%20see?">Send message</a>

Большой минус

Это все, конечно, очень хорошо и полезно. Но проблема в том, что не на всех телефонах работает. У меня были в наличии Nokia 2600 classic (самый обычный телефон) и Nokia же N73 (смартфон). Получилась такая картина:

Nokia 2600 classic (Series 40):

Default browserOpera Mini
SMSРугается на неверный адресМолча не работает
MMSРугается на неверный адресМолча не работает
telРаботаетРаботает

Nokia N73 (Series 60):

Default browserOpera Mini
SMSРаботает, если URI cодержит sms, но не работает с smsto (не заполняется тело сообщения) Работает smsto, но не работает sms (не заполняется тело сообщения)
MMSВроде работаетТоже вроде работает
telРаботаетРаботает

Эмулятор Android

Про него я сначала и забыла, но решила восполнить этот пробел.

Default browser
SMSИ sms, и smsto открывают редактор SMS, но адресат и тело сообщения не заполнены.
MMSОткрывается редактор, но, опять же, ничего не заполнено.
telРаботает

Зато я узнала, что эмулятор можно поворачивать на 90 градусов с помощью сочетания Ctrl+F11

В заключение

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

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

Ссылки

четверг, 13 ноября 2008 г.

Программа для отображения состояния коннекта

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

Так что приходилось располагать окошки так, чтобы видеть консоль с ping -t google.ru. А потом мне пришла в голову светлая идея написать программу для отображения состояния пинга в трее. В качестве языка реализации был выбран C#, дабы не заморачиваться.

Сначала создаем проект вида Windows Application. На главную форму можно не кидать никакие элементы управления, все равно не понадобятся. Зато в ресурсы надо добавить два каких-нибудь значка, один из которых будет называться Ping и обозначать коннект, а второй - Noping и отсутствие коннекта. Вот код формы:

namespace Pinger
{
  public partial class PingForm : Form
  {
    /// <summary>Иконка, обозначающая коннект</summary>
    private static NotifyIcon pingIcon;
    /// <summary>Иконка, обозначающая отсутствие коннекта</summary>
    private static NotifyIcon nopingIcon;

    /// <summary>true, если есть коннект, false иначе</summary>
    private static bool pingState;
    public static bool PingState
    {
      get { return pingState; }
      set
      {
        if (pingState != value) // если значение pingState изменилось
        {
          pingState = value;
          
          // переключаем иконки в трее
          NotifyIcon currentIcon = pingState ? pingIcon : nopingIcon;
          pingIcon.Visible = pingState;
          nopingIcon.Visible = !pingState;
          
          // показываем подсказку
          currentIcon.ShowBalloonTip(2000);
        }
      }
    }

    /// <summary>
    /// Функция, отправляющая пинг
    /// </summary>
    private void MakePing()
    {
      Ping pingSender = new Ping();

      // Добавляем обработчик для события окончания пинга
      pingSender.PingCompleted += new PingCompletedEventHandler(PingCompletedCallback);
      
      // инициализируем буфер с данными
      string data = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
      byte[] buffer = Encoding.ASCII.GetBytes(data);

      // куда посылаем пинг
      string who = "google.ru";

      // сколько ждем
      int timeout = 3000;

      AutoResetEvent waiter = new AutoResetEvent(false);
      PingOptions options = new PingOptions(64, true);

      while (true)
      {
        try
        {
          // собственно посыл пинга
          pingSender.SendAsync(who, timeout, buffer, options, waiter);
        }
        catch (Exception e)
        {
        }

        Thread.Sleep(timeout);
      }
    }

    /// <summary>
    /// Конструктор формы
    /// </summary>
    public PingForm()
    {
      InitializeComponent();

      System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(PingForm));

      // читаем из ресурсов иконки
      pingIcon = new NotifyIcon();
      pingIcon.Icon = ((System.Drawing.Icon)(resources.GetObject("Ping")));
      pingIcon.BalloonTipText = "Ping is OK";
      pingIcon.BalloonTipTitle = "Pinger";
      pingIcon.BalloonTipIcon = ToolTipIcon.Info;

      nopingIcon = new NotifyIcon();
      nopingIcon.Icon = ((System.Drawing.Icon)(resources.GetObject("Noping")));
      nopingIcon.BalloonTipText = "No Ping";
      nopingIcon.BalloonTipTitle = "Pinger";
      nopingIcon.BalloonTipIcon = ToolTipIcon.Warning;

      // значение по умолчанию
      PingState = true;

      // запускаем функцию MakePing в отдельном потоке
      Thread t = new Thread(new ThreadStart(MakePing));
      t.Start();
    }

    /// <summary>
    /// Обработка результата пинга
    /// </summary>
    public static void PingCompletedCallback(object sender, PingCompletedEventArgs e)
    {
      // пинг считается правильным, если не возникло ошибок и пришел ответ Success
      PingState = e.Error == null && e.Reply.Status == IPStatus.Success;
    }
  }
}

Вот, что получилось:

Появился коннект:

Есть коннект

Пропал коннект:

Нет коннекта

Я лично сейчас смотрю и очень радуюсь :) Правда, код для закрытия я поленилась писать, и закрыть программу можно только, отстрелив процесс в Task Manager.

Ссылки

среда, 12 ноября 2008 г.

Mobile Processing

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

Другое дело Mobile Processing. Тут вообще необязательно что-либо знать про мидлеты. Есть собственный простой язык, похожий на Java, с помощью которого можно описать логику приложения. Есть IDE, которая выглядит таким образом:

Единственное, что надо настроить в IDE - указать путь к WTK.

После этого мы пишем код, в котором просто определяем функции типа setup(), draw() и т.п., сохраняем все это дело в файл %progname%.PDE потом нажимаем кнопку "Пыщь" - и среда генерит нам jar.

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

int x, y;
int r = 5;

void setup()
{
 x = 100;
 y = 100;
}

void draw()
{
 background(255, 204, 0);
 ellipse(x, y, 2 * r, 2 * r);switch (keyCode){
  case UP: y--; break;
  case DOWN: y++; break;
  case LEFT: x--; break;
  case RIGHT: x++; break;
  default: break;}
}

void keyReleased()
{
 keyCode = 0;
}

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

Полученный jar-файл весил 46 Кб, но с помощью имеющегося оптимизатора-обфускатора ProGuard его удалось ужать до 7 Кб. Программа запросто запустилась на моей Nokia 2600c, а также на Nokia N73.

В общем, данная тулза привела меня в восторг своей простотой (особенно после монстров типа NetBeans или IDEA, которую все хвалят, но в которой мне так и не удалось создать Mobility-проект). Позиционируется она как средство быстрого создания прототипов приложений, но, честно говоря, возможности у нее не такие уж и слабые. Есть библиотеки для работы с Bluetooth, с камерой и другими возможностями телефона. И самое интересное - оно работает. Единственное, что меня удивляет, так это практически отсутствие упоминаний о данном продукте в рунете. Вроде, и проект не совсем мертвый (последний релиз в июле 2008). Интересно, почему бы это?

Но я в ней еще поковыряюсь, дабы узнать, что там плохо или хорошо. Пока что все обнаруженные минусы относятся к редактору: отсутствие отладчика и IntelliSense, неидеальный keymapping (я люблю, чтобы работали Ctrl+Ins и Shift+Ins, а там только Ctrl+C и Ctrl+V. Хотя, возможно, это настраивается в конфиге, который там далеко спрятан).

Ссылки по теме

четверг, 16 октября 2008 г.

Знакомство с Android. Часть 1: Простое приложение для Android

Недавно заинтересовала меня платформа Android. Как-то много говорят о нем в последнее время, да и вообще хотелось узнать, такая же ли там ужасная Java, как в мидлетах. Так что потратила я некоторое время на копание в нем, написала простое приложение, и сейчас вот буду делиться опытом.

UPDATE

Серия статей по игре Life обновлена 03.11.2010. Исправлены ошибки и проведена адаптация под Android версии 2.2. Всем спасибо за замечания и дополнения!

Постановка задачи

Первым нашим приложением для Android будет реализация всем известной игры Life. Местом дейтвия будет прямоугольное клеточное поле, размеры которого запрашиваются у пользователя. Также у пользователя запрашивается начальное количество клеток. Первое поколение расставляется по карте случайным образом. Последующие поколения получаются по следующим правилам:
  • Если у живой клетки меньше двух или больше трёх соседей, то она погибает.
  • Если у пустой клетки ровно три соседки, она оживает.
Все входные параметры должны проверяться на правильность: столбцов должно быть не меньше 5 и не больше 25, строк должно быть не меньше 5 и не больше 35, начальное количество клеток должно быть не больше, чем ячеек на поле. Для реализации поля будет использован класс GridView.
Для разработки была использована среда Eclipse и Android plugin для неё.
Статья будет из четырех частей:
Итак, начнём.

В этой части

Мы создадим проект, рассмотрим его структуру и напишем простое приложение, состоящее из одной формы. На форме будет интерфейс для ввода данных и кнопка Run.

Создание и обзор проекта

На установке Android SDK и плагина для Eclipse останавливаться не будем, т.к. это достаточно подробно описано в официальном мануале. Создаем в Eclipse новый Android Project:
Создание нового проекта под Android
После нажатия на кнопку Finish создастся новый проект с такой структурой файлов:
Структура проекта
Рассмотрим эту структуру внимательнее.

/res/drawable-dpi

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

/res/layout

В эту папку помещаются файлы, в которых в формате XML описывается внешний вид форм, расположение контролов и т.д. (как dfm-ки в Дельфи). Плагин даже создал разметку для нашей единственной формы и назвал её main.xml. Позже мы рассмотрим ее подробнее.

/res/values

В этой папке хранятся общие константы для всего приложения, как то: текст, используемый элементами управления, цвета, стили и т.д.. Например, если мы хотим вывести "Hello World" в TextView, можно это сделать явно в разметке, как мы всю жизнь делали в тех же dfm-ках или aspx; либо создать в strings.xml константу hello со значением "Hello World", после чего пойти обратно в разметку и в атрибутах этого TextView прописать android:text="@string/hello".

/gen/R.java

Это такой специальный сгенерированный класс, посредством которого осуществляется доступ к ресурсам приложения (т.е. ко всему тому, что есть в папке res). Например, R.string.hello возвращает константу с именем hello из strings.xml.

StartActivity.java

Это нам плагин сгенерировал класс для главной (и пока что единственной) формы приложения. Там пока содержится единственный обработчик onCreate, и написано там только setContentView(R.layout.main);. С помощью этой строчки к данной форме привязывается разметка, описанная в файле /res/layout/main.xml

AndroidManifest.xml

В этом файле перечисляются общие свойства проекта (версия, package и прочее), а также все формы (Activities), входящие в проект.

Разметка формы (Layout)

Элементы управления в Android называются Views и наследуются от класса View или ViewGroup. Класс ViewGroup также унаследован от View, но его отличие в том, что в него могут быть вложены другие View или ViewGroup.
Иерархия View
Плагин создал простейшую разметку для нашей единственной формы (main.xml):

main.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"
    >
<TextView  
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:text="@string/hello"
    />
</LinearLayout>
Вначале задаётся Layout, т.е. правило, согласно которому элементы управления следуют друг за другом. LinearLayout значит, что они идут друг за другом сверху вниз (android:orientation="vertical"). Бывают и другие Layout-ы: TableLayout, с помощью которого можно выстроить контролы в таблицу; FrameLayout, который ставит контролы один на другой; и т.д.
Мы воспользуемся TableLayout
Сделаем вот такую разметку:

main.xml

<?xml version="1.0" encoding="utf-8"?>
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:stretchColumns="1"
    android:padding="10dip"
    >
    <TableRow android:paddingBottom="5dip">
        <TextView
            android:text="@string/columns_title"
            android:paddingRight="10dip"
            android:gravity="right"
            android:textStyle="bold"
        />
        <EditText
            android:id="@+id/columns_count"
            android:text="25"
            android:inputType="number"
        />
    </TableRow>
    <TableRow android:paddingBottom="5dip">
        <TextView
            android:text="@string/rows_title"
            android:paddingRight="10dip"
            android:gravity="right"
            android:textStyle="bold"
        />
        <EditText
            android:id="@+id/rows_count"
            android:text="35"
            android:inputType="number"
        />
    </TableRow>
    <TableRow android:paddingBottom="5dip">
        <TextView
            android:text="@string/cells_title"
            android:paddingRight="10dip"
            android:gravity="right"
            android:textStyle="bold"
        />
        <EditText
            android:id="@+id/cells_count"
            android:text="100"
            android:inputType="number"
        />
    </TableRow>
    <TableRow>
        <Button
            android:id="@+id/run"
            android:text="@string/run_title"
            android:textStyle="bold"
            android:layout_span="2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
        />
    </TableRow>
</TableLayout>
В файл res/values/strings.xml при этом нужно добавить следующие строки:

strings.xml

<string name="run_title">Run!</string>
<string name="columns_title">Columns:</string>
<string name="rows_title">Rows:</string>
<string name="cells_title">Cells:</string>
Форма при этом будет выглядеть так:
StartActivity
Рассмотрим некоторые атрибуты, использованные в разметке

android:id

Идентификатор элемента. Если он указан, то в дальнейшем его можно найти на форме с помощью метода findViewById(id). Для контролов, которых мы не планируем в дальнейшем трогать (например, для заголовков), можно это свойство и вовсе не указывать. Идентификаторы можно складывать в файл ids.xml, но вместо этого обычно применяется синтаксис @+id/View1. Это означает, что идентификатор View1 добавляется в константы прямо на ходу. В R.java соответствующие поля также добавляются автоматически.

android:layout_width и android:layout_heigth

Свойства layout_width и layout_heigth обозначают, какую часть родительского контрола будет занимать данный элемент управления: всю (fill_parent или match_parent, начиная с 2.2) или ровно столько, сколько требуется (wrap_content). Можно также задавать численные значения.

android:inputType

Это атрибут EditText. С его помощью можно устанавливать формат вводимого значения. Есть ряд предопределенных форматов (date, phone, etc). Значение number значит, что в это поле можно вводить только целые положительные числа.

android:gravity, android:layout_gravity

Устанавливает выравнивание в данном элементе управления. Отличие в том, что gravity задает выравнивание дочерних контролов, а layout_gravity задает выравнивание самого контрола.

Заключение

Итак, мы создали проект для Android, рассмотрели его структуру, составили разметку для нашей единственной формы.
Исходники примера

Знакомство с Android. Часть 2: Переходы между формами

Итак, продолжим. В этой части мы добавим в проект ещё одну форму и будем открывать её по нажатию кнопки Run. Также сделаем так, чтобы параметры, которые были введены в первой форме, передавались во вторую (они там ещё пригодятся). Однако, ничего страшного мы пока с ними делать не будем, а просто напишем "Введены такие-то числа"

Знакомство с Android. Часть 3: Использование диалогов

В этой части мы сделаем проверку вводимых параметров по следующим правилам:
  • Число столбцов должно быть не меньше 5 и не больше 25.
  • Число строк должно быть не меньше 5 и не больше 35.
  • Начальное количество клеток должно быть не больше, чем ячеек на поле.
Если какое-то из этих условий не выполняется, будем выводить соответствующее предупреждение
Кроме того, мы добавим кнопку Close, при нажатии на которую приложение будет закрываться, спрашивая вначале согласие пользователя.

Знакомство с Android. Часть 4: Использование GridView

Итак, в нашем приложении осталось всего ничего: реализовать собственно алгоритм игры Life и отобразить его в GridView. Этим-то мы сейчас и займёмся.