Jetpack Compose Material 3 предлагает богатый набор UI-компонентов. Но помимо Button, TextField и Card существует множество менее известных компонентов, которые помогут сэкономить время и обеспечить лучший пользовательский опыт. В этой статье рассмотрим некоторые из них и узнаем, когда и как их использовать. Примечание: примеры в этой статье должны работать во всех версиях Compose Material 3: продакшен-версии (1.3.2), бета (1.4.0-beta02) и альфа (1.5.0-alpha04), так как API не менялся. Реклама 1. TriStateCheckbox Обычный флажок (чекбокс) поддерживает два состояния: включено и выключено. TriStateCheckbox добавляет третье состояние: неопределенное. Оно используется в ситуации, когда что-то не полностью отмечено и не полностью снято. В документации по TriStateCheckbox есть изображение (см. ниже), которое показывает все три состояния. Источник: https://developer.android.com/images/reference/androidx/compose/material3/indeterminate-checkbox.png Для чего нужен этот компонент? В основном он предназначен для использования в качестве родительского флажка, который отображает состояние всех дочерних флажков: Когда все дочерние флажки сняты → родительский TriStateCheckbox находится в состоянии «выключено». Когда дочерние флажки отмечены не все (часть снята) → родительский TriStateCheckbox переходит в неопределенное состояние (ни отмечено, ни снято). Когда все дочерние флажки отмечены → родительский TriStateCheckbox находится в состоянии «включено». Пример трех состояний TriStateCheckbox Вот как мы можем реализовать описанный выше выбор. Еще одна важная особенность заключается в том, что щелчок по TriStateCheckbox изменяет состояние всех дочерних флажков. @Composable private fun TriStateCheckboxSample() { var childStates by remember { mutableStateOf(List(5) { false }) } // Определение родительского состояния val parentState = when { childStates.all { it } -> ToggleableState.On childStates.none { it } -> ToggleableState.Off else -> ToggleableState.Indeterminate } Column(Modifier.padding(16.dp)) { // Родительский чекбокс Row(verticalAlignment = Alignment.CenterVertically) { TriStateCheckbox( state = parentState, onClick = { val newState = parentState != ToggleableState.On childStates = List(childStates.size) { newState } } ) Text("Options") } Spacer(Modifier.height(8.dp)) // Дочерние чекбоксы childStates.forEachIndexed { index, checked -> Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = 16.dp)) { Checkbox( checked = checked, onCheckedChange = { newValue -> childStates = childStates.toMutableList().apply { this[index] = newValue } } ) Text("Option ${index + 1}") } } } } Вот как это выглядит в действии: Видео, демонстрирующее работу TriStateCheckbox Забавный факт: composable-функция Checkbox является оберткой для composable TriStateCheckbox, при этом игнорируется третье, неопределенное состояние. 2. SegmentedButton SegmentedButton — это компонент, который позволяет выбрать от двух до пяти вариантов. Она может содержать значки, текст или и то, и другое. Существует два варианта SegmentedButton: с одиночным или множественным выбором. Они реализуются как разные компоненты. SegmentedButton с одиночным выбором SegmentedButton с одиночным выбором реализуется в виде composable SingleChoiceSegmentedButtonRow, к которой можно добавить несколько SegmentedButton. Может использоваться значок по умолчанию (галочка), также доступен вариант предоставления собственного значка. Пример SegmentedButton с одиночным выбором Пример реализации двух SegmentedButton, показанных выше: var selectedIndex by remember { mutableIntStateOf(0) } SingleChoiceSegmentedButtonRow( modifier = Modifier.fillMaxWidth() ) { (0..2).forEach { index -> SegmentedButton( selected = selectedIndex == index, onClick = { selectedIndex = index }, shape = SegmentedButtonDefaults.itemShape(index, 3), ) { Text("Option ${index + 1}") } } } Text( text = "Selected Option: ${selectedIndex + 1}", style = MaterialTheme.typography.bodySmall ) Spacer(Modifier.height(16.dp)) var selectedIndex1 by remember { mutableIntStateOf(0) } SingleChoiceSegmentedButtonRow( modifier = Modifier.fillMaxWidth() ) { (0..4).forEach { index -> SegmentedButton( selected = selectedIndex1 == index, onClick = { selectedIndex1 = index }, shape = SegmentedButtonDefaults.itemShape(index, 5), icon = { SegmentedButtonDefaults.Icon(selectedIndex1 == index, activeContent = { Icon(Icons.Default.Favorite, null) }) } ) { Text("${index + 1}") } } } Text( text = "Selected Option: ${selectedIndex1 + 1}", style = MaterialTheme.typography.bodySmall ) Вот как это выглядит в действии: Пример использования SegmentedButton с одиночным выбором SegmentedButton с множественным выбором SegmentedButton с множественным выбором реализуется в виде composable MultiChoiceSegmentedButtonRow, к которой можно добавить несколько SegmentedButton. Реализация аналогична SingleChoiceSegmentedButtonRow, с некоторыми незначительными отличиями в API, позволяющими одновременно отмечать несколько кнопок. Пример SegmentedButton с множественным выбором Вот пример реализации двух SegmentedButton, показанных выше. val selectedOptions = remember { mutableStateListOf() } MultiChoiceSegmentedButtonRow( modifier = Modifier.fillMaxWidth() ) { (0..4).forEach { index -> SegmentedButton( checked = index in selectedOptions, onCheckedChange = { if (index in selectedOptions) selectedOptions.remove(index) else selectedOptions.add( index ) }, shape = SegmentedButtonDefaults.itemShape(index, 5), ) { Text("${index + 1}") } } } Text( text = "Selected Options: ${selectedOptions.map { it + 1 }.joinToString()}", style = MaterialTheme.typography.bodySmall ) Вот как это выглядит в действии. Пример использования SegmentedButton с множественным выбором 3. RangeSlider RangeSlider (диапазонный слайдер) основан на концепции обычного слайдера, но с ключевым отличием: он позволяет пользователю выбрать два значения. Эти два значения образуют диапазон, где одно значение представляет минимум, а другое — максимум. Пример, где можно использовать RangeSlider — фильтрация по цене, позволяющая пользователю выбрать ценовой диапазон и показывающая результаты, которые ему соответствуют. Пример RangeSlider Приведем краткий пример использования RangeSlider. API похож на API обычного Slider. Нужно передать выбранный диапазон значений, допустимый диапазон, определяющий минимальное и максимальное значение, и количество шагов. В этом примере у нас диапазон от 1 до 100 с 9 шагами (плюс один, который всегда присутствует); это означает, что каждый шаг представляет значение 10. @OptIn(ExperimentalMaterial3Api::class) @Composable private fun RangeSliderExample() { var selectedValue by remember { mutableStateOf(0f..100f) } Column { Text( text = "Selected range: ${selectedValue.start.toInt()} - ${selectedValue.endInclusive.toInt()}", style = MaterialTheme.typography.bodyLarge ) RangeSlider( value = selectedValue, onValueChange = { newRange -> selectedValue = newRange }, valueRange = 1f..100f, steps = 9, modifier = Modifier.fillMaxWidth() ) } } Можно перетаскивать каждый бегунок, чтобы изменить выбранный диапазон. Два бегунка не могут пересекаться друг с другом. Пример использования RangeSlider 4. Badge Бейдж (Badge) представляет собой уведомление и предназначен для привлечения внимания к элементу, информируя пользователя о наличии ожидающих запросов или действий. Он также может отображать определенное количество ожидающих запросов или короткий текст. Обычно используется в нижней панели навигации на одном из навигационных элементов. Источник: https://developer.android.com/images/reference/androidx/compose/material3/badge.png BadgedBox — это компонент, оборачивающий элемент, к которому мы хотим прикрепить бейдж. Он принимает две composable-функции в качестве входных аргументов: одну для содержимого и одну для бейджа. Затем он закрепляет бейдж в правом верхнем углу содержимого. Badge также можно настраивать, применяя различные фоны и цвета текста. @Composable private fun BadgeExample() { Row( horizontalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier ) { BadgedBox( badge = { Badge { Text("5") } } ) { Icon( imageVector = Icons.Default.Email, contentDescription = "Messages", modifier = Modifier.padding(8.dp) ) } BadgedBox( badge = { Badge( containerColor = Color.Gray, contentColor = Color.Yellow ) { Text(500.toString()) } } ) { Text("Inbox", modifier = Modifier.padding(8.dp)) } } // Пример навигационной панели NavigationBar { NavigationBarItem( icon = { Icon(Icons.Filled.Home, contentDescription = "Home") }, selected = true, onClick = {} ) NavigationBarItem( icon = { BadgedBox( badge = { Badge() } ) { Icon(Icons.AutoMirrored.Filled.List, contentDescription = "List") } }, selected = false, onClick = {} ) NavigationBarItem( icon = { BadgedBox( badge = { Badge() { Text(3.toString()) } } ) { Icon(Icons.Filled.Person, contentDescription = "Profile") } }, selected = false, onClick = {} ) } } Приведенный пример показывает бейдж над значком, кастомизированный бейдж над текстом и бейджи внутри панели навигации. Примеры использования Badge и BadgedBox 5. Tooltip Более подробную информацию о компоненте Tooltip вы можете найти в отдельной статье. Заключение Мы подробно рассмотрели некоторые из менее известных компонентов Compose Material 3. Теперь можете уверенно добавлять эти незаслуженно обойденные вниманием компоненты в свой набор инструментов Compose, чтобы ваши приложения стали более проработанными и интерактивными. Читайте также: Оптимизация кэширования в TrendNow: объединение OkHttp Cache и базы данных Room. Часть 7 Роль Fragments в современной разработке приложений для Android Автоматизация скриншот-тестирования предварительных просмотров Compose с использованием отражения Читайте нас в Telegram, VK и Дзен Перевод статьи Domen Lanišnik: Exploring 5 Lesser-Known Compose Components ==============