Урок 120. Виджеты. Обработка нажатий
В этом уроке:
- обрабатываем нажатия на виджет
Продолжаем тему виджетов. Виджет, который показывает информацию – это хорошо, это мы теперь умеем. Но кроме этого виджет еще умеет реагировать на нажатия.
Т.к. прямого доступа к view-компонентам виджета мы не имеем, то использовать, как обычно, обработчики нажатий не получится. Но RemoteViews, используемый нами для работы с view, позволяет настроить реакцию view на нажатие. Для этого он использует PendingIntent. Т.е. мы можем на нажатие на виджет повесить вызов Activity, Service или BroadcastReceiver. В этом уроке сделаем непростой, но достаточно содержательный пример, отражающий различные техники реагирования на нажатия.
Создадим виджет, состоящий из двух текстов и трех зон для нажатий.
Первый текст будет отображать время последнего обновления, а второй – кол-во нажатий на третью зону нажатия.
Первая зона будет по клику открывать конфигурационное Activity. Это пригодится в том случае, когда вы хотите дать пользователю возможность донастроить виджет после установки. Конфигурировать будем формат отображаемого в первой строке времени.
Вторая зона нажатия будет просто обновлять виджет, тем самым будет меняться время в первом тексте.
Каждое нажатие на третью зону будет увеличивать на единицу счетчик нажатий и обновлять виджет. Тем самым будет меняться второй текст, отображающий текущее значение счетчика.
Для простоты, конечно, можно было разбить этот пример на три отдельных виджета. Но я решил сделать все в одном, чтобы наглядно показать, что один виджет может совершать разные действия в ответ на нажатия на разные view.
Создадим проект без Activity:
Project name: P1201_ClickWidget
Build Target: Android 2.3.3
Application name: ClickWidget
Package name: ru.startandroid.develop.p1201clickwidget
strings.xml:
<string name="config">Config</string>
<string name="update">Update</string>
<string name="count">Count</string>
<string name="ok">Ok</string>
Layout-файл виджета widget.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/tvTime"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#fff"
android:textColor="#000">
</TextView>
<TextView
android:id="@+id/tvCount"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#fff"
android:textColor="#000">
</TextView>
<TextView
android:id="@+id/tvPressConfig"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="#66ff0000"
android:gravity="center"
android:text="@string/config"
android:textColor="#000">
</TextView>
<TextView
android:id="@+id/tvPressUpdate"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="#6600ff00"
android:gravity="center"
android:text="@string/update"
android:textColor="#000">
</TextView>
<TextView
android:id="@+id/tvPressCount"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="#660000ff"
android:gravity="center"
android:text="@string/count"
android:textColor="#000">
</TextView>
</LinearLayout>
Первые два TextView – это тексты, а последние три – зоны нажатия.
Layout-файл для конфигурационного экрана config.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<EditText
android:id="@+id/etFormat"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10">
</EditText>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="onClick"
android:text="@string/ok">
</Button>
</LinearLayout>
Поле для ввода формата даты и кнопка подтверждения
Класс конфигурационного экрана ConfigActivity.java:
package ru.startandroid.develop.p1201clickwidget;
import android.app.Activity;
import android.appwidget.AppWidgetManager;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
public class ConfigActivity extends Activity {
public final static String WIDGET_PREF = "widget_pref";
public final static String WIDGET_TIME_FORMAT = "widget_time_format_";
public final static String WIDGET_COUNT = "widget_count_";
int widgetID = AppWidgetManager.INVALID_APPWIDGET_ID;
Intent resultValue;
SharedPreferences sp;
EditText etFormat;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// извлекаем ID конфигурируемого виджета
Intent intent = getIntent();
Bundle extras = intent.getExtras();
if (extras != null) {
widgetID = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
}
// и проверяем его корректность
if (widgetID == AppWidgetManager.INVALID_APPWIDGET_ID) {
finish();
}
// формируем intent ответа
resultValue = new Intent();
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetID);
// отрицательный ответ
setResult(RESULT_CANCELED, resultValue);
setContentView(R.layout.config);
sp = getSharedPreferences(WIDGET_PREF, MODE_PRIVATE);
etFormat = (EditText) findViewById(R.id.etFormat);
etFormat.setText(sp.getString(WIDGET_TIME_FORMAT + widgetID, "HH:mm:ss"));
int cnt = sp.getInt(ConfigActivity.WIDGET_COUNT + widgetID, -1);
if (cnt == -1) sp.edit().putInt(WIDGET_COUNT + widgetID, 0);
}
public void onClick(View v){
sp.edit().putString(WIDGET_TIME_FORMAT + widgetID, etFormat.getText().toString()).commit();
//MyWidget.updateWidget(this, AppWidgetManager.getInstance(this), widgetID);
setResult(RESULT_OK, resultValue);
finish();
}
}
Тут ничего нового для нас нет.
В onCreate мы извлекаем и проверяем ID экземпляра виджета, для которого открылся конфигурационный экран. Далее формируем отрицательный ответ на случай нажатия кнопки Назад. Читаем формат времени и помещаем его в EditText. Читаем значение счетчика и, если это значения еще нет в Preferences, то пишем туда 0.
В onClick мы сохраняем в Preferences формат из EditText, обновляем виджет, формируем положительный ответ и выходим.
Код обновления виджета пока закоментен, т.к. у нас еще нет класса MyWidget. Сейчас создадим и можно будет раскоментить.
Класс виджета MyWidget.java:
package ru.startandroid.develop.p1201clickwidget;
import java.sql.Date;
import java.text.SimpleDateFormat;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.Bundle;
import android.widget.RemoteViews;
public class MyWidget extends AppWidgetProvider {
final static String ACTION_CHANGE = "ru.startandroid.develop.p1201clickwidget.change_count";
public void onUpdate(Context context, AppWidgetManager appWidgetManager,
int[] appWidgetIds) {
super.onUpdate(context, appWidgetManager, appWidgetIds);
// обновляем все экземпляры
for (int i : appWidgetIds) {
updateWidget(context, appWidgetManager, i);
}
}
public void onDeleted(Context context, int[] appWidgetIds) {
super.onDeleted(context, appWidgetIds);
// Удаляем Preferences
Editor editor = context.getSharedPreferences(
ConfigActivity.WIDGET_PREF, Context.MODE_PRIVATE).edit();
for (int widgetID : appWidgetIds) {
editor.remove(ConfigActivity.WIDGET_TIME_FORMAT + widgetID);
editor.remove(ConfigActivity.WIDGET_COUNT + widgetID);
}
editor.commit();
}
static void updateWidget(Context ctx, AppWidgetManager appWidgetManager,
int widgetID) {
SharedPreferences sp = ctx.getSharedPreferences(
ConfigActivity.WIDGET_PREF, Context.MODE_PRIVATE);
// Читаем формат времени и определяем текущее время
String timeFormat = sp.getString(ConfigActivity.WIDGET_TIME_FORMAT
+ widgetID, null);
if (timeFormat == null) return;
SimpleDateFormat sdf = new SimpleDateFormat(timeFormat);
String currentTime = sdf.format(new Date(System.currentTimeMillis()));
// Читаем счетчик
String count = String.valueOf(sp.getInt(ConfigActivity.WIDGET_COUNT
+ widgetID, 0));
// Помещаем данные в текстовые поля
RemoteViews widgetView = new RemoteViews(ctx.getPackageName(),
R.layout.widget);
widgetView.setTextViewText(R.id.tvTime, currentTime);
widgetView.setTextViewText(R.id.tvCount, count);
// Конфигурационный экран (первая зона)
Intent configIntent = new Intent(ctx, ConfigActivity.class);
configIntent.setAction(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE);
configIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetID);
PendingIntent pIntent = PendingIntent.getActivity(ctx, widgetID,
configIntent, 0);
widgetView.setOnClickPendingIntent(R.id.tvPressConfig, pIntent);
// Обновление виджета (вторая зона)
Intent updateIntent = new Intent(ctx, MyWidget.class);
updateIntent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
updateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS,
new int[] { widgetID });
pIntent = PendingIntent.getBroadcast(ctx, widgetID, updateIntent, 0);
widgetView.setOnClickPendingIntent(R.id.tvPressUpdate, pIntent);
// Счетчик нажатий (третья зона)
Intent countIntent = new Intent(ctx, MyWidget.class);
countIntent.setAction(ACTION_CHANGE);
countIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetID);
pIntent = PendingIntent.getBroadcast(ctx, widgetID, countIntent, 0);
widgetView.setOnClickPendingIntent(R.id.tvPressCount, pIntent);
// Обновляем виджет
appWidgetManager.updateAppWidget(widgetID, widgetView);
}
public void onReceive(Context context, Intent intent) {
super.onReceive(context, intent);
// Проверяем, что это intent от нажатия на третью зону
if (intent.getAction().equalsIgnoreCase(ACTION_CHANGE)) {
// извлекаем ID экземпляра
int mAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
Bundle extras = intent.getExtras();
if (extras != null) {
mAppWidgetId = extras.getInt(
AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
}
if (mAppWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID) {
// Читаем значение счетчика, увеличиваем на 1 и записываем
SharedPreferences sp = context.getSharedPreferences(
ConfigActivity.WIDGET_PREF, Context.MODE_PRIVATE);
int cnt = sp.getInt(ConfigActivity.WIDGET_COUNT + mAppWidgetId, 0);
sp.edit().putInt(ConfigActivity.WIDGET_COUNT + mAppWidgetId,
++cnt).commit();
// Обновляем виджет
updateWidget(context, AppWidgetManager.getInstance(context),
mAppWidgetId);
}
}
}
}
А вот тут уже немного посложнее.
В onUpdate мы обновляем все требующие обновления экземпляры, в onDelete подчищаем Preferences после удаления экземпляров.
Метод updateWidget отвечает за обновления конкретного экземпляра виджета. Здесь мы настраиваем внешний вид и реакцию на нажатие.
Сначала мы читаем настройки формата времени (которые были сохранены в конфигурационном экране), берем текущее время и конвертируем в строку согласно формату. Также из настроек читаем значение счетчика. Создаем RemoteViews и помещаем время и счетчик в соответствующие TextView.
Далее идет настройка обработки нажатия. Механизм несложен. Сначала мы готовим Intent, который содержит в себе некие данные и знает куда он должен отправиться. Этот Intent мы упаковываем в PendingIntent. Далее конкретному view-компоненту мы методом setOnClickPendingIntent сопоставляем PendingIntent. И когда будет совершено нажатие на этот view, система достанет Intent из PendingIntent и отправит его по назначению.
В нашем виджете есть три зоны для нажатия. Для каждой из них мы формируем отдельный Intent и PendingIntent.
Первая зона – по нажатию должно открываться конфигурационное Activity. Создаем Intent, который будет вызывать наше Activity, помещаем данные об ID (чтобы экран знал, какой экземпляр он настраивает), упаковываем в PendingIntent и сопоставляем view-компоненту первой зоны.
Вторая зона – по нажатию должен обновляться виджет, на котором было совершено нажатие. Создаем Intent, который будет вызывать наш класс виджета, добавляем ему action = ACTION_APPWIDGET_UPDATE, помещаем данные об ID (чтобы обновился именно этот экземпляр), упаковываем в PendingIntent и сопоставляем view-компоненту второй зоны.
Третья зона – по нажатию должен увеличиваться на единицу счетчик нажатий. Создаем Intent, который будет вызывать наш класс виджета, добавляем ему наш собственный action = ACTION_CHANGE, помещаем данные об ID (чтобы работать со счетчиком именно этого экземпляра), упаковываем в PendingIntent и сопоставляем view-компоненту третьей зоны.
Теперь при нажатии на первую зону будет вызван конфигурационный экран. По нажатию на вторую будет обновлен виджет. А вот нажатие на третью ни к чему не приведет, т.к. наш класс MyWidget знает, как работать с Intent с action вида ACTION_APPWIDGET_UPDATE, ACTION_APPWIDGET_DELETED и пр. А мы ему послали свой левый action.
Значит надо научить его понимать наш Intent. Вспоминаем, что MyWidget – это расширение AppWidgetProvider, а AppWidgetProvider – это расширение BroadcastReceiver. А значит, мы можем сами реализовать метод onReceive, в котором будем ловить наш action и выполнять нужные нам действия.
В методе onReceive мы обязательно выполняем метод onReceive родительского класса, иначе просто перестанут работать обновления и прочие стандартные события виджета. Далее мы проверяем, что intent содержит наш action, читаем и проверяем ID из него, читаем из настроек значение счетчика, увеличиваем на единицу, пишем обратно в настройки и обновляем экземпляр виджета. Он прочтет новое значение счетчика из настроек и отобразит его.
Вы обратили внимание, что при создании PendingIntent мы использовали ID экземпляров виджета в качестве requestCode? Поясняю, зачем это сделано. Допустим, мы создаем два экземпляра виджета. Первый создается и создает свои PendingIntent для обновления, счетчика и конфигурирования. Эти PendingIntent содержат action и extra-данные. Теперь создается второй экземпляр. Он также пытается создать свои PendingIntent с теми же action и другими extra-данными. Тут мы вспоминаем прошлый урок, а именно дефолтное поведение системы. Если создаваемый PendingIntent похож на существующий, то создаваемый станет копией уже существующего. Т.е. все PendingIntent второго экземпляра получат extra-данные из Intent первого. В extra-данных у нас лежит ID экземпляра. Значит второй экземпляр виджета будет обновлять время/счетчик и открывать конфигурационный экран первого экземпляра. Если интересно, можете поставить нули вместо ID при создании PendingIntent и убедиться, что так все и будет. Чтобы избежать этого, используем requestCode. Надеюсь, что этот момент понятен, т.к. для этого и была написана бОльшая часть прошлого урока )
Теперь не забудьте раскаментить код обновления виджета в классе ConfigActivity в методе onClick. Иначе ничего работать не будет.
Создадим файл метаданных xml/widget_metadata.xml:
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider
xmlns:android="http://schemas.android.com/apk/res/android"
android:configure="ru.startandroid.develop.p1201clickwidget.ConfigActivity"
android:initialLayout="@layout/widget"
android:minHeight="110dp"
android:minWidth="40dp"
android:updatePeriodMillis="0">
</appwidget-provider>
Виджет будет вертикальным. Число 0 – в updatePeriodMillis говорит о том, что виджет не будет обновляться системой. Мы его сами обновлять будем.
Осталось прописать классы в манифесте. Должен получиться примерно такой фрагмент кода:
<receiver
android:name="MyWidget">
<intent-filter>
<action
android:name="android.appwidget.action.APPWIDGET_UPDATE">
</action>
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_metadata">
</meta-data>
</receiver>
<activity
android:name="ConfigActivity">
<intent-filter>
<action
android:name="android.appwidget.action.APPWIDGET_CONFIGURE">
</action>
</intent-filter>
</activity>
Если для Receiver не указана иконка и текст, он возьмет их из приложения.
Т.е. наш виджет будет иметь стандартную системную иконку и имя приложения – ClickWidget.
Все сохраняем и инсталлим приложение.
Для наглядности давайте создадим пару экземпляров виджета. Настройки в конфигурационном экране пока оставляйте дефолтными.
Виджеты отображают время, когда они были последний раз обновлены и счетчик нажатий.
Теперь понажимайте Update на обоих виджетах, время будет обновляться. А, нажимая Count, вы меняете значение счетчика, и виджет это отображает. Вместе со счетчиком, кстати, актуализируется и время, т.к. оно актуализируется при каждом обновлении виджета.
Нажав на Config, мы попадаем в конфигурационный экран. Здесь можно изменить формат отображаемого времени. Настроим так, чтобы первый экземпляр отображал только часы и минуты
а второй – секунды
Получилось так
Предлагаю вам самостоятельно допилить виджет так, чтобы при нажатии на Count обновлялся только счетчик, а время не менялось. Также попробуйте добавить еще одну (четвертую) зону, по нажатию на которую открывался бы, например, www.google.com в браузере.
На всякий случай проговорю явно следующее. В нашем примере мы при нажатиях вызывали через Intent свои же классы. Но, думаю, всем понятно, что можно вызывать все, что позволит Intent, никаких ограничений нет.
На следующем уроке:
- создаем виджет со списком