пятница, 15 июля 2011 г.

Android: Управляем виртуальной клавиатурой программно

В современных Android-смартфонах виртуальная (экранная) клавиатура очень полезный инструмент - это мало кто поставит под сомнение. А для разработчика она приятна ещё и своей гибкостью. Тут я приведу пару приёмов по управлению этим "девайсом".

Изменить вид клавиатуры для данного EditText-а:
EditText ipt = new EditText(this);
ipt.setInputType(InputType.TYPE_CLASS_PHONE);  - установит клавиатуру для ввода номера телефона. Другие варианты:
TYPE_CLASS_DATETIME - дата и время
TYPE_CLASS_NUMBER - цифры
TYPE_CLASS_TEXT - буквы

Убрать клавиатуру с экрана:
Context context = getApplicationContext();
InputMethodManager imm = (InputMethodManager)context.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(edit.getWindowToken(), 0);
Тут edit - EditText для которого нужно скрыть клавиатуру.
Код так выглядит, если писать его в Activity. Если расположить его в другом классе, экземпляр Activity нужно передать туда как параметр и вызывать методы как  act.getApplicationContext(), где act - экземпляр Activity

вторник, 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: по многочисленным просьбам даю ссылку на исходники моего проекта, "по мотивам" которого была написана эта статья.

среда, 6 июля 2011 г.

Строим диаграмму в Android-приложении

Допустим, вы решили сделать виджет, который будет отображать текущий курс валют. Вы идёте на любой открытый источник, берёте данные и выводите клиенту. Ничего сложного. Но и ничего особенного - таких виджетов миллион. Вы идёте дальше: принимаете решение хранить полученные данные за предыдущие дни и отображать клиенту информацию в виде диаграммы. Вот тут-то мы и сталкиваемся с реализацией 2D-графики в Android.
Чтобы создать объект изображения, нужно переопределить View. Назовём наш объект ChartView:

  1. public class ChartView extends View {
  2.   private int width = 10;
  3.   ArrayList<ChartItem> charts;
  4.   int max = 0;
  5.   ShapeDrawable background;
  6.   
  7.   public ChartView(Context context, ArrayList<ChartItem> charts, int top, int bgColor) {
  8.     super(context);
  9.     Resources res = getResources();
  10.     int ws = res.getDisplayMetrics().widthPixels;
  11.     
  12.     ArrayList<ChartItem> gencharts = new ArrayList<ChartItem>();
  13.     for (ChartItem ci : charts) {
  14.       int[] points = scaleTo(ci.getPoints(), top);
  15.       ArrayList<ShapeDrawable> myDraws = new ArrayList<ShapeDrawable>();
  16.       for (int i=0; i<points.length; i++) {
  17.         int h = points[i];
  18.         ShapeDrawable mDrawable = new ShapeDrawable(new RectShape());
  19.         mDrawable.getPaint().setColor(ci.getColor());
  20.         mDrawable.getPaint().setAlpha(ci.getAlpha());
  21.         mDrawable.setBounds(width*i, top-h, width*(i+1), top);
  22.         mDrawable.getPaint().setShader(new LinearGradient(width*i, top-h, width*(i+1), top-h, ci.getColor(), Color.WHITE, Shader.TileMode.REPEAT));
  23.         myDraws.add(mDrawable);
  24.       }
  25.       ci.setDraws(myDraws);
  26.       gencharts.add(ci);
  27.     }
  28.     this.charts = gencharts;
  29.     background = new ShapeDrawable(new RectShape());
  30.     background.setBounds(0, 0, ws, top);
  31.     background.getPaint().setColor(bgColor);
  32.   }
  33.  
  34.   @Override
  35.   protected void onDraw(Canvas canvas) {
  36.     background.draw(canvas);
  37.     for (ChartItem ci : charts) {
  38.       ArrayList<ShapeDrawable> draws = ci.getDraws();
  39.       for (int i=0; i<draws.size(); i++) {
  40.         ShapeDrawable d = draws.get(i);
  41.         d.draw(canvas);
  42.       }
  43.     }
  44.   }
  45.   
  46.   private int getMax(int[] ii) {
  47.     for (int i : ii) {
  48.       if (max<i) max = i;
  49.     }
  50.     return max;
  51.   }
  52.   
  53.   private int[] scaleTo(int[] ii, int vmax) {
  54.     int[] scaled = new int[ii.length];
  55.     double k = (double) vmax / getMax(ii);
  56.     for(int i=0; i<ii.length; i++) {
  57.       scaled[i] = (int) (ii[i]*k);
  58.     }
  59.     return scaled;
  60.   }
  61. }
Рассмотрим наш класс подробнее.
В конструктор обязательно нужно передавать Context - его мы сразу же отдадим в конструктор предка. Кроме этого в конструктор мы передаём массив объектов ChartItem, которые представляют данные для каждой из диаграмм, которые мы собираемся построить на нашем изображении. Следующие два параметра общие для всех диаграмм на изображении: максимальная высота и фон. 
В строках 9-10 получаем текущую ширину экрана, чтобы полностью закрасить её фоном. 
Все графические примитивы создаются как объекты ShapeDrawable. В конструкторе мы устанавливаем параметры для прямоугольников, составляющих столбцы диаграмм: ширину, высоту, цвет. Делаем их симпатичнее с помощью градиента (строка 22). Все созданные объекты должны быть нарисованы на канве, для чего переопределяем метод protected void onDraw(Canvas canvas) и в нём для всех ShapeDrawable вызываем метод draw(canvas) (строки 35-44). Делать это нужно в правильном порядке: следующий объект будет рисоваться поверх предыдущего. 
Определённая сложность есть в расчёте высоты столбцов диаграммы: нужно чтобы они поместились в максимальную высоту и при этом сохранили пропорции. Это решаем с помощью метода  private int[] scaleTo(int[] ii, int vmax) в который передаём массив данных для конкретной диаграммы и максимальную высоту. На выходе получаем уже масштабированный массив (строки 53-60). 
Добавляя в setContentView вашего Activity созданный экземпляр СhartView, получаете результат вроде того, что на картинке. В данном случае выбран серый фон и две диаграммы с прозрачностью 50% синего и зелёного цвета. 

пятница, 1 июля 2011 г.

TabWidget в Android: простой способ построить окно с вкладками

Пожалуй, главная проблема при создании интерфейсов в Android-приложениях - нехватка места на экране. Прокрутка спасает не всегда. И часто выгодно бывает реализовать вкладки для "подразделов" содержимого. Вот код метода, который строит интерфейс с вкладками без какой-либо дополнительной XML-разметки:
public static void getTabs(final Activity act) {
TabHost tabHost = new TabHost(act);
tabHost.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));

LinearLayout lll = new LinearLayout(act);
lll.setOrientation(LinearLayout.VERTICAL);

TabWidget tabWidget = new TabWidget(act);
tabWidget.setId(android.R.id.tabs);
lll.addView(tabWidget, new LinearLayout.LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT));

FrameLayout frameLayout = new FrameLayout(act);
frameLayout.setId(android.R.id.tabcontent);
lll.addView(frameLayout, new LinearLayout.LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));

tabHost.addView(lll, LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);

tabHost.setup();

// первая вкладка
TabSpec firstTab = tabHost.newTabSpec("FIRST_TAB");
firstTab.setIndicator("Первая вкладка");
firstTab.setContent(new TabHost.TabContentFactory() {

public View createTabContent(String string) {

TextView in = new TextView(act);
in.setText("Текст в первой вкладке");

return in;
}
});
tabHost.addTab(firstTab);

// вторая вкладка
TabSpec nextTab = tabHost.newTabSpec("NEXT_TAB");
nextTab.setIndicator("Вторая вкладка");
nextTab.setContent(new TabHost.TabContentFactory() {

public View createTabContent(String string) {

TextView in = new TextView(act);
in.setText("Текст во второй вкладке");

return in;
}
});
tabHost.addTab(nextTab);

act.setContentView(tabHost);
}
В параметрах метода передаётся Activity в котором нужно построить вкладки. Туда же можно передать данные для отображения.
Преимущество такого подхода, думаю, очевидно: в зависимости от данных, которые нужно отобразить в окне можно "на лету" менять число вкладок.