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 есть изображение (см. ниже), которое показывает все три состояния.

Для чего нужен этот компонент? В основном он предназначен для использования в качестве родительского флажка, который отображает состояние всех дочерних флажков:

  • Когда все дочерние флажки сняты → родительский 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) представляет собой уведомление и предназначен для привлечения внимания к элементу, информируя пользователя о наличии ожидающих запросов или действий.

Он также может отображать определенное количество ожидающих запросов или короткий текст.

Обычно используется в нижней панели навигации на одном из навигационных элементов.

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, чтобы ваши приложения стали более проработанными и интерактивными. 

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Domen Lanišnik: Exploring 5 Lesser-Known Compose Components

Предыдущая статьяШаблоны проектирования Python: рекомендации и антипаттерны