Библиотека Material Design 3 в Compose предоставляет готовый API для отображения всплывающих подсказок в приложениях. Мы рассмотрим их использование в стабильной версии 1.3.2 и в новейшей альфа-версии 1.4.0.
Примечание: все упомянутые composable в настоящее время находятся на этапе экспериментирования, поэтому их API может измениться в будущем.

Предварительные требования
Прежде чем использовать API всплывающих подсказок в Compose, необходимо добавить библиотеку Compose Material 3 в проект.
Если мы применяем Compose BOM, то у нас будет использоваться последняя стабильная версия (1.3.2 на момент написания статьи). Если будем использовать последнюю альфа-версию, необходимо вручную указать номер версии (1.4.0-alpha13).
API для всплывающих подсказок немного изменился в альфа-версиях 1.4.0, были добавлены некоторые новые функции. Примеры в этой статье будут основаны на API версии 1.3.2. Однако в конце мы перечислим все различия в 1.4.0, чтобы вы могли работать с обеими версиями.
Типы всплывающих подсказок
Есть два основных типа поддерживаемых всплывающих подсказок: простые и расширенные.
Простые подсказки обычно отображают обычный текст и используются для информирования пользователя о конкретных действиях или элементах интерфейса.

Расширенные подсказки больше по размеру и могут отображать больше содержимого. У них есть отдельные заголовок и описание, а также возможность добавить пользовательское действие, которое можно применить для отображения кнопок, запускающих определенную логику.

Оба типа подсказок могут отображаться либо автоматически при долгом нажатии пользователем на компонент, либо вручную из кода. Они скрываются либо автоматически через 1500 миллисекунд, либо после касания пользователем любой части экрана, либо вручную из кода.
У подсказок также может быть опциональный указатель — стрелка в нижней части подсказки, направленная на привязанный компонент.
API для всплывающих подсказок
Основной composable-функцией является TooltipBox, которая выступает в качестве обертки для composable, над которым нужно отображать подсказку. Она содержит логику для вычисления позиции подсказки, чтобы разместить ее над целевым содержимым.
Функция принимает следующие аргументы:
positionProvider: PopupPositionProvider: используется для размещения подсказки относительно привязанного содержимого.tooltip: @Composable TooltipScope.() -> Unit: composable для наполнения содержимым подсказки.state: TooltipState: управляет состоянием видимости подсказки.modifier: Modifier: стандартный модификатор для composable.focusable: Boolean: определяет, является ли подсказка доступной для фокусировки, что влияет на ее доступность.enableUserInput: Boolean: определяет, будет лиTooltipBoxобрабатывать долгое нажатие и наведение курсора мыши для вызова подсказки через поставщик состояния.content: @Composable () -> Unit: composable для привязки подсказки. По сути, это содержимое, которое мы хотим показывать по умолчанию и над которым будет отображаться подсказка при срабатывании.
@Composable
@ExperimentalMaterial3Api
fun TooltipBox(
positionProvider: PopupPositionProvider,
tooltip: @Composable TooltipScope.() -> Unit,
state: TooltipState,
modifier: Modifier = Modifier,
focusable: Boolean = true,
enableUserInput: Boolean = true,
content: @Composable () -> Unit,
)
Простая подсказка
Допустим, у нас есть простая кнопка, над которой нужно отображать подсказку. Для этого мы оборачиваем наше содержимое в TooltipBox:
TooltipBox(
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
tooltip = { PlainTooltip { Text("This is a simple plain tooltip") } },
state = rememberTooltipState()
) {
Button(onClick = {}) {
Text(text = "Show Plain Tooltip on Long Press")
}
}
Используем TooltipDefaults.rememberPlainTooltipPositionProvider, чтобы поручить API обработку позиционирования. Сама подсказка создается с помощью composable PlainTooltip.
@Composable
@ExperimentalMaterial3Api
expect fun TooltipScope.PlainTooltip(
modifier: Modifier = Modifier,
caretSize: DpSize = DpSize.Unspecified,
shape: Shape = TooltipDefaults.plainTooltipContainerShape,
contentColor: Color = TooltipDefaults.plainTooltipContentColor,
containerColor: Color = TooltipDefaults.plainTooltipContainerColor,
tonalElevation: Dp = 0.dp,
shadowElevation: Dp = 0.dp,
content: @Composable () -> Unit
)
Можем настроить следующие элементы подсказки:
caretSize: DpSize: размер указателя (стрелки внизу) (скрыто по умолчанию).shape: Shape: форма, применяемая к контейнеру подсказки; может использоваться для настройки углов, например.contentColor: Color: цвет содержимого (например, текста) внутри подсказки.containerColor: Color: цвет контейнера/фона подсказки.
Передача rememberTooltipState в TooltipBox означает, что подсказка будет автоматически появляться при долгом нажатии на содержимое. Она также автоматически скроется через короткое время.

Расширенная всплывающая подсказка
Допустим, нам нужно отображать подсказку с заголовком и описанием при нажатии на кнопку. Подсказка должна оставаться на экране до тех пор, пока мы не нажмем кнопку закрытия на самой подсказке или в любом месте за ее пределами.

Снова используем TooltipBox с двумя изменениями для поддержки расширенной подсказки: передаем TooltipDefaults.rememberRichTooltipPositionProvider для positionProvider и используем composable RichTooltip для создания подсказки в другом стиле.
val tooltipState = rememberTooltipState(isPersistent = true)
val scope = rememberCoroutineScope()
TooltipBox(
positionProvider = TooltipDefaults.rememberRichTooltipPositionProvider(8.dp),
tooltip = {
RichTooltip(
caretSize = TooltipDefaults.caretSize,
title = { Text("Title of the tooltip") },
action = {
TextButton(
onClick = {
scope.launch {
tooltipState.dismiss()
}
}
) {
Text("Dismiss")
}
}
) {
Text("This is the main content of the rich tooltip")
}
},
state = tooltipState
) {
Button(onClick = {
scope.launch {
tooltipState.show()
}
}) {
Text(text = "Show Rich Tooltip on Click")
}
}
Поскольку мы хотим, чтобы подсказка появлялась только при нажатии на кнопку и оставалась видимой до тех пор, пока мы не нажмем в любом месте экрана, создаем и сохраняем экземпляр TooltipState, передавая параметр isPersistent = true.
Затем используем это сохраненное состояние для вызова tooltipState.show(), чтобы показать всплывающую подсказку, и tooltipState.dismiss(), чтобы скрыть ее. Обратите внимание: обе функции являются приостанавливающими. Их нужно вызывать в области видимости корутины.
@Composable
@ExperimentalMaterial3Api
expect fun TooltipScope.RichTooltip(
modifier: Modifier = Modifier,
title: (@Composable () -> Unit)? = null,
action: (@Composable () -> Unit)? = null,
caretSize: DpSize = DpSize.Unspecified,
shape: Shape = TooltipDefaults.richTooltipContainerShape,
colors: RichTooltipColors = TooltipDefaults.richTooltipColors(),
tonalElevation: Dp = ElevationTokens.Level0,
shadowElevation: Dp = RichTooltipTokens.ContainerElevation,
text: @Composable () -> Unit
)
API composable-функции RichTooltip похож на PlainTooltip, с той основной разницей, что в него можно передать три composable-функции:
text: @Composable () -> Unit: обязательное содержимое, которое представляет основное сообщение всплывающей подсказки.title: (@Composable () -> Unit)?: необязательное содержимое для заголовка подсказки, которое отображается над основным сообщением.action: (@Composable () -> Unit)?: необязательное содержимое для действия, отображаемого в подсказке; как правило, этоTextButton.
Настройка всплывающих подсказок
Оба типа всплывающих подсказок предлагают широкие возможности настройки. Рассмотрим, как можно использовать различные параметры для изменения внешнего вида подсказки.
Следующий фрагмент кода создает простую подсказку с серым фоном, округленными углами по вашему выбору, увеличенным указателем и пользовательским содержимым — желтыми иконкой и текстом.
TooltipBox(
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(16.dp),
tooltip = {
PlainTooltip(
caretSize = DpSize(32.dp, 16.dp),
contentColor = Color.Yellow,
containerColor = Color.DarkGray,
shadowElevation = 4.dp,
tonalElevation = 12.dp,
shape = RoundedCornerShape(16.dp)
) {
Column(
modifier = Modifier
.padding(8.dp)
.clip(
RoundedCornerShape(16.dp)
)
.background(Color.Gray)
.padding(8.dp)
) {
Icon(Icons.Default.AccountCircle, contentDescription = null)
Spacer(modifier = Modifier.height(4.dp))
Text("This is a simple customized plain tooltip")
Spacer(modifier = Modifier.height(4.dp))
Text("This is a second Text in the tooltip")
}
}
},
state = tooltipState
)
Ниже показан результат. Мы видим, что можем настроить как содержимое, так и стиль всплывающей подсказки.

Таким же образом можно настроить и расширенную подсказку. Вот фрагмент кода, который создает подсказку без округленных углов и с пользовательскими цветами.
RichTooltip(
caretSize = TooltipDefaults.caretSize,
colors = TooltipDefaults.richTooltipColors(
containerColor = Color.Black.copy(alpha = 0.9f),
titleContentColor = Color.Green,
contentColor = Color.White,
),
shape = RectangleShape,
title = {
Row {
Icon(Icons.Default.CheckCircle, contentDescription = null)
Spacer(modifier = Modifier.width(4.dp))
Text("Awesome!")
}
},
action = {
Row {
TextButton(
onClick = {
scope.launch {
tooltipState.dismiss()
}
}
) {
Text("Dismiss")
}
TextButton(
onClick = {
scope.launch {
tooltipState.dismiss()
}
}
) {
Text("Next")
}
}
}
) {
Text("You've successfully opened a rich tooltip! 🎉")
}
Вот результат:

Изменения API в последних альфа-версиях 1.4.0
Приведенный выше код основан на стабильной версии 1.3.2 библиотеки Material 3 Compose. В альфа-версиях 1.4.0 в API всплывающих подсказок были внесены некоторые неразрушающие изменения. К ним относятся:
rememberPlainTooltipPositionProviderпомечена как устаревшая в пользуrememberTooltipPositionProvider.rememberRichTooltipPositionProviderтакже помечена как устаревшая в пользуrememberTooltipPositionProvider.- У
TooltipBoxпоявилсяonDismissRequest: (() -> Unit)? = null— обратный вызов, который активируется, когда пользователь кликает за пределами подсказки. - У
PlainTooltipиRichTooltipпоявился новый параметрmaxWidth: Dp, который контролирует максимальную ширину подсказки. По умолчанию используется значение из проектной спецификации: 200 dp для простых подсказок и 320 dp для расширенных. - В
rememberTooltipStateдобавлен новый параметр конструктораinitialIsVisible: Boolean, который управляет начальной видимостью подсказки. Это полезно, если вам нужно, чтобы подсказка отображалась сразу при прорисовке экрана или для неинтерактивных элементов. Значение по умолчанию —false. Это означает, что подсказка скрыта до запроса.
val tooltipState = rememberTooltipState(isPersistent = true, initialIsVisible = false)
val scope = rememberCoroutineScope()
Column(horizontalAlignment = Alignment.CenterHorizontally) {
TooltipBox(
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(),
onDismissRequest = {
// Вызывается, когда подсказка скрывается
tooltipState.dismiss()
},
tooltip = { PlainTooltip(maxWidth = 100.dp) { Text("This is a simple plain tooltip") } },
state = tooltipState
) {
Button(onClick = {
scope.launch {
tooltipState.show()
}
}) {
Text(text = "Show Plain Tooltip on Click")
}
}
}
Вот пример, в котором используется новый обратный вызов для ручного скрытия подсказки. Он также устанавливает ширину подсказки в 100.dp, что приводит к переносу текста на несколько строк.

Полную реализацию вы можете найти в alpha-ветке этого репозитория.
Отображение нескольких всплывающих подсказок одновременно
Если попытаться отобразить несколько всплывающих подсказок одновременно, можно увидеть, что это невозможно. Это происходит потому, что в API всплывающих подсказок заложена глобальная логика, разрешающая одновременное отображение только одной подсказки. Функция rememberTooltipState() принимает параметр mutatorMutex: MutatorMutex, который используется для синхронизации подсказок. По умолчанию применяется BasicTooltipDefaults.GlobalMutatorMutex — статический экземпляр, который является общим для всех подсказок.
Если необходимо отобразить две (или более) подсказки одновременно, нужно передать экземпляр MutatorMutex в rememberTooltipState().
val tooltipState1 = rememberTooltipState(isPersistent = true)
val tooltipState2 = rememberTooltipState(
isPersistent = true,
mutatorMutex = MutatorMutex()
)
Затем мы можем, например, по нажатию кнопки вызвать tooltipState1.show() и tooltipState2.show(), чтобы отобразить обе подсказки одновременно.

Заключение
Библиотека Material Design 3 в Compose предоставляет встроенную поддержку отображения всплывающих подсказок. Ее API прост в использовании и позволяет гибко настраивать как внешний вид, так и поведение подсказок.
Мы рассмотрели, как использовать этот API и различные изменения в последней альфа-версии библиотеки. Теперь вы сможете применять всплывающие подсказки в своих приложениях без необходимости подключать сторонние библиотеки.
Полный пример кода для стабильной и альфа-версий библиотеки находится в этом репозитории.
Читайте также:
- 5 функций-расширений в арсенале каждого разработчика Jetpack Compose
- 5 малоизвестных компонентов Compose
- Предварительный просмотр Jetpack Compose-анимации по ключевым кадрам в Android Studio
Читайте нас в Telegram, VK и Дзен
Перевод статьи Domen Lanišnik: Tooltips in Compose Material 3





