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