Статьи

Убийца способ показать список элементов в Android Collection Widget

В более ранних версиях Android виджеты приложений могли отображать только такие представления, как TextView , ImageView и т. Д. Но что, если мы хотим отобразить список элементов в нашем виджете? Например, отображение списка информации о температуре за всю следующую неделю. Коллекционные виджеты были представлены в Android 3.0, чтобы обеспечить это дополнительное преимущество. Коллекционные виджеты поддерживают макеты ListView , GridView и StackView .

Сегодня я собираюсь помочь вам понять, как работает виджет коллекции. Мы собираемся создать виджет приложения для приложения Todo. Коллекционные виджеты будут использоваться для отображения списка ожидающих задач.

Я предполагаю, что вы уже знаете, как сделать основной виджет приложения. Если нет, пожалуйста, обратитесь к этой статье и вернитесь, когда будете готовы создавать собственные коллекции виджетов.

Начиная

Пожалуйста, загрузите код стартового проекта здесь, как мы будем строить из него.

В коде уже реализован базовый виджет, мы собираемся создать виджет коллекции в том же проекте. Базовый виджет показывает количество ожидающих задач, а виджет коллекции покажет полный список. Для создания виджета коллекции необходимы два основных компонента в дополнение к базовым компонентам:

  • RemoteViewsService
  • RemoteViewsFactory

Давайте разберемся, что делают эти компоненты.

Использование RemoteViewsFactory

RemoteViewsFactory служит для адаптера в контексте виджета. Адаптер используется для соединения элементов коллекции (например, элементов ListView или элементов GridView) с набором данных.
Давайте добавим этот класс в наш проект. Создайте новый класс Java, назовите его MyWidgetRemoteViewsFactory и установите его для реализации класса RemoteViewsService.RemoteViewsFactory .

 public class MyWidgetRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory { private Context mContext; private Cursor mCursor; public MyWidgetRemoteViewsFactory(Context applicationContext, Intent intent) { mContext = applicationContext; } @Override public void onCreate() { } @Override public void onDataSetChanged() { if (mCursor != null) { mCursor.close(); } final long identityToken = Binder.clearCallingIdentity(); Uri uri = Contract.PATH_TODOS_URI; mCursor = mContext.getContentResolver().query(uri, null, null, null, Contract._ID + " DESC"); Binder.restoreCallingIdentity(identityToken); } @Override public void onDestroy() { if (mCursor != null) { mCursor.close(); } } @Override public int getCount() { return mCursor == null ? 0 : mCursor.getCount(); } @Override public RemoteViews getViewAt(int position) { if (position == AdapterView.INVALID_POSITION || mCursor == null || !mCursor.moveToPosition(position)) { return null; } RemoteViews rv = new RemoteViews(mContext.getPackageName(), R.layout.collection_widget_list_item); rv.setTextViewText(R.id.widgetItemTaskNameLabel, mCursor.getString(1)); return rv; } @Override public RemoteViews getLoadingView() { return null; } @Override public int getViewTypeCount() { return 1; } @Override public long getItemId(int position) { return mCursor.moveToPosition(position) ? mCursor.getLong(0) : position; } @Override public boolean hasStableIds() { return true; } } 

В приведенном выше коде MyWidgetRemoteViewsFactory переопределяет несколько методов из класса RemoteViewsFactory :

  • onCreate вызывается, когда appwidget создается впервые.
  • onDataSetChanged вызывается всякий раз, когда обновляется приложение.
  • getCount возвращает количество записей в курсоре. (В нашем случае количество элементов задачи, которые должны отображаться в виджете приложения)
  • getViewAt обрабатывает всю обработку. Он возвращает объект RemoteViews который в нашем случае является единственным элементом списка.
  • getViewTypeCount возвращает количество типов представлений, которые мы имеем в ListView . В нашем случае мы имеем одинаковые типы представлений в каждом элементе ListView поэтому мы return 1 там.

Использование RemoteViewsService

Основная цель RemoteViewsService — вернуть объект RemoteViewsFactory который дополнительно обрабатывает задачу заполнения виджета соответствующими данными. В этом классе мало что происходит.

Создайте новый класс с именем MyWidgetRemoteViewsService, расширяющий класс RemoteViewsService .

 public class MyWidgetRemoteViewsService extends RemoteViewsService { @Override public RemoteViewsFactory onGetViewFactory(Intent intent) { return new MyWidgetRemoteViewsFactory(this.getApplicationContext(), intent); } } 

Как и со всеми другими сервисами в Android, мы должны зарегистрировать этот сервис в файле манифеста.

 <service android:name=".AppWidget.MyWidgetRemoteViewsService" android:permission="android.permission.BIND_REMOTEVIEWS"></service> 

Обратите внимание на специальное разрешение android.permission.BIND_REMOTEVIEWS . Это позволяет системе связывать ваш сервис для создания представлений виджетов для каждой строки и предотвращает доступ других приложений к данным вашего виджета.

Запуск RemoteViewsService

Теперь, когда у нас настроены дополнительные компоненты, пришло время создать WidgetProvider для вызова RemoteViewsService .

Создайте новый класс в пакете AppWidget и назовите его CollectionAppWidgetProvider :

 @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { for (int appWidgetId : appWidgetIds) { RemoteViews views = new RemoteViews( context.getPackageName(), R.layout.collection_widget ); Intent intent = new Intent(context, MyWidgetRemoteViewsService.class); views.setRemoteAdapter(R.id.widgetListView, intent); appWidgetManager.updateAppWidget(appWidgetId, views); } } 

Создание макета виджета

Теперь создайте новый файл ресурсов в res / xml и назовите его collection_widget.xml .

В этом файле мы определяем настройки виджета, например, какой файл макета должен использовать виджет, и добавляем изображение для предварительного просмотра для лучшего взаимодействия с пользователем.

 <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:minWidth="40dp" android:minHeight="40dp" android:updatePeriodMillis="864000" android:previewImage="@drawable/simple_widget_preview" android:initialLayout="@layout/collection_widget" android:resizeMode="horizontal|vertical" android:widgetCategory="home_screen"> </appwidget-provider> 

Создайте еще один файл ресурсов, но на этот раз в res / layout и назовите его collection_widget.xml
В этом файле мы определяем макет того, что мы хотим показать в нашем виджете коллекции. Мы собираемся иметь заголовок сверху, а затем ListView внизу, чтобы отобразить список задач.

 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/colorWhite" xmlns:tools="http://schemas.android.com/tools" android:orientation="vertical"> <FrameLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView android:layout_width="match_parent" android:id="@+id/widgetTitleLabel" android:text="@string/title_collection_widget" android:textColor="@color/colorWhite" android:background="@color/colorPrimary" android:textSize="18dp" android:gravity="center" android:textAllCaps="true" android:layout_height="@dimen/widget_title_min_height"></TextView> </FrameLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <ListView android:id="@+id/widgetListView" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/colorWhite" android:dividerHeight="1dp" android:divider="#eeeeee" tools:listitem="@layout/collection_widget_list_item"></ListView> </LinearLayout> </LinearLayout> 1 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/colorWhite" xmlns:tools="http://schemas.android.com/tools" android:orientation="vertical"> <FrameLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView android:layout_width="match_parent" android:id="@+id/widgetTitleLabel" android:text="@string/title_collection_widget" android:textColor="@color/colorWhite" android:background="@color/colorPrimary" android:textSize="18dp" android:gravity="center" android:textAllCaps="true" android:layout_height="@dimen/widget_title_min_height"></TextView> </FrameLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <ListView android:id="@+id/widgetListView" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/colorWhite" android:dividerHeight="1dp" android:divider="#eeeeee" tools:listitem="@layout/collection_widget_list_item"></ListView> </LinearLayout> </LinearLayout> 

Нам нужно создать еще один файл в res / layout, чтобы определить макет каждого элемента списка.

Создайте этот файл и назовите его collection_widget_list_item.xml

 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="match_parent" android:paddingLeft="@dimen/widget_listview_padding_x" android:paddingRight="@dimen/widget_listview_padding_x" android:paddingStart="@dimen/widget_listview_padding_x" android:paddingEnd="@dimen/widget_listview_padding_x" android:minHeight="@dimen/widget_listview_item_height" android:weightSum="2" android:id="@+id/widgetItemContainer" android:layout_height="wrap_content"> <TextView android:id="@+id/widgetItemTaskNameLabel" android:layout_width="wrap_content" android:gravity="start" android:layout_weight="1" android:textColor="@color/text" android:layout_gravity="center_vertical" android:layout_height="wrap_content"></TextView> </LinearLayout> 

Запустите приложение сейчас, вы должны увидеть виджет, заполненный элементами todo. (Убедитесь, что вы переустановили приложение, чтобы увидеть изменения. Вы также можете отключить опцию Instant Run в Android Studio).

Обновление виджета вручную

Логика выглядит следующим образом: всякий раз, когда вы создаете новый элемент todo, вы должны отправить Broadcast на WidgetProvider .
Определите новый метод в классе CollectionAppWidgetProvider .

 public static void sendRefreshBroadcast(Context context) { Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); intent.setComponent(new ComponentName(context, CollectionAppWidgetProvider.class)); context.sendBroadcast(intent); } 

затем переопределите метод onReceive в классе CollectionAppWidgetProvider ,

 @Override public void onReceive(final Context context, Intent intent) { final String action = intent.getAction(); if (action.equals(AppWidgetManager.ACTION_APPWIDGET_UPDATE)) { // refresh all your widgets AppWidgetManager mgr = AppWidgetManager.getInstance(context); ComponentName cn = new ComponentName(context, CollectionAppWidgetProvider.class); mgr.notifyAppWidgetViewDataChanged(mgr.getAppWidgetIds(cn), R.id.widgetListView); } super.onReceive(context, intent); } 

Когда новая задача todo создана, вызовите метод sendRefreshBroadcast определенный в классе CollectionAppWidgetProvider .

В MainActivity измените метод addTodoItem соответствующим образом.

 a.runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(mContext, "New task created", Toast.LENGTH_LONG).show(); getTodoList(); // this will send the broadcast to update the appwidget CollectionAppWidgetProvider.sendRefreshBroadcast(mContext); } }); 

Обработка событий в виджетах

В нашем виджете у нас есть заголовок вверху и список внизу. Поэтому, когда пользователь нажимает на заголовок, мы запускаем приложение. Когда в представлении списка щелкается один элемент, мы запускаем операцию детализации. В нашем приложении todo детализация действий может быть не такой полезной, но давайте просто сделаем это, чтобы понять концепцию.

Нажмите событие на отдельных просмотров

Добавить события кликов в представления, такие как TextView , ImageView и т. Д. Очень легко. Вот обновленный код для метода onUpdate .

 @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { for (int appWidgetId : appWidgetIds) { RemoteViews views = new RemoteViews( context.getPackageName(), R.layout.collection_widget ); // click event handler for the title, launches the app when the user clicks on title Intent titleIntent = new Intent(context, MainActivity.class); PendingIntent titlePendingIntent = PendingIntent.getActivity(context, 0, titleIntent, 0); views.setOnClickPendingIntent(R.id.widgetTitleLabel, titlePendingIntent); Intent intent = new Intent(context, MyWidgetRemoteViewsService.class); views.setRemoteAdapter(R.id.widgetListView, intent); appWidgetManager.updateAppWidget(appWidgetId, views); } } 

Идея здесь похожа на то, как мы добавляем события кликов в наши приложения. Но так как виджеты работают в другом контексте, нам нужно зарегистрировать событие click через PendingIntent .

События ListView на ListView

Добавление событий щелчка к ListView не так просто, как настройка setOnItemClickListener для объекта ListView . Это требует некоторых дополнительных шагов.

Сначала вам нужно настроить шаблон для PendingIntent . Добавьте этот код в метод onUpdate в классе CollectionAppWidgetProvider после views.setRemoteAdapter(R.id.widgetListView, intent);

 // template to handle the click listener for each item Intent clickIntentTemplate = new Intent(context, DetailsActivity.class); PendingIntent clickPendingIntentTemplate = TaskStackBuilder.create(context) .addNextIntentWithParentStack(clickIntentTemplate) .getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); views.setPendingIntentTemplate(R.id.widgetListView, clickPendingIntentTemplate); 

Для каждого ListView мы запускаем DetailsActivity которое будет просто отображать описание задачи, отправленное как дополнительное.

Затем заполняйте этот шаблон каждый раз, когда RemoteViews создает новый объект RemoteViewsFactory .
Добавьте этот код в метод getViewAt класса MyWidgetRemoteViewsFactory .

 Intent fillInIntent = new Intent(); fillInIntent.putExtra(CollectionAppWidgetProvider.EXTRA_LABEL, mCursor.getString(1)); rv.setOnClickFillInIntent(R.id.widgetItemContainer, fillInIntent); 

Здесь мы заполняем шаблон ожидающих намерений, определенный в классе CollectionAppWidgetProvider . Обратите внимание, что мы хотим сделать всю строку кликабельной, поэтому мы устанавливаем прослушиватель кликов для корневого элемента collection_widget_list_item.xml

Вывод

В этой статье я попытался помочь с наиболее распространенными проблемами, с которыми обычно сталкиваются новички. Если у вас есть какие-либо вопросы или если что-то не работает для вас, дайте мне знать в комментариях ниже.

Вы можете скачать полный рабочий код здесь .