Как создать виджет приложения с помощью Glance

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

Объявите AppWidget в манифесте

После выполнения шагов по настройке объявите AppWidget и его метаданные в приложении.

  1. Расширьте приемник AppWidget из GlanceAppWidgetReceiver:

    class MyAppWidgetReceiver : GlanceAppWidgetReceiver() {
        override val glanceAppWidget: GlanceAppWidget = TODO("Create GlanceAppWidget")
    }

  2. Зарегистрируйте поставщика виджета приложения в файле AndroidManifest.xml и связанном файле метаданных:

        <receiver android:name=".glance.MyReceiver"
        android:exported="true">
        <intent-filter>
            <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
        </intent-filter>
        <meta-data
            android:name="android.appwidget.provider"
            android:resource="@xml/my_app_widget_info" />
    </receiver>
    

Добавьте метаданные AppWidgetProviderInfo

Затем следуйте инструкциям из руководства Создание виджета, чтобы создать и определить информацию о виджете приложения в файле @xml/my_app_widget_info.

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

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:initialLayout="@layout/glance_default_loading_layout">
</appwidget-provider>

Объявление XML-файла AppWidgetProviderInfo

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

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

По умолчанию размер виджета – 2 x 2. Размер виджета можно уменьшить до 2 x 1 или увеличить до 4 x 3.

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

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

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

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

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

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

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

Атрибуты и описание
updatePeriodMillis Определяет, как часто фреймворк виджета запрашивает обновление у GlanceAppWidgetReceiver, вызывая метод обратного вызова onUpdate(). Чтобы экономить заряд батареи, рекомендуем обновлять данные как можно реже, например не чаще одного раза в час. Подробную информацию можно найти в разделе Когда обновлять виджеты в статье об управлении состояниями Glance.
initialLayout Указывает на ресурс разметки, который определяет макет загрузки виджета до отрисовки композиций интерфейса Glance. Вы можете использовать стандартный макет загрузки, доступный в библиотеке: @layout/glance_default_loading_layout.
configure Определяет действие конфигурации, которое запускается, когда пользователь добавляет виджет. Ознакомьтесь с руководством Как разрешить пользователям настраивать виджеты приложений.
description Задает описание, которое будет показываться в окне выбора виджетов. Появился в Android 12.
previewLayout (Android 12) и previewImage (Android 11 и более ранние версии).
  • Начиная с Android 12 атрибут previewLayout указывает на масштабируемый предпросмотр, который вы предоставляете в виде макета XML, заданного для размера виджета по умолчанию. В идеале этот URL должен указывать на статический XML-файл сопоставления, соответствующий вашему макету.
  • В Android 11 и более ранних версиях атрибут previewImage указывает на статичное изображение, которое показывает, как выглядит виджет, и отображается в окне выбора виджетов.
Рекомендуем указать оба варианта, чтобы приложение корректно работало на старых платформах. Для более новых платформ (Android 15 и выше) вы можете определить динамически генерируемые предпросмотры в Kotlin, используя `GlanceAppWidget.providePreview`. См. руководство по генерируемым предпросмотрам.
autoAdvanceViewId Указывает идентификатор представления подпредставления виджета, которое автоматически продвигается хостом виджета.
widgetCategory Указывает, где может показываться виджет: на главном экране (home_screen), заблокированном экране (keyguard) или на обоих. Для Android 5.0 и более поздних версий допустимо только значение home_screen.
widgetFeatures Объявляет функции, поддерживаемые виджетом. Например, если настройка виджета необязательна, укажите и configuration_optional, и reconfigurable.

GlanceAppWidget: определение

  1. Создайте новый класс, который расширяет GlanceAppWidget и переопределяет метод provideGlance. Это метод, с помощью которого вы можете загрузить данные, необходимые для отрисовки виджета:

    class MyAppWidget : GlanceAppWidget() {
    
        override suspend fun provideGlance(context: Context, id: GlanceId) {
    
            // In this method, load data needed to render the AppWidget.
            // Use `withContext` to switch to another thread for long running
            // operations.
    
            provideContent {
                // create your AppWidget here
                Text("Hello World")
            }
        }
    }

  2. Создайте экземпляр в методе glanceAppWidget вашего класса GlanceAppWidgetReceiver:

    class MyAppWidgetReceiver : GlanceAppWidgetReceiver() {
    
        // Let MyAppWidgetReceiver know which GlanceAppWidget to use
        override val glanceAppWidget: GlanceAppWidget = MyAppWidget()
    }

Вы настроили AppWidget с помощью Glance.

Управление памятью для изображений в виджетах разных размеров

Если виджет показывает растровые изображения или URI изображений content://, общий объем сериализованной памяти изображений RemoteViews не должен превышать системный лимит (6 * width * height байт, контролируется с помощью IllegalArgumentException в Android 17 и более поздних версиях). Для этого:

  • Декодирование общих растровых изображений перед provideContent. Если в виджете используется SizeMode.Responsive или SizeMode.Exact, Glance запускает лямбда-выражение provideContent один раз для каждого сегмента DpSize. Декодирование Bitmap внутри provideContent выделяет отдельный экземпляр Bitmap для каждого сегмента, умножая использование памяти в RemoteViews.mBitmapCache. Декодируйте растровые изображения с пониженной частотой дискретизации (Bitmap.Config.RGB_565 для непрозрачных изображений) один раз в provideGlance перед provideContent, чтобы все сегменты использовали одну ссылку Bitmap.
  • Используйте подходящий импорт ImageProvider. Jetpack Glance предоставляет две разные перегрузки ImageProvider в двух пакетах:

    • Используйте import androidx.glance.ImageProvider, когда передаете ImageProvider(bitmap: Bitmap) в памяти или прорисовываемый ресурс ImageProvider(resId: Int).
    • При передаче URI используйте import androidx.glance.appwidget.ImageProvider. ImageProvider(uri: Uri). При показе кешированных файлов через URI FileProvider предоставьте разрешение Intent.FLAG_GRANT_READ_URI_PERMISSION каждому установленному пакету запуска Intent.CATEGORY_HOME, чтобы хост мог отрисовать изображение.

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

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

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

Объявите подкласс класса GlanceAppWidgetReceiver как широковещательный приемник в файле AndroidManifest.xml:

<receiver android:name="MyReceiver"
          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/my_app_widget_info" />
</receiver>

Элемент <receiver> требует атрибута android:name, который указывает класс получателя. Получатель должен принять действие трансляции ACTION_APPWIDGET_UPDATE внутри <intent-filter>.

Элемент <meta-data> должен иметь имя android.appwidget.provider, а атрибут android:resource должен указывать на ресурс метаданных XML AppWidgetProviderInfo (@xml/my_app_widget_info).

Реализуйте класс GlanceAppWidgetReceiver

В Glance вы расширяете GlanceAppWidgetReceiver, а не AppWidgetProvider напрямую. Для этого свяжите приемник с экземпляром GlanceAppWidget. Основные обратные вызовы, доступные в GlanceAppWidgetReceiver, работают следующим образом:

  • onUpdate() – автоматически переопределяется Glance для выполнения обновлений композиции. Если вы вручную переопределяете onUpdate, то должны вызвать super.onUpdate, чтобы Glance мог запустить потоки композиции.
  • onAppWidgetOptionsChanged() – вызывается при первом размещении виджета или изменении его размера. Glance считывает элементы пакета параметров под капотом, поэтому ваш макет плавно настраивается на основе размеров среды выполнения.
  • onDeleted(Context, IntArray) – вызывается, когда пользователь удаляет определенный экземпляр виджета.
  • onEnabled(Context) – срабатывает, когда успешно создан первый экземпляр виджета. Отлично подходит для глобальных переносов.
  • onDisabled(Context) – вызывается при удалении последнего активного экземпляра поставщика.
  • onReceive(Context, Intent) перехватывает все трансляции платформы перед определенными методами обратного вызова. Вы должны убедиться, что любая логика пользовательского приемника, которую вы пишете, вызывает super.onReceive(context, intent) и никогда не должна вызывать goAsync самостоятельно, поскольку Glance автоматически маршрутизирует работу асинхронно.

Получение широковещательных намерений виджета

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

Создать интерфейс

В следующем фрагменте кода показано, как создать интерфейс:

/* Import Glance Composables
 In the event there is a name clash with the Compose classes of the same name,
 you may rename the imports per https://kotlinlang.org/docs/packages.html#imports
 using the `as` keyword.

import androidx.glance.Button
import androidx.glance.layout.Column
import androidx.glance.layout.Row
import androidx.glance.text.Text
*/
class MyAppWidget : GlanceAppWidget() {

    override suspend fun provideGlance(context: Context, id: GlanceId) {
        // Load data needed to render the AppWidget.
        // Use `withContext` to switch to another thread for long running
        // operations.

        provideContent {
            // create your AppWidget here
            MyContent()
        }
    }

    @Composable
    private fun MyContent() {
        Column(
            modifier = GlanceModifier.fillMaxSize(),
            verticalAlignment = Alignment.Top,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text(text = "Where to?", modifier = GlanceModifier.padding(12.dp))
            Row(horizontalAlignment = Alignment.CenterHorizontally) {
                Button(
                    text = "Home",
                    onClick = actionStartActivity<MyActivity>()
                )
                Button(
                    text = "Work",
                    onClick = actionStartActivity<MyActivity>()
                )
            }
        }
    }
}

Приведенный выше пример кода выполняет следующие действия:

  • На верхнем уровне Column элементы размещаются вертикально один за другим.
  • Column расширяется, чтобы занять доступное пространство (с помощью GlanceModifier), выравнивает контент по верхнему краю (verticalAlignment) и центрирует его по горизонтали (horizontalAlignment).
  • Контент Column определяется с помощью лямбда-выражения. Порядок важен.
    • Первый элемент в Column – это компонент Text с отступом 12.dp.
    • Второй элемент – Row, в котором элементы размещаются горизонтально один за другим, с двумя Buttons, выровненными по центру по горизонтали (horizontalAlignment). Окончательный вид зависит от доступного пространства. Вот как это может выглядеть:
destination_widget
Рисунок 1. Пример интерфейса.

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

Как реализовать скругленные углы

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

  • system_app_widget_background_radius – радиус закругления углов контейнера виджета (не более 28 dp).
  • Внутренний радиус. Чтобы контент не обрезался, рассчитайте пропорциональный радиус для внутреннего контента на основе контура системного фона: systemRadiusValue - widgetPadding

В Glance можно динамически применять свойства размера радиуса закругления в композиции с помощью GlanceModifier.cornerRadius(android.R.dimen.system_app_widget_background_radius).

Чтобы обеспечить обратную совместимость на устройствах с Android 11 (уровень API 30) или более ранней версии, реализуйте резервные варианты для настраиваемых атрибутов и ресурсов темы:

  • /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>