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<Int>() }
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
==============
