Создайте пользовательский интерфейс с помощью Glance

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

Используйте Box , Column и Row

Glance имеет три основных компонуемых макета:

  • Box : Размещает элементы друг поверх друга. Преобразуется в RelativeLayout .

  • Column : Размещает элементы друг за другом по вертикальной оси. Преобразуется в LinearLayout с вертикальной ориентацией.

  • Row : Размещает элементы друг за другом по горизонтальной оси. Преобразуется в LinearLayout с горизонтальной ориентацией.

Glance поддерживает объекты Scaffold . Размещайте ваши составные элементы Column , Row и Box внутри заданного объекта Scaffold .

Макет в виде столбцов, строк и блоков.
Рисунок 1. Примеры макетов с использованием столбцов, строк и блоков.

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

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

Row(modifier = GlanceModifier.fillMaxWidth().padding(16.dp)) {
    val modifier = GlanceModifier.defaultWeight()
    Text("first", modifier)
    Text("second", modifier)
    Text("third", modifier)
}

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

Используйте прокручиваемые макеты.

Ещё один способ обеспечить адаптивность контента — сделать его прокручиваемым. Это возможно с помощью компонента LazyColumn . Этот компонент позволяет определить набор элементов, которые будут отображаться внутри прокручиваемого контейнера в виджете приложения.

Следующие фрагменты кода демонстрируют различные способы определения элементов внутри LazyColumn .

Вы можете указать количество товаров:

// Remember to import Glance Composables
// import androidx.glance.appwidget.layout.LazyColumn

LazyColumn {
    items(10) { index: Int ->
        Text(
            text = "Item $index",
            modifier = GlanceModifier.fillMaxWidth()
        )
    }
}

Предоставьте отдельные предметы:

LazyColumn {
    item {
        Text("First Item")
    }
    item {
        Text("Second Item")
    }
}

Предоставьте список или массив элементов:

LazyColumn {
    items(peopleNameList) { name ->
        Text(name)
    }
}

Вы также можете использовать комбинацию приведенных выше примеров:

LazyColumn {
    item {
        Text("Names:")
    }
    items(peopleNameList) { name ->
        Text(name)
    }

    // or in case you need the index:
    itemsIndexed(peopleNameList) { index, person ->
        Text("$person at index $index")
    }
}

Обратите внимание, что в предыдущем фрагменте кода не указан itemId . Указание itemId помогает повысить производительность и сохранить положение прокрутки при обновлениях списков и appWidget , начиная с Android 12 (например, при добавлении или удалении элементов из списка). В следующем примере показано, как указать itemId :

items(items = peopleList, itemId = { person -> person.id.hashCode().toLong() }) { person ->
    Text(person.name)
}

Прокрутка с щелчком

Анимация «привязка к верхней части контейнера» позволяет прокручиваемому содержимому мгновенно перемещаться в верхнюю часть контейнера виджета.

Видео 1. Слева показан элемент списка, который не фиксируется на месте при прокрутке, справа — фиксируется.


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

  • Обновите зависимость Glance до версии 1.3.0-alpha02 или выше.
  • Установите значение compileSdk на 37 или выше, поскольку функция прокрутки с привязкой к кадру поддерживается на устройствах под управлением Android 17 и выше.
  • Настройте LazyColumn с помощью VerticalScrollMode . Если устройство поддерживает привязку к прокрутке, используйте SnapScrollMatchHeight . В противном случае используйте Normal .

Если вы используете прокрутку изображений с привязкой к фону, см. шаблон Canonical Layout для изображений, занимающих всю ширину страницы .

@Composable
fun SnapScrollLayout() {
    val height = LocalSize.current.height
    val items = listOf(
        ColorItem(Color.Red, "Red"),
        ColorItem(Color.Yellow, "Yellow"),
        ColorItem(Color.Blue, "Blue")
    )

    val scrollMode = if (Build.VERSION.SDK_INT >= 37) {
        VerticalScrollMode.SnapScrollMatchHeight(height)
    } else {
        VerticalScrollMode.Normal
    }

    LazyColumn(
        verticalScrollMode = scrollMode
    ) {
        items(items) { item ->
            ColorCard(item, height)
        }
    }
}

@Composable
private fun ColorCard(item: ColorItem, height: Dp) {
    Box(
        modifier = GlanceModifier
            .background(item.color)
            .fillMaxWidth()
            .height(height),
        contentAlignment = Alignment.Center
    ) {
        Text(
            text = item.name,
            modifier = GlanceModifier.background(Color.White)
        )
    }
}

Определите режим SizeMode

Размеры AppWidget могут различаться в зависимости от устройства, выбора пользователя или лаунчера, поэтому важно обеспечить гибкую компоновку, как описано на странице «Обеспечение гибкой компоновки виджетов» . Glance упрощает это с помощью определения SizeMode и значения LocalSize . В следующих разделах описаны три режима.

SizeMode.Single

SizeMode.Single — это режим по умолчанию. Он указывает, что предоставляется только один тип контента; то есть, даже если доступный размер AppWidget изменяется, размер контента не изменяется.

class MyAppWidget : GlanceAppWidget() {

    override val sizeMode = SizeMode.Single

    override suspend fun provideGlance(context: Context, id: GlanceId) {
        // ...

        provideContent {
            MyContent()
        }
    }

    @Composable
    private fun MyContent() {
        // Size will be the minimum size or resizable
        // size defined in the App Widget metadata
        val size = LocalSize.current
        // ...
    }
}

При использовании этого режима убедитесь в следующем:

  • Минимальный и максимальный размеры метаданных определены корректно в зависимости от размера контента.
  • Содержание достаточно гибкое в пределах ожидаемого диапазона размеров.

В целом, этот режим следует использовать в следующих случаях:

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

SizeMode.Responsive

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

Например, в нашем целевом AppWidget вы можете задать три размера и его содержимое:

class MyAppWidget : GlanceAppWidget() {

    companion object {
        private val SMALL_SQUARE = DpSize(100.dp, 100.dp)
        private val HORIZONTAL_RECTANGLE = DpSize(250.dp, 100.dp)
        private val BIG_SQUARE = DpSize(250.dp, 250.dp)
    }

    override val sizeMode = SizeMode.Responsive(
        setOf(
            SMALL_SQUARE,
            HORIZONTAL_RECTANGLE,
            BIG_SQUARE
        )
    )

    override suspend fun provideGlance(context: Context, id: GlanceId) {
        // ...

        provideContent {
            MyContent()
        }
    }

    @Composable
    private fun MyContent() {
        // Size will be one of the sizes defined above.
        val size = LocalSize.current
        Column {
            if (size.height >= BIG_SQUARE.height) {
                Text(text = "Where to?", modifier = GlanceModifier.padding(12.dp))
            }
            Row(horizontalAlignment = Alignment.CenterHorizontally) {
                Button()
                Button()
                if (size.width >= HORIZONTAL_RECTANGLE.width) {
                    Button("School")
                }
            }
            if (size.height >= BIG_SQUARE.height) {
                Text(text = "provided by X")
            }
        }
    }
}

В предыдущем примере метод provideContent вызывается трижды и сопоставляется с заданным размером.

  • При первом вызове размер составляет 100x100 . Содержимое не включает дополнительную кнопку, а также верхний и нижний текст.
  • Во втором вызове размер составляет 250x100 . Содержимое включает дополнительную кнопку, но не верхний и нижний текст.
  • В третьем вызове размер составляет 250x250 . Содержимое включает дополнительную кнопку и оба текста.

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

В следующей таблице показано значение размера в зависимости от параметра SizeMode и доступного размера AppWidget :

Доступные размеры 105 x 110 203 x 112 72 x 72 203 x 150
SizeMode.Single 110 x 110 110 x 110 110 x 110 110 x 110
SizeMode.Exact 105 x 110 203 x 112 72 x 72 203 x 150
SizeMode.Responsive 80 x 100 80 x 100 80 x 100 150 x 120
* Точные значения приведены только для демонстрационных целей.

SizeMode.Exact

SizeMode.Exact эквивалентен предоставлению точных макетов , что означает запрос содержимого GlanceAppWidget каждый раз, когда изменяется доступный размер AppWidget (например, когда пользователь изменяет размер AppWidget на главном экране).

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

class MyAppWidget : GlanceAppWidget() {

    override val sizeMode = SizeMode.Exact

    override suspend fun provideGlance(context: Context, id: GlanceId) {
        // ...

        provideContent {
            MyContent()
        }
    }

    @Composable
    private fun MyContent() {
        // Size will be the size of the AppWidget
        val size = LocalSize.current
        Column {
            Text(text = "Where to?", modifier = GlanceModifier.padding(12.dp))
            Row(horizontalAlignment = Alignment.CenterHorizontally) {
                Button()
                Button()
                if (size.width > 250.dp) {
                    Button("School")
                }
            }
        }
    }
}

Этот режим обеспечивает большую гибкость, чем другие, но имеет несколько ограничений:

  • При каждом изменении размера AppWidget приходится полностью пересоздавать. Это может привести к проблемам с производительностью и скачкам в пользовательском интерфейсе при работе со сложным контентом.
  • Доступный размер может отличаться в зависимости от реализации лаунчера. Например, если лаунчер не предоставляет список размеров, используется минимально возможный размер.
  • На устройствах, выпущенных до Android 12, логика расчета размера может работать не во всех ситуациях.

В целом, этот режим следует использовать, если SizeMode.Responsive использовать невозможно (то есть, создание небольшого набора адаптивных макетов не представляется возможным).

Доступ к ресурсам

Используйте LocalContext.current для доступа к любому ресурсу Android, как показано в следующем примере:

LocalContext.current.getString(R.string.glance_title)

Мы рекомендуем указывать идентификаторы ресурсов напрямую, чтобы уменьшить размер итогового объекта RemoteViews и включить динамические ресурсы, такие как динамические цвета .

Композитные объекты и методы принимают ресурсы, используя «провайдер», например ImageProvider , или используя перегруженный метод, такой как GlanceModifier.background(R.color.blue) . Например:

Column(
    modifier = GlanceModifier.background(R.color.default_widget_background)
) { /**...*/ }

Image(
    provider = ImageProvider(R.drawable.ic_logo),
    contentDescription = "My image",
)

Обработчик текста

В Glance 1.1.0 добавлен API для установки стилей текста. Стили текста можно задать с помощью атрибутов fontSize , fontWeight или fontFamily класса TextStyle.

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

Text(
    style = TextStyle(
        fontWeight = FontWeight.Bold,
        fontSize = 18.sp,
        fontFamily = FontFamily.Monospace
    ),
    text = "Example Text"
)

Добавить составные кнопки

Составные кнопки были введены в Android 12. Glance поддерживает обратную совместимость для следующих типов составных кнопок:

Каждая из этих составных кнопок отображает кликабельный элемент, представляющий собой состояние «отмечено».

var isApplesChecked by remember { mutableStateOf(false) }
var isEnabledSwitched by remember { mutableStateOf(false) }
var isRadioChecked by remember { mutableIntStateOf(0) }

CheckBox(
    checked = isApplesChecked,
    onCheckedChange = { isApplesChecked = !isApplesChecked },
    text = "Apples"
)

Switch(
    checked = isEnabledSwitched,
    onCheckedChange = { isEnabledSwitched = !isEnabledSwitched },
    text = "Enabled"
)

RadioButton(
    checked = isRadioChecked == 1,
    onClick = { isRadioChecked = 1 },
    text = "Checked"
)

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

class MyAppWidget : GlanceAppWidget() {

    override suspend fun provideGlance(context: Context, id: GlanceId) {
        val myRepository = MyRepository.getInstance()

        provideContent {
            val scope = rememberCoroutineScope()

            val saveApple: (Boolean) -> Unit =
                { scope.launch { myRepository.saveApple(it) } }
            MyContent(saveApple)
        }
    }

    @Composable
    private fun MyContent(saveApple: (Boolean) -> Unit) {

        var isAppleChecked by remember { mutableStateOf(false) }

        Button(
            text = "Save",
            onClick = { saveApple(isAppleChecked) }
        )
    }
}

Также вы можете задать атрибут colors для CheckBox , Switch и RadioButton , чтобы настроить их цвета:

CheckBox(
    // ...
    colors = CheckboxDefaults.colors(
        checkedColor = ColorProvider(day = colorAccentDay, night = colorAccentNight),
        uncheckedColor = ColorProvider(day = Color.DarkGray, night = Color.LightGray)
    ),
    checked = isChecked,
    onCheckedChange = { isChecked = !isChecked }
)

Switch(
    // ...
    colors = SwitchDefaults.colors(
        checkedThumbColor = ColorProvider(day = Color.Red, night = Color.Cyan),
        uncheckedThumbColor = ColorProvider(day = Color.Green, night = Color.Magenta),
        checkedTrackColor = ColorProvider(day = Color.Blue, night = Color.Yellow),
        uncheckedTrackColor = ColorProvider(day = Color.Magenta, night = Color.Green)
    ),
    checked = isChecked,
    onCheckedChange = { isChecked = !isChecked },
    text = "Enabled"
)

RadioButton(
    // ...
    colors = RadioButtonDefaults.colors(
        checkedColor = ColorProvider(day = Color.Cyan, night = Color.Yellow),
        uncheckedColor = ColorProvider(day = Color.Red, night = Color.Blue)
    ),

)

Дополнительные компоненты

В Glance 1.1.0 включены дополнительные компоненты, описанные в следующей таблице:

Имя Изображение Ссылка на источник Дополнительные примечания
Заполненная кнопка alt_text Компонент
Кнопки контура alt_text Компонент
Кнопки-иконки alt_text Компонент Основной / Дополнительный / Только значок
Заголовочная строка alt_text Компонент
Строительные леса Элементы Scaffold и Title bar находятся в одном демонстрационном ролике.

Для получения более подробной информации о специфике проектирования см. примеры компонентов в этом наборе инструментов для проектирования на Figma.

Для получения дополнительной информации о канонических макетах посетите страницу «Канонические макеты виджетов» .