Статья рассчитана на тех, кто уже написал хелловорлд под 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. Выглядит это так:
Еще нюанс, android developer's guide предупреждает нас о необходимости иметь точную копию какой-нибудь локализации в качестве ресурса по умолчанию, чаще всего — английскую. И приводит пример, что если не будет хватать строки в файлеres/values/strings.xml, которая есть в res/values-en/strings.xml и используется в приложении, то возможно все скомпилируется без проблем, но в локали отличной от английской пользователь увидит сообщение об ошибке и кнопку Force Close.
Layout, анимация и вибрация
Тут просто, картинкой фона установлено инвертированное изображение космоса. На нем лежит ImageView с шаром, а наше сообщение будет показываться с помощью TextView. Код main.xml:
Для появляющегося ответа будем использовать AlphaAnimation. Этот класс как нельзя кстати подходит для нашей задачи, позволяя объектам появляться и исчезать, используя alpha уровень объекта.
Перед тем как дать ответ Magic 8-Ball завибрирует на время VIBRATE_TIME, взятое из настроек (о них ниже). По дефолту это значение 250мс. Вибрацию маленькой длительностью (50мс) можно использовать в приложениях как ответную реакцию на действия пользователя. Для корректной работы с классом Vibrator нужно не забыть объявить uses-permission в файле AndroidManifest.xml: <uses-permissionandroid: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.
Класс MenuInflater используется для создания объектов Меню из xml файла. В метод onOptionsItemSelected мы попадаем в момент выбора элемента меню, о чем говорит нам само название метода. При нажатии на кнопку Shake, мы получим ответ. О кнопке Preferences расскажет глава Настройки.
Работа с сенсором
Для взаимодействий с сенсором нам нужно имплементировать интерфейс SensorEventListener. У него объявлены два метода:
onAccuracyChanged, вызывается когда изменилась точность датчика, он нам не нужен
onSensorChanged, вызывается при изменениях значений датчика, этот нужен нам даже очень.
Еще один немаловажный момент — чтобы работать с сенсором нам нужно его найти и зарегистрироваться у SensorManager’a.
Заметим, что мы получаем сенсор с атрибутом 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);
Временем жизни нашей 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(); }
Наконец, мы дошли до настроек. Тестирование на различных телефонах показало необходимость задавать порог силы встряхивания — threshold. Между HTC Wildfire, Motorola Milestone и, например, Highscreen Zeus, это значение отличалось раза в три. Это ставит перед нами задачи:
Нужно окно настроек, где пользователь может вручную задать значение порога и другие параметры
Эти значения настроек надо где-то хранить и откуда-то читать.
Тут в помощь нам приходят два уже готовых класса PreferenceActivity для отображения настроек и SharedPreferences для их хранения. Теперь более подробно.
PreferenceActivity — класс, который визуально отображает нам иерархию объектов preference. Он загружает контент из xml файла методом addPreferencesFromResource. Он автоматически взаимодействует с объектом класса SharedPreferences и сохраняет настройки по указанному ключу. Создадим xml файл наших настроек и надуем (inflate) наш PreferenceActivity.
Корневым элементом 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" — краткое описание опции
Теперь, пользователь может менять значения некоторых настроек, нам осталось начать их корректно считывать. Чтобы получить экземпляр 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. Что можно еще добавить в приложение? Его можно сделать более юзерфрендли. Например, добавить трек бар вместо окна со значениями для изменения порога чувствительности и при первом запуске попросить потрясти, чтобы автоматически откалибровать шар. Еще можно создать в приложении редактор ответов, чтобы пользователь мог добавлять их в соответствии со своей локалью.