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