По мотивам поста Шарик, отвечающий на вопросы
В данной статье мы напишем локализованный Magic 8-Ball для Android, которому можно будет задать вопрос, потрясти и получить ответ. Небольшая вибрация оповестит нас о том, что тряски достаточно.
Статья рассчитана на тех, кто уже написал хелловорлд под Android и собирается идти дальше в этом направлении. Полная версия исходного кода лежит на google code. Там же можно попробовать файл magic-8-ball 1.1.apk во вкладке download.
Для успешной работы нам будут нужны установленные jdk, android sdk, eclipse и ADT плагин. Как это сделать, доступно написано здесь.
Локализация
География Android устройств обширна, поэтому вопрос локализации занимает ключевое место при разработке приложений под эту платформу. К счастью, нам не придется изобретать велосипед, Google позаботился о программистах в этом вопросе.
Итак, концепция локализации приложений такова, мы создаем несколько наборов ресурсов, первый — по умолчанию res/values/strings.xml, остальные для нужной нам локали res/values-<qualifiers>/strings.xml, например, values-en для английского или values-ja для японского. При запуске Android выбирает какие ресурсы загрузить, основываясь на локали самого устройства. К слову, под ресурсами в Android подразумеваются не только текстовые строки, а также layout’ы, звуковые файлы, графика и другие статические данные.
Для нашего приложения мы создадим папки res/values/, res/values-en и res/values-ru, в них будут лежать файлы strings.xml. Эти файлы содержат название приложения, заголовки и ответы магического шара, которые хранятся в массиве responses. Выглядит это так:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Magic 8-Ball</string>
<string name="shake_me_caption">Shake Me</string>
<string name="press_menu_shake_caption">Press Menu\n- Shake</string>
<string name="menu_shake_caption">Shake</string>
<string name="menu_preferences_caption">Preferences</string>
<string name="preferences_section_title">Magic 8-ball Preferences</string>
<string name="shake_count_id">shakeCount</string>
<string name="shake_count_title">Shake count</string>
...
<string-array name="responses">
<item>As I see\nit, yes</item>
<item>It is\ncertain</item>
<item>It is\ndecidedly\nso</item>
<item>Most\nlikely</item>
<item>Outlook\ngood</item>
<item>Signs point\nto yes</item>
<item>Without\na doubt</item>
<item>Yes</item>
<item>Yes -\ndefinitely</item>
...
<item>Very\ndoubtful</item>
</string-array>
</resources>
* This source code was highlighted with Source Code Highlighter.Еще нюанс, android developer's guide предупреждает нас о необходимости иметь точную копию какой-нибудь локализации в качестве ресурса по умолчанию, чаще всего — английскую. И приводит пример, что если не будет хватать строки в файле res/values/strings.xml, которая есть в res/values-en/strings.xml и используется в приложении, то возможно все скомпилируется без проблем, но в локали отличной от английской пользователь увидит сообщение об ошибке и кнопку Force Close.
Layout, анимация и вибрация
Тут просто, картинкой фона установлено инвертированное изображение космоса. На нем лежит ImageView с шаром, а наше сообщение будет показываться с помощью TextView. Код main.xml:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="@drawable/bg">
<ImageView android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:src="@drawable/eight_ball"
android:layout_gravity="center_vertical"
android:layout_margin="10px" />
<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="center">
<TextView android:id="@+id/MessageTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@drawable/triangle"
android:text="@string/shake_me_caption"
android:focusable="false"
android:gravity="center_vertical|center" android:layout_marginTop="14dip">
</TextView>
</LinearLayout>
</FrameLayout>
* This source code was highlighted with Source Code Highlighter.Для появляющегося ответа будем использовать AlphaAnimation. Этот класс как нельзя кстати подходит для нашей задачи, позволяя объектам появляться и исчезать, используя alpha уровень объекта.
private void showMessage(String message) {
TextView triangle = (TextView) findViewById(R.id.MessageTextView);
triangle.setVisibility(TextView.INVISIBLE);
triangle.setText(message);
AlphaAnimation animation = new AlphaAnimation(0, 1);
animation.setStartOffset(Defaults.START_OFFSET);
triangle.setVisibility(TextView.VISIBLE);
animation.setDuration(Defaults.FADE_DURATION);
triangle.startAnimation(animation);
vibrator.vibrate(Integer.parseInt(preferences.getString(
getString(R.string.vibrate_time_id), Defaults.VIBRATE_TIME)));
}
* This source code was highlighted with Source Code Highlighter.Перед тем как дать ответ Magic 8-Ball завибрирует на время VIBRATE_TIME, взятое из настроек (о них ниже). По дефолту это значение 250мс. Вибрацию маленькой длительностью (50мс) можно использовать в приложениях как ответную реакцию на действия пользователя. Для корректной работы с классом Vibrator нужно не забыть объявить uses-permission в файле AndroidManifest.xml:
<uses-permission android:name="android.permission.VIBRATE" />Меню
Для начала, реализуем возможность получить ответ с помощью меню. Создадим файл menu.xml в папке res/menu/ который содержит кнопки Shake и Preference.
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/shake"
android:title="@string/menu_shake_caption"
android:icon="@android:drawable/ic_menu_always_landscape_portrait" />
<item android:id="@+id/preferences"
android:title="@string/menu_preferences_caption"
android:icon="@android:drawable/ic_menu_preferences" />
</menu>
* This source code was highlighted with Source Code Highlighter.В классе Magic8Ball напишем два таких метода:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater menuInflater = getMenuInflater();
menuInflater.inflate(R.menu.menu, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.shake:
showMessage(getAnswer());
return true;
case R.id.preferences:
startActivity(new Intent(this, Preferences.class));
return true;
}
return false;
}
* This source code was highlighted with Source Code Highlighter.Класс MenuInflater используется для создания объектов Меню из xml файла. В метод onOptionsItemSelected мы попадаем в момент выбора элемента меню, о чем говорит нам само название метода. При нажатии на кнопку Shake, мы получим ответ. О кнопке Preferences расскажет глава Настройки.
Работа с сенсором
Для взаимодействий с сенсором нам нужно имплементировать интерфейс SensorEventListener. У него объявлены два метода:
- onAccuracyChanged, вызывается когда изменилась точность датчика, он нам не нужен
- onSensorChanged, вызывается при изменениях значений датчика, этот нужен нам даже очень.
Еще один немаловажный момент — чтобы работать с сенсором нам нужно его найти и зарегистрироваться у SensorManager’a.
sensorManager = (SensorManager)getSystemService(Context.SENSOR_SERVICE);
sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
Заметим, что мы получаем сенсор с атрибутом Sensor.TYPE_ACCELEROMETER, это означает, что данные приходящие в метод onSensorChanged будут в единицах ускорения (м/с^2). Значениям по трем осям X, Y и Z будут соответствовать значения event.values[0], event.values[1], event.values[2] класса SensorEvent.
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER)
if (isShakeEnough(event.values[0], event.values[1], event.values[2]))
showMessage(getAnswer());
}
private boolean isShakeEnough(float x, float y, float z) {
double force = 0.0d;
force += Math.pow((x - lastX) / SensorManager.GRAVITY_EARTH, 2.0);
force += Math.pow((y - lastY) / SensorManager.GRAVITY_EARTH, 2.0);
force += Math.pow((z - lastZ) / SensorManager.GRAVITY_EARTH, 2.0);
force = Math.sqrt(force);
lastX = x;
lastY = y;
lastZ = z;
if (force > Float.parseFloat(preferences.getString(
getString(R.string.threshold_id), Defaults.THRESHOLD))) {
shakeCount++;
if (shakeCount > Integer.parseInt(preferences.getString(
getString(R.string.shake_count_id), Defaults.SHAKE_COUNT))) {
shakeCount = 0;
lastX = 0;
lastY = 0;
lastZ = 0;
return true;
}
}
return false;
}
* This source code was highlighted with Source Code Highlighter.Временем жизни нашей Activitiy управляет Android, и нам не нужно чтобы наше приложение вибрировало от случайных трясок в кармане, поэтому мы перестаем работать с сенсором, как только приложение становится неактивно. Также если сенсор не найден, то мы не предлагаем трясти девайс.
@Override
public void onResume() {
super.onResume();
registerSensorListener();
if (isSensorRegistered())
showMessage(getString(R.string.shake_me_caption));
else
showMessage(getString(R.string.menu_shake_caption));
}
@Override
public void onPause() {
unregisterSensorListener();
super.onPause();
}
* This source code was highlighted with Source Code Highlighter.Настройки
Наконец, мы дошли до настроек. Тестирование на различных телефонах показало необходимость задавать порог силы встряхивания — threshold. Между HTC Wildfire, Motorola Milestone и, например, Highscreen Zeus, это значение отличалось раза в три. Это ставит перед нами задачи:
- Нужно окно настроек, где пользователь может вручную задать значение порога и другие параметры
- Эти значения настроек надо где-то хранить и откуда-то читать.
Тут в помощь нам приходят два уже готовых класса PreferenceActivity для отображения настроек и SharedPreferences для их хранения. Теперь более подробно.
PreferenceActivity — класс, который визуально отображает нам иерархию объектов preference. Он загружает контент из xml файла методом addPreferencesFromResource. Он автоматически взаимодействует с объектом класса SharedPreferences и сохраняет настройки по указанному ключу. Создадим xml файл наших настроек и надуем (inflate) наш PreferenceActivity.
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory android:title="@string/preferences_section_title">
<EditTextPreference
android:key="@string/shake_count_id"
android:defaultValue="4"
android:title="@string/shake_count_title"
android:summary="@string/shake_count_summary"
android:dialogTitle="@string/shake_count_dialogTitle"
android:inputType="number"
android:numeric="integer"
android:maxLength="1"
/>
<EditTextPreference
android:key="@string/threshold_id"
android:defaultValue="2.75f"
android:title="@string/threshold_title"
android:summary="@string/threshold_summary"
android:dialogTitle="@string/threshold_dialogTitle"
android:inputType="numberDecimal"
android:numeric="decimal"
android:maxLength="4"
/>
<EditTextPreference
android:key="@string/vibrate_time_id"
android:defaultValue="250"
android:title="@string/vibrate_time_title"
android:summary="@string/vibrate_time_summary"
android:dialogTitle="@string/vibrate_time_dialogTitle"
android:inputType="number"
android:numeric="integer"
android:maxLength="3"
/>
</PreferenceCategory>
</PreferenceScreen>
* This source code was highlighted with Source Code Highlighter.Корневым элементом preference xml файла всегда должен быть PreferenceScreen. Вложенные элементы могут же являться различными подклассами Preference (в том числе также PreferenceScreen). В нашем файле все вложенные элементы — EditTextPreference. Этот класс удобен для показа пользователю диалогового окна, в TextEdit которого он может ввести значение. Теперь подробнее про некоторые аттрибуты EditTextPreference:
- android:key="@string/shake_count_id" — ключ, по которому мы обращаемся в SharedPreferences для чтения или записи значения
- android:title="@string/shake_count_title" — название (надпись крупными буквами в PreferenceActivity)
- android:summary="@string/shake_count_summary" — краткое описание опции
- android:dialogTitle="@string/shake_count_dialogTitle" — заголовок выплывающего диалога
- android:numeric=«integer» — указываем, что метод ввода должен быть числовой
Создаем новую activity с настройками (см. Preferences.java) и не забываем указать ее в AndroidManifest’е:
<activity android:name=".activities.Preferences"
android:label="@string/app_name">
</activity>

Теперь, пользователь может менять значения некоторых настроек, нам осталось начать их корректно считывать. Чтобы получить экземпляр SharedPreferences мы обратимся к методу PreferenceManager.getDefaultSharedPreferences(this). У нас есть ключи по которым мы храним данные, как нам их прочитать?.. Например, время вибрации, так: vibrator.vibrate(Integer.parseInt(preferences.getString(getString(R.string.vibrate_time_id), Defaults.VIBRATE_TIME)));
К сожалению, preferences.getInt(...) упорно выдает ClassCastException. Похоже, это связано с тем, что preferences хранятся как строки. Остальные значения считываем по такому же принципу. Изменив значение threshold’а мы можем убедиться в том, что нужна различная сила, чтобы растрясти шарик на ответ.
P.S.
Что можно еще добавить в приложение? Его можно сделать более юзерфрендли. Например, добавить трек бар вместо окна со значениями для изменения порога чувствительности и при первом запуске попросить потрясти, чтобы автоматически откалибровать шар. Еще можно создать в приложении редактор ответов, чтобы пользователь мог добавлять их в соответствии со своей локалью.
