Создайте простой виджет

Виджеты приложений — это миниатюрные представления приложений, которые можно встраивать в другие приложения, например на главный экран, и получать периодические обновления. Эти представления называются виджетами в пользовательском интерфейсе, и вы можете опубликовать их с помощью поставщика виджетов приложения (или поставщика виджетов ). Компонент приложения, содержащий другие виджеты, называется хостом виджета приложения (или хостом виджета ). На рис. 1 показан пример музыкального виджета:

Пример музыкального виджета
Рисунок 1. Пример музыкального виджета.

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

Информацию о том, как создать виджет, см. в разделе Обзор виджетов приложений .

Компоненты виджета

Для создания виджета вам потребуются следующие основные компоненты:

Объект AppWidgetProviderInfo
Описывает метаданные виджета, такие как макет виджета, частота обновления и класс AppWidgetProvider . AppWidgetProviderInfo определяется в XML , как описано в этом документе.
Класс AppWidgetProvider
Определяет основные методы, которые позволяют программно взаимодействовать с виджетом. Через него вы получаете трансляции, когда виджет обновляется, включается, отключается или удаляется. Вы объявляете AppWidgetProvider в манифесте , а затем реализуете его, как описано в этом документе.
Посмотреть макет
Определяет исходный макет виджета. Макет определяется в XML , как описано в этом документе.

На рис. 2 показано, как эти компоненты вписываются в общий процесс обработки виджетов приложения.

Процесс обработки виджета приложения
Рисунок 2. Процесс обработки виджета приложения.

Если ваш виджет требует пользовательской настройки, реализуйте действие по настройке виджета приложения. Это действие позволяет пользователям изменять настройки виджета, например часовой пояс для виджета часов.

Мы также рекомендуем следующие улучшения: гибкие макеты виджетов , различные улучшения , расширенные виджеты , виджеты-коллекции и создание хоста виджетов .

Объявите XML AppWidgetProviderInfo.

Объект AppWidgetProviderInfo определяет основные качества виджета. Определите объект AppWidgetProviderInfo в файле ресурсов XML, используя один элемент <appwidget-provider> , и сохраните его в папке res/xml/ проекта.

Это показано в следующем примере:

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="40dp"
    android:minHeight="40dp"
    android:targetCellWidth="1"
    android:targetCellHeight="1"
    android:maxResizeWidth="250dp"
    android:maxResizeHeight="120dp"
    android:updatePeriodMillis="86400000"
    android:description="@string/example_appwidget_description"
    android:previewLayout="@layout/example_appwidget_preview"
    android:initialLayout="@layout/example_loading_appwidget"
    android:configure="com.example.android.ExampleAppWidgetConfigurationActivity"
    android:resizeMode="horizontal|vertical"
    android:widgetCategory="home_screen"
    android:widgetFeatures="reconfigurable|configuration_optional">
</appwidget-provider>

Атрибуты размера виджета

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

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

В следующей таблице описаны атрибуты <appwidget-provider> , относящиеся к размеру виджета:

Атрибуты и описание
targetCellWidth и targetCellHeight (Android 12), minWidth и minHeight
  • Начиная с Android 12, атрибуты targetCellWidth и targetCellHeight определяют размер виджета по умолчанию в виде ячеек сетки. Эти атрибуты игнорируются в Android 11 и более ранних версиях и могут быть проигнорированы, если главный экран не поддерживает макет на основе сетки.
  • Атрибуты minWidth и minHeight определяют размер виджета по умолчанию в dp. Если значения минимальной ширины или высоты виджета не соответствуют размерам ячеек, значения округляются до ближайшего размера ячейки.
Мы рекомендуем указать оба набора атрибутов targetCellWidth и targetCellHeight , а также minWidth и minHeight — чтобы ваше приложение могло вернуться к использованию minWidth и minHeight , если устройство пользователя не поддерживает targetCellWidth и targetCellHeight . Если они поддерживаются, атрибуты targetCellWidth и targetCellHeight имеют приоритет над атрибутами minWidth и minHeight .
minResizeWidth и minResizeHeight Укажите абсолютный минимальный размер виджета. Эти значения определяют размер, при котором виджет становится неразборчивым или непригоден для использования по другим причинам. Использование этих атрибутов позволяет пользователю изменить размер виджета до размера, меньшего, чем размер виджета по умолчанию. Атрибут minResizeWidth игнорируется, если он больше minWidth или если горизонтальное изменение размера не включено. См resizeMode . Аналогично, атрибут minResizeHeight игнорируется, если он больше minHeight или если вертикальное изменение размера не включено.
maxResizeWidth и maxResizeHeight Укажите рекомендуемый максимальный размер виджета. Если значения не кратны размерам ячейки сетки, они округляются до ближайшего размера ячейки. Атрибут maxResizeWidth игнорируется, если он меньше minWidth или если горизонтальное изменение размера не включено. См resizeMode . Аналогично, атрибут maxResizeHeight игнорируется, если он больше minHeight или если вертикальное изменение размера не включено. Представлено в Android 12.
resizeMode Определяет правила, по которым можно изменить размер виджета. Вы можете использовать этот атрибут, чтобы изменить размер виджетов главного экрана по горизонтали, вертикали или по обеим осям. Пользователи касаются и удерживают виджет, чтобы отобразить его маркеры изменения размера, затем перетаскивают горизонтальные или вертикальные маркеры, чтобы изменить его размер в сетке макета. Значения атрибута resizeMode включают horizontal , vertical и none . Чтобы объявить виджет как изменяемый по горизонтали и вертикали, используйте horizontal|vertical .

Пример

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

  • Ячейка сетки имеет ширину 30 dp и высоту 50 dp.
  • Предоставляется следующая спецификация атрибута:
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="80dp"
    android:minHeight="80dp"
    android:targetCellWidth="2"
    android:targetCellHeight="2"
    android:minResizeWidth="40dp"
    android:minResizeHeight="40dp"
    android:maxResizeWidth="120dp"
    android:maxResizeHeight="120dp"
    android:resizeMode="horizontal|vertical" />

Начиная с Android 12:

Используйте атрибуты targetCellWidth и targetCellHeight в качестве размера виджета по умолчанию.

По умолчанию размер виджета составляет 2x2. Размер виджета можно уменьшить до 2x1 или до 4x3.

Android 11 и более ранние версии:

Используйте атрибуты minWidth и minHeight для вычисления размера виджета по умолчанию.

Ширина по умолчанию = Math.ceil(80 / 30) = 3.

Высота по умолчанию = Math.ceil(80 / 50) = 2.

По умолчанию размер виджета составляет 3x2. Размер виджета можно изменить до 2х1 или до полноэкранного.

Дополнительные атрибуты виджета

В следующей таблице описаны атрибуты <appwidget-provider> , относящиеся к качествам, отличным от размера виджета.

Атрибуты и описание
updatePeriodMillis Определяет, как часто платформа виджетов запрашивает обновление у AppWidgetProvider , вызывая метод обратного вызова onUpdate() . При этом значении фактическое обновление не гарантируется точно в срок, и мы рекомендуем обновлять как можно реже — не чаще одного раза в час — для экономии заряда батареи. Полный список рекомендаций по выбору подходящего периода обновления см. в разделе «Оптимизация обновления содержимого виджета» .
initialLayout Указывает на ресурс макета, определяющий макет виджета.
configure Определяет действие, которое запускается, когда пользователь добавляет виджет, позволяя ему настраивать свойства виджета. См. раздел Разрешение пользователям настраивать виджеты . Начиная с Android 12, ваше приложение может пропускать начальную настройку. Подробности см. в разделе Использование конфигурации виджета по умолчанию .
description Указывает описание средства выбора виджета, которое будет отображаться для вашего виджета. Представлено в Android 12.
previewLayout (Android 12) и previewImage (Android 11 и более ранние версии)
  • Начиная с Android 12, previewLayout указывает масштабируемый предварительный просмотр, который вы предоставляете в виде макета XML с размером виджета по умолчанию. В идеале XML макета, указанный в качестве этого атрибута, является тем же XML макета, что и фактический виджет, с реалистичными значениями по умолчанию.
  • В Android 11 или более ранней версии previewImage указывает предварительный просмотр того, как выглядит виджет после его настройки, который пользователь видит при выборе виджета приложения. Если он не указан, вместо этого пользователь увидит значок запуска вашего приложения. Это поле соответствует атрибуту android:previewImage в элементе <receiver> в файле AndroidManifest.xml .
Примечание. Мы рекомендуем указать previewImage previewLayout , чтобы ваше приложение могло вернуться к previewImage если устройство пользователя не previewLayout . Дополнительные сведения см. в разделе Обратная совместимость с масштабируемыми предпросмотрами виджетов .
autoAdvanceViewId Указывает идентификатор представления подпредставления виджета, которое автоматически расширяется хостом виджета.
widgetCategory Объявляет, может ли ваш виджет отображаться на главном экране ( home_screen ), экране блокировки ( keyguard ) или на обоих. Для Android 5.0 и выше допустим только home_screen .
widgetFeatures Объявляет функции, поддерживаемые виджетом. Например, если вы хотите, чтобы ваш виджет использовал конфигурацию по умолчанию, когда пользователь добавляет его, укажите флаги configuration_optional и reconfigurable . Это обходит запуск действия по настройке после того, как пользователь добавляет виджет. Пользователь по-прежнему может впоследствии перенастроить виджет .

Используйте класс AppWidgetProvider для обработки трансляций виджетов.

Класс AppWidgetProvider обрабатывает широковещательные рассылки виджетов и обновляет виджет в ответ на события жизненного цикла виджета. В следующих разделах описано, как объявить AppWidgetProvider в манифесте и затем реализовать его.

Объявить виджет в манифесте

Сначала объявите класс AppWidgetProvider в файле AndroidManifest.xml вашего приложения, как показано в следующем примере:

<receiver android:name="ExampleAppWidgetProvider"
                 android:exported="false">
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
    </intent-filter>
    <meta-data android:name="android.appwidget.provider"
               android:resource="@xml/example_appwidget_info" />
</receiver>

Элементу <receiver> требуется атрибут android:name , который указывает AppWidgetProvider , используемый виджетом. Компонент нельзя экспортировать, если только отдельный процесс не требует трансляции в ваш AppWidgetProvider , что обычно не происходит.

Элемент <intent-filter> должен включать элемент <action> с атрибутом android:name . Этот атрибут указывает, что AppWidgetProvider принимает широковещательную рассылку ACTION_APPWIDGET_UPDATE . Это единственная трансляция, которую вы должны явно заявить. AppWidgetManager автоматически отправляет все остальные трансляции виджетов в AppWidgetProvider по мере необходимости.

Элемент <meta-data> указывает ресурс AppWidgetProviderInfo и требует следующих атрибутов:

  • android:name : указывает имя метаданных. Используйте android.appwidget.provider , чтобы идентифицировать данные как дескриптор AppWidgetProviderInfo .
  • android:resource : указывает расположение ресурса AppWidgetProviderInfo .

Реализуйте класс AppWidgetProvider.

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

onUpdate()
Это вызывается для обновления виджета с интервалами, определяемыми атрибутом updatePeriodMillis в AppWidgetProviderInfo . Дополнительную информацию см. в таблице, описывающей дополнительные атрибуты виджета на этой странице.
Этот метод также вызывается, когда пользователь добавляет виджет, поэтому он выполняет необходимые настройки, такие как определение обработчиков событий для объектов View или запуск заданий по загрузке данных для отображения в виджете. Однако если вы объявляете действие настройки без флага configuration_optional , этот метод не вызывается, когда пользователь добавляет виджет, но он вызывается для последующих обновлений. Ответственность за выполнение первого обновления после завершения настройки лежит на деятельности по настройке. Дополнительные сведения см. в разделе Разрешить пользователям настраивать виджеты приложений .
Самый важный обратный вызов — onUpdate() . Дополнительную информацию см. в разделе «Обработка событий с помощью класса onUpdate() на этой странице.
onAppWidgetOptionsChanged()

Это вызывается при первом размещении виджета и при каждом изменении размера виджета. Используйте этот обратный вызов, чтобы показать или скрыть контент в зависимости от диапазонов размеров виджета. Получите диапазоны размеров (а, начиная с Android 12, список возможных размеров, которые может принимать экземпляр виджета), вызвав getAppWidgetOptions() , который возвращает Bundle , который включает в себя следующее:

  • OPTION_APPWIDGET_MIN_WIDTH : содержит нижнюю границу ширины экземпляра виджета в единицах dp.
  • OPTION_APPWIDGET_MIN_HEIGHT : содержит нижнюю границу высоты экземпляра виджета в единицах dp.
  • OPTION_APPWIDGET_MAX_WIDTH : содержит верхнюю границу ширины экземпляра виджета в единицах dp.
  • OPTION_APPWIDGET_MAX_HEIGHT : содержит верхнюю границу высоты экземпляра виджета в единицах dp.
  • OPTION_APPWIDGET_SIZES : содержит список возможных размеров ( List<SizeF> ) в единицах dp, которые может принимать экземпляр виджета. Представлено в Android 12.
onDeleted(Context, int[])

Это вызывается каждый раз, когда виджет удаляется с хоста виджетов.

onEnabled(Context)

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

onDisabled(Context)

Это вызывается, когда последний экземпляр вашего виджета удаляется с хоста виджетов. Здесь вы очищаете любую работу, выполненную в onEnabled(Context) , например удаление временной базы данных.

onReceive(Context, Intent)

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

Вы должны объявить реализацию класса AppWidgetProvider как получатель широковещательной рассылки, используя элемент <receiver> в AndroidManifest . Дополнительные сведения см. в разделе «Объявление виджета в манифесте» на этой странице.

Обработка событий с помощью класса onUpdate().

Самый важный обратный вызов AppWidgetProvider — это onUpdate() , поскольку он вызывается при добавлении каждого виджета на хост, если только вы не используете действие настройки без флага configuration_optional . Если ваш виджет принимает какие-либо события взаимодействия с пользователем, зарегистрируйте обработчики событий в этом обратном вызове. Если ваш виджет не создает временные файлы или базы данных и не выполняет другую работу, требующую очистки, то onUpdate() может быть единственным методом обратного вызова, который вам нужно определить.

Например, если вам нужен виджет с кнопкой, которая запускает действие при нажатии, вы можете использовать следующую реализацию AppWidgetProvider :

Котлин

class ExampleAppWidgetProvider : AppWidgetProvider() {

    override fun onUpdate(
            context: Context,
            appWidgetManager: AppWidgetManager,
            appWidgetIds: IntArray
    ) {
        // Perform this loop procedure for each widget that belongs to this
        // provider.
        appWidgetIds.forEach { appWidgetId ->
            // Create an Intent to launch ExampleActivity.
            val pendingIntent: PendingIntent = PendingIntent.getActivity(
                    /* context = */ context,
                    /* requestCode = */  0,
                    /* intent = */ Intent(context, ExampleActivity::class.java),
                    /* flags = */ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
            )

            // Get the layout for the widget and attach an onClick listener to
            // the button.
            val views: RemoteViews = RemoteViews(
                    context.packageName,
                    R.layout.appwidget_provider_layout
            ).apply {
                setOnClickPendingIntent(R.id.button, pendingIntent)
            }

            // Tell the AppWidgetManager to perform an update on the current
            // widget.
            appWidgetManager.updateAppWidget(appWidgetId, views)
        }
    }
}

Ява

public class ExampleAppWidgetProvider extends AppWidgetProvider {

    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        // Perform this loop procedure for each widget that belongs to this
        // provider.
        for (int i=0; i < appWidgetIds.length; i++) {
            int appWidgetId = appWidgetIds[i];
            // Create an Intent to launch ExampleActivity
            Intent intent = new Intent(context, ExampleActivity.class);
            PendingIntent pendingIntent = PendingIntent.getActivity(
                /* context = */ context,
                /* requestCode = */ 0,
                /* intent = */ intent,
                /* flags = */ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
            );

            // Get the layout for the widget and attach an onClick listener to
            // the button.
            RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.example_appwidget_layout);
            views.setOnClickPendingIntent(R.id.button, pendingIntent);

            // Tell the AppWidgetManager to perform an update on the current app
            // widget.
            appWidgetManager.updateAppWidget(appWidgetId, views);
        }
    }
}

Этот AppWidgetProvider определяет только метод onUpdate() , используя его для создания PendingIntent , который запускает Activity и прикрепляет его к кнопке виджета с помощью setOnClickPendingIntent(int, PendingIntent) . Он включает в себя цикл, который перебирает каждую запись в appWidgetIds , который представляет собой массив идентификаторов, идентифицирующих каждый виджет, созданный этим поставщиком. Если пользователь создает более одного экземпляра виджета, все они обновляются одновременно. Однако для всех экземпляров виджета управляется только одно расписание updatePeriodMillis . Например, если расписание обновления определено каждые два часа, а второй экземпляр виджета добавляется через час после первого, то они оба обновляются в период, определенный первым и вторым периодом обновления. игнорируется. Они оба обновляются каждые два часа, а не каждый час.

Дополнительные сведения см. в примере класса ExampleAppWidgetProvider.java .

Получение намерений трансляции виджета

AppWidgetProvider — это удобный класс. Если вы хотите получать трансляции виджета напрямую, вы можете реализовать свой собственный BroadcastReceiver или переопределить обратный вызов onReceive(Context,Intent) . Намерения, о которых вам нужно заботиться, следующие:

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

Вы должны определить первоначальный макет вашего виджета в XML и сохранить его в каталоге res/layout/ проекта. Подробную информацию см. в Руководстве по проектированию .

Создать макет виджета несложно, если вы знакомы с макетами . Однако имейте в виду, что макеты виджетов основаны на RemoteViews , который поддерживает не все виды виджетов макетов и представлений. Вы не можете использовать собственные представления или подклассы представлений, поддерживаемых RemoteViews .

RemoteViews также поддерживает ViewStub — невидимое View нулевого размера, которое можно использовать для ленивого раздувания ресурсов макета во время выполнения.

Поддержка поведения с сохранением состояния

В Android 12 добавлена ​​поддержка поведения с отслеживанием состояния с использованием следующих существующих компонентов:

Виджет по-прежнему не имеет состояния. Ваше приложение должно хранить состояние и регистрироваться для событий изменения состояния.

Пример виджета списка покупок, демонстрирующего поведение с сохранением состояния
Рисунок 3. Пример поведения с сохранением состояния.

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

Котлин

// Check the view.
remoteView.setCompoundButtonChecked(R.id.my_checkbox, true)

// Check a radio group.
remoteView.setRadioGroupChecked(R.id.my_radio_group, R.id.radio_button_2)

// Listen for check changes. The intent has an extra with the key
// EXTRA_CHECKED that specifies the current checked state of the view.
remoteView.setOnCheckedChangeResponse(
        R.id.my_checkbox,
        RemoteViews.RemoteResponse.fromPendingIntent(onCheckedChangePendingIntent)
)

Ява

// Check the view.
remoteView.setCompoundButtonChecked(R.id.my_checkbox, true);

// Check a radio group.
remoteView.setRadioGroupChecked(R.id.my_radio_group, R.id.radio_button_2);

// Listen for check changes. The intent has an extra with the key
// EXTRA_CHECKED that specifies the current checked state of the view.
remoteView.setOnCheckedChangeResponse(
    R.id.my_checkbox,
    RemoteViews.RemoteResponse.fromPendingIntent(onCheckedChangePendingIntent));

Предоставьте два макета: один предназначен для устройств под управлением Android 12 или более поздней версии в res/layout-v31 , а другой — для предыдущей версии Android 11 или более ранней версии в папке res/layout по умолчанию.

Реализуйте закругленные углы

В Android 12 представлены следующие системные параметры для установки радиусов закругленных углов вашего виджета:

  • system_app_widget_background_radius : угловой радиус фона виджета, который никогда не превышает 28 dp.

  • system_app_widget_inner_radius : угловой радиус любого представления внутри виджета. Это ровно на 8 dp меньше радиуса фона, чтобы обеспечить хорошее выравнивание при использовании заполнения 8 dp.

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

Виджет, показывающий радиусы фона виджета и виды внутри виджета.
Рисунок 4. Закругленные углы.

1 Угол виджета.

2 Угол обзора внутри виджета.

Важные соображения относительно закругленных углов

  • Сторонние программы запуска и производители устройств могут переопределить параметр system_app_widget_background_radius , чтобы он был меньше 28 dp. Параметр system_app_widget_inner_radius всегда на 8 dp меньше значения system_app_widget_background_radius .
  • Если ваш виджет не использует @android:id/background и не определяет фон, который обрезает его содержимое на основе контура (если для android:clipToOutline установлено значение true , средство запуска автоматически идентифицирует фон и обрезает виджет, используя прямоугольник с закругленными углами. до 16 дп. См. раздел Убедитесь, что ваш виджет совместим с Android 12 .

Для совместимости виджета с предыдущими версиями Android мы рекомендуем определить пользовательские атрибуты и использовать пользовательскую тему для их переопределения для Android 12, как показано в следующих примерах XML-файлов:

/values/attrs.xml

<resources>
  <attr name="backgroundRadius" format="dimension" />
</resources>

/values/styles.xml

<resources>
  <style name="MyWidgetTheme">
    <item name="backgroundRadius">@dimen/my_background_radius_dimen</item>
  </style>
</resources>

/values-31/styles.xml

<resources>
  <style name="MyWidgetTheme" parent="@android:style/Theme.DeviceDefault.DayNight">
    <item name="backgroundRadius">@android:dimen/system_app_widget_background_radius</item>
  </style>
</resources>

/drawable/my_widget_background.xml

<shape xmlns:android="http://schemas.android.com/apk/res/android"
  android:shape="rectangle">
  <corners android:radius="?attr/backgroundRadius" />
  ...
</shape>

/layout/my_widget_layout.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  ...
  android:background="@drawable/my_widget_background" />