В этом уроке:

- обрабатываем нажатия на виджет

 

Продолжаем тему виджетов. Виджет, который показывает информацию – это хорошо, это мы теперь умеем. Но кроме этого виджет еще умеет реагировать на нажатия.

Т.к. прямого доступа к 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, никаких ограничений нет.

 

На следующем уроке:

- создаем виджет со списком


Присоединяйтесь к нам в Telegram:

- в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.

- в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Compose, Kotlin, RxJava, Dagger, Тестирование, Performance 

- ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня




Language

Автор сайта

Дмитрий Виноградов

Подробнее можно посмотреть или почитать.

Никакие другие люди не имеют к этому сайту никакого отношения и просто занимаются плагиатом.

Социальные сети

 

В канале я публикую ссылки на интересные и полезные статьи по Android

В чате можно обсудить вопросы и проблемы, возникающие при разработке



Группа ВКонтакте



Поддержка проекта

Яндекс
410011180491924

WebMoney
R248743991365
Z551306702056

Paypal