По мотивам поста Шарик, отвечающий на вопросы
В данной статье мы напишем локализованный 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.
Что можно еще добавить в приложение? Его можно сделать более юзерфрендли. Например, добавить трек бар вместо окна со значениями для изменения порога чувствительности и при первом запуске попросить потрясти, чтобы автоматически откалибровать шар. Еще можно создать в приложении редактор ответов, чтобы пользователь мог добавлять их в соответствии со своей локалью.