Создайте пользовательский интерфейс с помощью 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, key = { person -> person.id }) { person ->
    Text(person.name)
}

Определите 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 х 110 203 х 112 72 х 72 203 х 150
SizeMode.Single 110 х 110 110 х 110 110 х 110 110 х 110
SizeMode.Exact 105 х 110 203 х 112 72 х 72 203 х 150
SizeMode.Responsive 80 х 100 80 х 100 80 х 100 150 х 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 { mutableStateOf(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.