Создайте расширенный виджет

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

Оптимизация обновления содержимого виджета

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

Типы обновлений виджетов

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

Ниже описан каждый тип обновления и приведены фрагменты кода для каждого из них.

  • Полное обновление: вызовите AppWidgetManager.updateAppWidget(int, android.widget.RemoteViews) для полного обновления виджета. Это заменит ранее предоставленные RemoteViews на новый RemoteViews . Это самое затратное с точки зрения вычислений обновление.

    Котлин

    val appWidgetManager = AppWidgetManager.getInstance(context)
    val remoteViews = RemoteViews(context.getPackageName(), R.layout.widgetlayout).also {
    setTextViewText(R.id.textview_widget_layout1, "Updated text1")
    setTextViewText(R.id.textview_widget_layout2, "Updated text2")
    }
    appWidgetManager.updateAppWidget(appWidgetId, remoteViews)

    Ява

    AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
    RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widgetlayout);
    remoteViews.setTextViewText(R.id.textview_widget_layout1, "Updated text1");
    remoteViews.setTextViewText(R.id.textview_widget_layout2, "Updated text2");
    appWidgetManager.updateAppWidget(appWidgetId, remoteViews);
  • Частичное обновление: вызовите AppWidgetManager.partiallyUpdateAppWidget для обновления частей виджета. Это объединяет новые RemoteViews с ранее предоставленными RemoteViews . Этот метод игнорируется, если виджет не получает хотя бы одно полное обновление через updateAppWidget(int[], RemoteViews) .

    Котлин

    val appWidgetManager = AppWidgetManager.getInstance(context)
    val remoteViews = RemoteViews(context.getPackageName(), R.layout.widgetlayout).also {
    setTextViewText(R.id.textview_widget_layout, "Updated text")
    }
    appWidgetManager.partiallyUpdateAppWidget(appWidgetId, remoteViews)

    Ява

    AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
    RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widgetlayout);
    remoteViews.setTextViewText(R.id.textview_widget_layout, "Updated text");
    appWidgetManager.partiallyUpdateAppWidget(appWidgetId, remoteViews);
  • Обновление данных коллекции: вызовите AppWidgetManager.notifyAppWidgetViewDataChanged , чтобы сделать данные представления коллекции недействительными в вашем виджете. Это запускает RemoteViewsFactory.onDataSetChanged . В это время в виджете отображаются старые данные. С помощью этого метода вы можете безопасно выполнять ресурсоемкие задачи синхронно.

    Котлин

    val appWidgetManager = AppWidgetManager.getInstance(context)
    appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.widget_listview)

    Ява

    AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
    appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.widget_listview);

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

Определите, как часто обновлять виджет

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

Периодически обновлять

Вы можете контролировать частоту периодического обновления, указав значение для AppWidgetProviderInfo.updatePeriodMillis в XML appwidget-provider . Каждое обновление запускает метод AppWidgetProvider.onUpdate() , в котором вы можете разместить код для обновления виджета. Однако рассмотрите альтернативы для обновлений широковещательного приемника, описанные в следующем разделе, если ваш виджет должен загружать данные асинхронно или требует более 10 секунд для обновления, поскольку по истечении 10 секунд система считает BroadcastReceiver неотзывчивым.

updatePeriodMillis не поддерживает значения менее 30 минут. Однако, если вы хотите отключить периодические обновления, вы можете указать 0.

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

Обновление в ответ на взаимодействие с пользователем

Вот несколько рекомендуемых способов обновления виджета на основе взаимодействия с пользователем:

  • Из активности приложения: напрямую вызовите AppWidgetManager.updateAppWidget в ответ на взаимодействие с пользователем, например, на нажатие пользователем.

  • Из удаленных взаимодействий, таких как уведомление или виджет приложения: создайте PendingIntent , затем обновите виджет из вызванного Activity , Broadcast или Service . Вы можете выбрать свой собственный приоритет. Например, если вы выбираете Broadcast для PendingIntent , вы можете выбрать трансляцию переднего плана , чтобы дать приоритет BroadcastReceiver .

Обновление в ответ на трансляцию события

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

Вы можете запланировать задание с помощью JobScheduler и указать трансляцию в качестве триггера, используя метод JobInfo.Builder.addTriggerContentUri .

Вы также можете зарегистрировать BroadcastReceiver для трансляции, например, прослушивая ACTION_LOCALE_CHANGED . Однако, поскольку это потребляет ресурсы устройства, используйте это с осторожностью и прослушивайте только определенную трансляцию. С введением ограничений трансляции в Android 7.0 (уровень API 24) и Android 8.0 (уровень API 26) приложения не могут регистрировать неявные трансляции в своих манифестах, за некоторыми исключениями .

Что следует учитывать при обновлении виджета из BroadcastReceiver

Если виджет обновляется с помощью BroadcastReceiver , включая AppWidgetProvider , учтите следующие соображения относительно продолжительности и приоритета обновления виджета.

Продолжительность обновления

Как правило, система позволяет широковещательным приемникам, которые обычно работают в основном потоке приложения, работать до 10 секунд, прежде чем считать их неотзывчивыми и вызывать ошибку Application Not Responding (ANR). Чтобы избежать блокировки основного потока при обработке трансляции, используйте метод goAsync . Если обновление виджета занимает больше времени, рассмотрите возможность планирования задачи с помощью WorkManager .

Caution: Any work you do here blocks further broadcasts until it completes,
so it can slow the receiving of later events.

Более подробную информацию см. в разделе «Соображения безопасности и передовой опыт» .

Приоритет обновления

По умолчанию трансляции, включая те, которые сделаны с помощью AppWidgetProvider.onUpdate , запускаются как фоновые процессы. Это означает, что перегруженные системные ресурсы могут вызвать задержку в вызове приемника трансляций. Чтобы приоритизировать трансляцию, сделайте ее процессом переднего плана.

Например, добавьте флаг Intent.FLAG_RECEIVER_FOREGROUND к Intent , передаваемому в PendingIntent.getBroadcast , когда пользователь нажимает на определенную часть виджета.

Создавайте точные предварительные просмотры, включающие динамические элементы

Рисунок 1: Предварительный просмотр виджета, не отображающего элементы списка.

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

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

Чтобы предварительные просмотры виджетов с представлениями коллекций отображались правильно в селекторе виджетов, мы рекомендуем поддерживать отдельный файл макета, предназначенный только для предварительного просмотра. Этот отдельный файл макета включает в себя реальный макет виджета и представление коллекции-заполнителя с поддельными элементами. Например, вы можете имитировать ListView , предоставив заполнитель LinearLayout с несколькими поддельными элементами списка.

Чтобы проиллюстрировать пример для ListView , начнем с отдельного файла макета:

// res/layout/widget_preview.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:background="@drawable/widget_background"
   android:orientation="vertical">

    // Include the actual widget layout that contains ListView.
    <include
        layout="@layout/widget_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    // The number of fake items you include depends on the values you provide
    // for minHeight or targetCellHeight in the AppWidgetProviderInfo
    // definition.

    <TextView android:text="@string/fake_item1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginVertical="?attr/appWidgetInternalPadding" />

    <TextView android:text="@string/fake_item2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginVertical="?attr/appWidgetInternalPadding" />

</LinearLayout>

Укажите файл макета предварительного просмотра при предоставлении атрибута previewLayout метаданных AppWidgetProviderInfo . Вы по-прежнему указываете фактический макет виджета для атрибута initialLayout и используете фактический макет виджета при построении RemoteViews во время выполнения.

<appwidget-provider
    previewLayout="@layout/widget_previe"
    initialLayout="@layout/widget_view" />

Сложные элементы списка

Пример в предыдущем разделе предоставляет поддельные элементы списка, поскольку элементы списка являются объектами TextView . Может быть сложнее предоставить поддельные элементы, если элементы являются сложными макетами.

Рассмотрим элемент списка, который определен в widget_list_item.xml и состоит из двух объектов TextView :

<LinearLayout  xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

    <TextView android:id="@id/title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/fake_title" />

    <TextView android:id="@id/content"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/fake_content" />
</LinearLayout>

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

  1. Создайте набор атрибутов для текстовых значений:

    <resources>
        <attr name="widgetTitle" format="string" />
        <attr name="widgetContent" format="string" />
    </resources>
    
  2. Используйте эти атрибуты для задания текста:

    <LinearLayout  xmlns:android="http://schemas.android.com/apk/res/android"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">
    
        <TextView android:id="@id/title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="?widgetTitle" />
    
        <TextView android:id="@id/content"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="?widgetContent" />
    </LinearLayout>
    
  3. Создайте столько стилей, сколько необходимо для предварительного просмотра. Переопределите значения в каждом стиле:

    <resources>
    
        <style name="Theme.Widget.ListItem">
            <item name="widgetTitle"></item>
            <item name="widgetContent"></item>
        </style>
        <style name="Theme.Widget.ListItem.Preview1">
            <item name="widgetTitle">Fake Title 1</item>
            <item name="widgetContent">Fake content 1</item>
        </style>
        <style name="Theme.Widget.ListItem.Preview2">
            <item name="widgetTitle">Fake title 2</item>
            <item name="widgetContent">Fake content 2</item>
        </style>
    
    </resources>
    
  4. Примените стили к поддельным элементам в макете предварительного просмотра:

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
       android:layout_width="match_parent"
       android:layout_height="wrap_content" ...>
    
        <include layout="@layout/widget_view" ... />
    
        <include layout="@layout/widget_list_item"
            android:theme="@style/Theme.Widget.ListItem.Preview1" />
    
        <include layout="@layout/widget_list_item"
            android:theme="@style/Theme.Widget.ListItem.Preview2" />
    
    </LinearLayout>