Как создавать интерфейсы с помощью 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.

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

@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 логика расчета размера может работать не во всех ситуациях.

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

Подробную информацию о дизайне можно найти в наборе макетов на платформе Figma.

Подробнее о канонических макетах виджетов…