пятница, 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

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

воскресенье, 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. Этим-то мы сейчас и займёмся.

четверг, 7 августа 2008 г.

URL rewriting в ASP.NET

URL rewriting - это такая штука, которая позволяет из некрасивых урлов с параметрами, вроде
www.somesite.ru/?cat=blogs&author=darja&year=2008&month=8
позволяет делать красивые, т.н. Человеку понятные урлы (ЧПУ), вроде
www.somesite.ru/blogs/darja/2008/8

Возникла у меня такая задача. Есть табличка (вернее, ListView), данные в которой располагаются на нескольких страницах. Пейджер у таблицы устроен так, что к N-ной странице можно обратиться http://localhost/Default.aspx?page=N И вот захотелось, чтобы к n-ной странице таблички можно было бы обратиться как http://localhost/N. Типа так ссылки приятнее выглядят и роботам хорошо. Итак, было найдено следующее решение.

URL rewriting

  1. Скачиваем библиотеку ThunderMain.URLRewriter.dll, кидаем в свой проект.
  2. В Global.asax добавляем:
    protected void Application_BeginRequest(Object sender, EventArgs e)
    {
      ThunderMain.URLRewriter.Rewriter.Process();
    }
    
  3. Вносим изменения в web.config. В configSections пишем:
    <sectionGroup name="system.web">
      <section name="urlrewrites" type="ThunderMain.URLRewriter.Rewriter, ThunderMain.URLRewriter,
     Version=1.0.783.30976, Culture=neutral, PublicKeyToken=7a95f6f4820c8dc3"/></sectionGroup>
    
    Далее, в system.web пишем:
    <urlrewrites><rule>
      <url>/(\d*)</url>
      <rewrite>Default.aspx?page=$1</rewrite></rule>
    </urlrewrites>
    
    Элементов rule может быть несколько. В узле url указывается регулярное выражение, которому должен удовлетворять url, который мы хотим переписывать, в rewrite - на что мы будем его заменять. Можно использовать стандартные для регулярных выражений группы ($1, $2 и т.д., как в перле). В нашем случае, по адресу http://localhost/N, где N - число, загрузится http://localhost/Default.aspx?page=N. Всё очень просто.
  4. Также потребуется некоторая донастройка IIS. А именно, нужно зайти в настройки сайта -> Home directory -> Configuration -> Mappings и добавить там обработчик для файлов .* такой же, как для aspx (ну там C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll). И обязательно снять галочку "Verify that file exists".
UPDATE: Кстати, у меня тут практика показала, что прописывать в IIS мэппинг для .* не есть хорошо, потому что при этом ломаются стили и прочие статические файлы, потому что IIS и их начинает спрашивать с asp.net-a. Я в итоге, дабы не париться, сделала адреса вида http://localhost/N.htm и добавила в IIS мэппинг только для .htm

Пейджер у ListView

Итак, к страницам уже можно обращаться по адресу http://localhost/N. Но в ссылки пейджера всё ещё выглядят по-прежнему. Делаем следующее.
  1. Пейджер выносим в отдельный div и ставим у него style="display:none".
  2. Под листвью добавляем панельку:
    <asp:Panel ID="Pager" runat="server" onprerender="Pager_PreRender">
    </asp:Panel>
    
  3. В коде пишем:
    protected void Pager_PreRender(object sender, EventArgs e)
    {
      Pager.Controls.Clear();
    
      DataPager pager = (DataPager)RatingListView.FindControl("RatingPager");
    
      int count = pager.TotalRowCount;
      int pageSize = pager.PageSize;
      int pagesCount = count / pageSize + (count % pageSize == 0 ? 0 : 1);
      int pageSelected = pager.StartRowIndex / pageSize + 1;
    
      for (int i = 1; i <= pagesCount; ++i)
      {
        if (pageSelected != i)
        {
          HyperLink link = new HyperLink();
          link.NavigateUrl = link.ResolveUrl(i.ToString());
          link.Text = i.ToString();
          Pager.Controls.Add(link);
        }
        else
        {
          Label label = new Label();
          label.Text = i.ToString();
          Pager.Controls.Add(label);
        }
    
        Literal space = new Literal();
        space.Text = " ";
        Pager.Controls.Add(space);
      }
    }
    
    Здесь RatingListView - наш листвью, RatingPager - его "родной" пейджер.

Литература