В сети есть достаточно много инструкций по написанию виджетов для Android. Большая часть из них "HelloWorld-ы", в остальных местах упущены какие-то важные мелочи... В общем давайте разберём создание виджета "по взрослому": с фоновыми процессами, обработкой кликов, локализацией. Пусть наш виджет получает курс валюты Bitcoin по отношению к доллару из API биржы Mt. Gox и отображает его текущее значение. В результате мы получим что-то вроде
этого.
Во-первых, чем виджет отчается от Activity?
Виджет работает в рамках "рабочего стола" нашего смартфона и от этого имеет как плюсы так и минусы. Плюсы: можно настроить обновление средствами системы. Интервал обновления при этом не может быть меньше получаса (180000 ms). Приложение всегда на виду: клиенту можно что-то сообщать не дожидаясь его действий. Минусы: ограниченный набор компонентов, доступных для использования в интерфейсе (из компоновщиков можно использовать только
"FrameLayout", "LinearLayout" и "RelativeLayout". Из View: "AnalogClock", "Button", "Chromometer", "ImageButton", "ImageView", "ProgressBar" и "TextView".). Также ограничено время, которое отводится на исполнение запросов. Ну, и, само собой, ограниченный размер "рабочей площади". Также достаточно сложно реализовать обработку событий в виджете: onclickListener на кнопку в виджете "повесить" не получится.
Как регистрировать виджет?
Так же как и всё остальное в нашем приложении, виджет описывается в AndroidManifest.xml. В тег application добавляем структуру:
<receiver android:name=".CourceWidget" android:label="@string/app_label">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data android:name="android.appwidget.provider" android:resource="@xml/widget_cource" />
</receiver>
Тут
.CourceWidget - класс, описывающий виджет,
widget_cource - xml файл (фактически: res/xml/widget_cource.xml), описывающий параметры виджета.
Как описать параметры виджета?
Вот, например, так:
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="146dip"
android:minHeight="72dip"
android:updatePeriodMillis="1800000"
android:initialLayout="@layout/cource_message"
/>
Тут
android:minWidth и
android:minHeight соответственно ширина и высота виджета. Рекомендуется приводить размеры по формуле: число пикселей = (число ячеек * 74) – 2. Одну ячейку на "рабочем столе" занимает одна иконка. Значение
android:updatePeriodMillis, как уже говорили, определяет период обновления виджета в миллисекундах. В атрибуте
android:initialLayout указываем ссылку на xml файл (res/layout/cource_message.xml) с описанием интерфейса виджета.
Как описать интерфейс виджета?
Вот, например, так:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:background="@drawable/widget_bg">
<LinearLayout
android:orientation="vertical"
android:gravity="center"
android:layout_width="wrap_content"
android:layout_height="fill_parent">
<TextView
android:layout_marginTop="10sp"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textColor="#000000"
android:gravity="center"
android:text="@string/buy" />
<TextView
android:id="@+id/message_b"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textColor="#ff0000"
android:textSize="16sp"
android:textStyle="bold"
android:gravity="center"
android:text="@string/loading" />
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textColor="#000000"
android:gravity="center"
android:text="@string/sell" />
<TextView
android:id="@+id/message_s"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textColor="#ff0000"
android:textSize="16sp"
android:textStyle="bold"
android:gravity="center"
android:text="@string/loading" />
</LinearLayout>
</LinearLayout>
Это обычный LinearLayout с четырьмя TextView, два из которых содержат строковые константы ("покупка" и "продажа") а другие два заполняются данными, полученными из API. Строковые константы для поддержки "мультиязычности" выносим в файл res/values/strings.xml (значения по умолчанию - английская локализация) и res/values-ru/strings.xml (русская локализация).
Как обеспечить локализацию виджета?
Как сказано выше: вынести все константы в xml-файл вида
<resources>
<string name="app_name">Exchange rate of Bitcoin</string>
<string name="app_label">Bitcoin:USD exchange rate</string>
<string name="buy">Buy:</string>
<string name="sell">Sell:</string>
<string name="loading">Loading</string>
<string name="err_connect">Connect error</string>
</resources>
... и обращаться к ним как:
@string/loading (вернёт "Loading"). Система сама выберет нужный файл с константами в зависимости от настроек языка в системе. В java-коде обращение к локализованным строковым константам будет выглядеть так:
R.string.loading.
Как описать логику, которая должна выполняться при обновлении виджета?
Вот тут мы, наконец-то, начинаем писать java-код. Для описания логики обновления данных мы должны реализовать класс, имя которого указано в самом первом xml-конфиге, приведённом в этой статье. Это
CourceWidget.java:
- public class CourceWidget extends AppWidgetProvider {
-
- @Override
- public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
- context.startService(new Intent(context, UpdateService.class));
- }
-
- public static class UpdateService extends Service {
-
- @Override
- public void onStart(Intent intent, int startId) {
- RemoteViews updateViews = buildUpdate(this);
- AppWidgetManager.getInstance(this).updateAppWidget(new ComponentName(this, CourceWidget.class), updateViews);
- }
-
- /**
- * Build a widget update
- */
- public RemoteViews buildUpdate(Context context) {
-
- RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.cource_message);
-
- String buy = "";
- String sell = "";
-
- // get gata from JSON
- try {
- JSONObject resp = new JSONObject(Http.Request("https://mtgox.com/code/data/ticker.php"));
- buy = resp.getJSONObject("ticker").getString("buy");
- sell = resp.getJSONObject("ticker").getString("sell");
- } catch (Exception e) {
- buy = "...";
- sell = "...";
- e.printStackTrace(System.err);
- Toast.makeText(this, R.string.err_connect, Toast.LENGTH_SHORT).show();
- }
- views.setTextViewText(R.id.message_b, "$" + buy);
- views.setTextViewText(R.id.message_s, "$" + sell);
- return views;
- }
-
- @Override
- public IBinder onBind(Intent intent) {
- return null;
- }
- }
- }
Наш класс расширяет
AppWidgetProvider, переопределяя его метод
onUpdate. В этом методе мы ничего не делаем, кроме одного: мы запускаем сервис
UpdateService, описанный тут же, как вложенный класс.
Что такое сервис и зачем он нам нужен?
В контексте виджета мы не можем обращаться к сетевым ресурсам: время обновления "рабочего стола" должно быть минимальным. Сервис - это фоновый процесс, который Android исполняет в отдельном потоке настолько долго, насколько это нам нужно. Чтобы зарегистрировать сервис в AndroidManifest.xml внутри тега application добавим:
<service android:name=".CourceWidget$UpdateService" />
Как сервис получит данные из API?
Очень просто. Отправив get-запрос, мы получаем строку, которая на самом деле представляет собой JSON-структуру. Парсинг JSON выполняем при помощи классов org.json.* которые доступны без подключения всяких сторонних библиотек. Передаём полученную из API строку в конструктор объекта JSONObject. Методами этого объекта мы получаем вложенные объекты и их поля, т.е. значения из ответа (строки 28-30). В случае любой ошибки присваиваем переменным значения по умолчанию.
Как сервис передаёт данные в виджет?
У виджета нельзя обратиться к отдельному элементу View, чтобы изменить его свойства. Можно только заменить всю иерархию компонентов целиком. Для этого создаём объект
RemoteViews (строка 21) и "вкладываем" в него полученные из API данные с помощью метода
setTextViewText. Потом иерархию с установленными свойствами выдаём в виджет с помощью метода
updateAppWidget (строка 13).
И напоследок: как обрабатывать нажатие на виджет?
Тут мы с сожалением вспомним удобные Listener-ы из Activity. В виджетах всё существенно сложнее. Добавим в наш класс CourceWidget константу:
public static String ACTION_WIDGET_RELOAD = "reload";
Затем в нашем сервисе создадим событие:
Intent active = new Intent(context, CourceWidget.class);
active.setAction(ACTION_WIDGET_RELOAD);
PendingIntent actionPendingIntent = PendingIntent.getBroadcast(context, 0, active, 0);
... и зарегистрируем его (привязав к какому-нибудь id элемента из нашей иерархии RemoteViews):
views.setOnClickPendingIntent(R.id.reload, actionPendingIntent);
В данном случае мы "навешиваем" событие на элемент с id "reload".
Чтобы получить и обработать событие в классе CourceWidget переопределяем метод:
@Override
public void onReceive(Context context, Intent intent) {
// Receive event
final String action = intent.getAction();
if (ACTION_WIDGET_RELOAD.equals(action)) {
context.startService(new Intent(context, UpdateService.class));
}
super.onReceive(context, intent);
}
Тут мы заново стартуем сервис прои нажатии на виджет, тем самым вызывая "внеочередное" обновление данных.
P.S: по многочисленным просьбам даю
ссылку на исходники моего проекта, "по мотивам" которого была написана эта статья.