Прошло время с тех пор, как я написал предыдущую статью о Jetpack Compose, в которой утверждал, что Jetpack Compose не имеет некоторых (на мой взгляд) базовых компонентов, и одним из них является всплывающая подсказка.

В то время не существовало встроенного composable для отображения всплывающих подсказок, и в сети циркулировали несколько альтернативных решений. Проблема с этими решениями заключалась в том, что с выходом новых версий Jetpack Compose они могли оказаться несовместимыми. Так что это было не идеальным, и сообщество оставалось надеяться, что в будущем добавится поддержка всплывающих подсказок.

С удовольствием сообщаю, что начиная с версии 1.1.0 Compose Material 3, у нас теперь есть встроенная поддержка всплывающих подсказок. 👏

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

Если вы просмотрите журнал изменений, увидите, как изменились публичные и внутренние API. Так что имейте в виду, что когда вы читаете эту статью, вещи могут продолжать меняться, так как все, связанное с всплывающими подсказками, все еще помечено аннотацией ExperimentalMaterial3Api::class.

❗️ Версия material 3, использованная в этой статье, — 1.2.1, выпущенная 6 марта 2024 года

Типы всплывающих подсказок

Теперь мы имеем поддержку двух различных типов всплывающих подсказок:

  1. Обычная всплывающая подсказка

  2. Ричард медиа подсказка

Простая подсказка

Вы можете использовать первый тип для добавления информации о значке кнопки, которая не ясна по умолчанию. Например, вы можете использовать обычную подсказку, чтобы указать пользователю, что значок кнопки представляет.

Для добавления подсказки в ваше приложение вы используете компоновку TooltipBox. Эта компоновка принимает несколько аргументов:

fun TooltipBox(
    positionProvider: PopupPositionProvider,
    tooltip: @Composable TooltipScope.() -> Unit,
    state: TooltipState,
    modifier: Modifier = Modifier,
    focusable: Boolean = true,
    enableUserInput: Boolean = true,
    content: @Composable () -> Unit,
)

Часть из них вам известны, если вы уже использовали компоновки раньше. Я выделю те, которые имеют особое использование:

  • positionProvider – Типа PopupPositionProvider, используется для вычисления позиции подсказки.

  • tooltip – В этом месте вы можете создать UI подсказки.

  • state – Здесь содержится состояние, связанное с конкретным экземпляром Tooltip. Она предоставляет методы, такие как показание/скрытие подсказки и при инициализации экземпляра, вы можете задать, должна ли подсказка быть постоянной (意味着她应该在用户执行点击操作在工具提示外之前一直显示在屏幕上).

  • содержимое – Это UI, которое tooltip будет отображать над или под содержимым.

Вот пример инициализации BasicTooltipBox с всеми необходимыми аргументами заполненными:

@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable
fun BasicTooltip() {
    val tooltipPosition = TooltipDefaults.rememberPlainTooltipPositionProvider()
    val tooltipState = rememberBasicTooltipState(isPersistent = false)

    BasicTooltipBox(positionProvider = tooltipPosition,
        tooltip =  { Text("Hello World") } ,
        state = tooltipState) {
        IconButton(onClick = { }) {
            Icon(imageVector = Icons.Filled.Favorite, 
                 contentDescription = "Your icon's description")
        }
    }
}

Jetpack Compose имеет встроенный класс, называемый TooltipDefaults. Вы можете использовать этот класс, чтобы помочь вам инициировать аргументы, составляющие TooltipBox. Например, вы могли бы использовать TooltipDefaults.rememberPlainTooltipPositionProvider, чтобы правильно позиционировать tooltip в отношении элемента-анкеры.

Rich Tooltip

Rich media tooltip занимает больше места, чем обычный tooltip и может использоваться для предоставления большего контекста о функциональности иконочного кнопки. Когда tooltip отображается, вы можете добавить кнопки и ссылки на него, чтобы предоставить дополнительные объяснения или определения.

его инициализируют похоже, как и обычного tooltip, внутри TooltipBox, но вы используете RichTooltip композицию.

TooltipBox(positionProvider = tooltipPosition,
        tooltip = {
                  RichTooltip(
                      title = { Text("RichTooltip") },
                      caretSize = caretSize,
                      action = {
                          TextButton(onClick = {
                              scope.launch {
                                  tooltipState.dismiss()
                                  tooltipState.onDispose()
                              }
                          }) {
                              Text("Dismiss")
                          }
                      }
                  ) {
                        Text("This is where a description would go.")
                  }
        },
        state = tooltipState) {
        IconButton(onClick = {
            /* Click event of the icon button */
        }) {
            Icon(imageVector = tooltipIcon,
                contentDescription = "Your icon's description",
                tint = iconColor)
        }
    }

few things to notice about a Rich tooltip:

  1. A Rich tooltip has support for a caret.

  2. Вы можете добавить действие (то есть кнопку) в подсказку, чтобы позволить пользователям более подробную информацию.
  3. Вы можете добавить логику для скрытия подсказки.

Edge Cases

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

Если вы посмотрели примерыRich tooltip сверху, вы, скорее всего, заметите, что мы добавили кнопку, чтобы скрыть подсказку после ее нажатия.

Возникнет проблема, когда пользователь нажмет эту кнопку.since удаление действия выполняется в подсказке, если пользователь хочет выполнить еще один длинный нажатий на UI элементе, который вызывает эту подсказку, подсказка не будет отображена снова. Это означает, что состояние подсказки является persistent при ее скрытии. Так как мы решили это?

Чтобы “очистить” состояние подсказки, мы должны вызвать метод onDispose, который про exposed через состояние подсказки. Как только мы это делаем, состояние подсказки сбрасывается и подсказка отобразится снова, когда пользователь выполнит длинный нажатий на UI элементе.

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RichTooltip() {
    val tooltipPosition = TooltipDefaults.rememberRichTooltipPositionProvider()
    val tooltipState = rememberTooltipState(isPersistent = true)
    val scope = rememberCoroutineScope()

    TooltipBox(positionProvider = tooltipPosition,
        tooltip = {
                  RichTooltip(
                      title = { Text("RichTooltip") },
                      caretSize = TooltipDefaults.caretSize,
                      action = {
                          TextButton(onClick = {
                              scope.launch {
                                  tooltipState.dismiss()
                                  tooltipState.onDispose()  /// <---- AQUI
                              }
                          }) {
                              Text("Dismiss")
                          }
                      }
                  ) {

                  }
        },
        state = tooltipState) {
        IconButton(onClick = {  }) {
            Icon(imageVector = Icons.Filled.Call, contentDescription = "Your icon's description")
        }
    }
}

Другой сценарий, когда состояние tooltip не сбрасывается, это когда пользователь, вместо того, чтобы вызвать метод dismiss самому себе по действию пользователя, кликнет вне tooltip, что приведет к его закрытию. Это вызывает метод dismiss в фоновом режиме, и состояние tooltip устанавливается в открытое.长按UI элемента, чтобы снова увидеть наш tooltip, не даст никакого результата.

Наша логика, вызывающая метод onDispose tooltip, не запускается, поэтому как мы можем сбросить состояние tooltip?

В настоящее время я еще не смог этого понять. Возможно, это связано с MutatorMutex tooltip.也许在未来的版本中,将会有一个API用于此。我注意到,如果 на экране есть другие tooltips, и на них кликают, это сбрасывает состояние ранее кликнутого tooltip.

Если вы хотите увидеть код, описанный здесь, вы можете пойти в этот GitHub репозиторий

Если вы хотите увидеть tooltips в приложении, вы можете проверить его здесь.

Примечания