Показаны сообщения с ярлыком Widget. Показать все сообщения
Показаны сообщения с ярлыком Widget. Показать все сообщения

вторник, 12 июля 2011 г.

Пишем виджет для Android

В сети есть достаточно много инструкций по написанию виджетов для 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:

  1. public class CourceWidget extends AppWidgetProvider {
  2.  
  3.   @Override
  4.   public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
  5.     context.startService(new Intent(context, UpdateService.class));
  6.   }
  7.  
  8.   public static class UpdateService extends Service {
  9.  
  10.     @Override
  11.     public void onStart(Intent intent, int startId) {
  12.       RemoteViews updateViews = buildUpdate(this);
  13.       AppWidgetManager.getInstance(this).updateAppWidget(new ComponentName(this, CourceWidget.class), updateViews);
  14.     }
  15.  
  16.     /**
  17.      * Build a widget update
  18.      */
  19.     public RemoteViews buildUpdate(Context context) {
  20.  
  21.       RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.cource_message);
  22.  
  23.       String buy = "";
  24.       String sell = "";
  25.  
  26.       // get gata from JSON
  27.       try {
  28.         JSONObject resp = new JSONObject(Http.Request("https://mtgox.com/code/data/ticker.php"));
  29.         buy = resp.getJSONObject("ticker").getString("buy");
  30.         sell = resp.getJSONObject("ticker").getString("sell");
  31.       } catch (Exception e) {
  32.         buy = "...";
  33.         sell = "...";
  34.         e.printStackTrace(System.err);
  35.         Toast.makeText(this, R.string.err_connect, Toast.LENGTH_SHORT).show();
  36.       }
  37.       views.setTextViewText(R.id.message_b, "$" + buy);
  38.       views.setTextViewText(R.id.message_s, "$" + sell);
  39.       return views;
  40.     }
  41.  
  42.     @Override
  43.     public IBinder onBind(Intent intent) {
  44.       return null;
  45.     }
  46.   }
  47. }
Наш класс расширяет 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: по многочисленным просьбам даю ссылку на исходники моего проекта, "по мотивам" которого была написана эта статья.