如何在 android jetpack compose 中显示工具提示

问题描述 投票:0回答:7

我正在尝试在我的应用程序 UI 中添加一个简单的工具提示,用于

FAB
IconButton
Menu

Example tooltip

如何将其添加到jetpack compose
我熟悉如何使用 XML 和以编程方式进行添加,如此处所述。

这些方法的局限性 - 尝试尽可能避免 XML,对于编程方法,出于明显的原因,在 compose 中没有 findViewById。

参考了 Jetpack 文档Codelabs示例
没有任何与工具提示相关的内容。

如有任何帮助,我们将不胜感激。

注意
不需要任何自定义,简单明了的工具提示就可以了。
最好没有第 3 方库。

更新
任何有相同需求的人,请提出此已创建问题

android tooltip android-jetpack-compose
7个回答
22
投票

更新

自版本 1.1.0(2023 年 5 月 10 日发布)以来,Jetpack Compose Material 3 现在包含官方工具提示。它们有两种类型:普通工具提示 (

PlainTooltipBox
) 和丰富工具提示 (
RichTooltipBox
)。

简单的工具提示

简单的工具提示简要描述了 UI 元素。普通工具提示非常适合标记没有文本的 UI 元素,例如仅图标和字段元素。

Plain tooltip on an icon button

要将普通工具提示应用到任何组件,请用

PlainTooltipBox()
包裹组件并将
Modifier.tooltipAnchor()
添加到组件的修饰符中:

import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.PlainTooltipBox
import androidx.compose.material3.Text

PlainTooltipBox(
   tooltip = { Text("Present now") }
) {
   IconButton(
       onClick = { /* Icon button's click event */ },
       modifier = Modifier.tooltipTrigger()
   ) {
       Icon(
           imageVector = Icons.Filled.FilePresent,
           contentDescription = "Present now"
       )
   }
}

丰富的工具提示

丰富的工具提示非常适合较长的文本,例如定义或解释。丰富的工具提示为 UI 元素提供了额外的上下文,并且可以包含按钮或超链接。

Rich tooltip with description and text button as action

您可以通过向

isPersistent
提供
RichTooltipState()
参数来获得持久或非持久的丰富工具提示。要关闭持久工具提示,您需要点击工具提示区域外部或在工具提示状态上调用关闭操作。

非持久工具提示会在短时间内自动消失。

要添加丰富的工具提示,您可以使用

RichTooltipBox()
可组合项并修改工具提示状态来控制工具提示的可见性。

val tooltipState = remember { RichTooltipState() }
val scope = rememberCoroutineScope()
RichTooltipBox(
   title = { Text("Add others") },
   action = {
       TextButton(
           onClick = { scope.launch { tooltipState.dismiss() } }
       ) { Text("Learn More") }
   },
   text = { Text("Share this collection with friends...") },
   tooltipState = tooltipState
) {
   IconButton(
       onClick = { /* Icon button's click event */ },
       modifier = Modifier.tooltipTrigger()
   ) {
       Icon(
           imageVector = Icons.Filled.People,
           contentDescription = "Add others"
       )
   }
}

旧答案,对于手动实施仍然有用。

截至 2021 年 10 月 21 日,Jetpack Compose 中没有可组合的官方工具提示。

但是可以使用

androidx.compose.ui.window.Popup
有效地构建一个漂亮的工具提示。
我们可以以材料DropdownMenu实现为起点。

结果示例(见下面的源代码):

Tooltip example for AndroidX Jetpack Compose

如何在长按时显示工具提示(使用示例):

import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.material.Text
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.Role

@Composable
@OptIn(ExperimentalFoundationApi::class)
fun TooltipOnLongClickExample(onClick: () -> Unit = {}) {
  // Commonly a Tooltip can be placed in a Box with a sibling
  // that will be used as the 'anchor' for positioning.
  Box {
    val showTooltip = remember { mutableStateOf(false) }

    // Buttons and Surfaces don't support onLongClick out of the box,
    // so use a simple Box with combinedClickable
    Box(
      modifier = Modifier
        .combinedClickable(
          interactionSource = remember { MutableInteractionSource() },
          indication = rememberRipple(),
          onClickLabel = "Button action description",
          role = Role.Button,
          onClick = onClick,
          onLongClick = { showTooltip.value = true },
        ),
    ) {
      Text("Click Me (will show tooltip on long click)")
    }

    Tooltip(showTooltip) {
      // Tooltip content goes here.
      Text("Tooltip Text!!")
    }
  }
}

工具提示可组合源代码:

@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")

import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties
import androidx.core.graphics.ColorUtils
import kotlinx.coroutines.delay


/**
 * Tooltip implementation for AndroidX Jetpack Compose.
 * Based on material [DropdownMenu] implementation
 *
 * A [Tooltip] behaves similarly to a [Popup], and will use the position of the parent layout
 * to position itself on screen. Commonly a [Tooltip] will be placed in a [Box] with a sibling
 * that will be used as the 'anchor'. Note that a [Tooltip] by itself will not take up any
 * space in a layout, as the tooltip is displayed in a separate window, on top of other content.
 *
 * The [content] of a [Tooltip] will typically be [Text], as well as custom content.
 *
 * [Tooltip] changes its positioning depending on the available space, always trying to be
 * fully visible. It will try to expand horizontally, depending on layout direction, to the end of
 * its parent, then to the start of its parent, and then screen end-aligned. Vertically, it will
 * try to expand to the bottom of its parent, then from the top of its parent, and then screen
 * top-aligned. An [offset] can be provided to adjust the positioning of the menu for cases when
 * the layout bounds of its parent do not coincide with its visual bounds. Note the offset will
 * be applied in the direction in which the menu will decide to expand.
 *
 * @param expanded Whether the tooltip is currently visible to the user
 * @param offset [DpOffset] to be added to the position of the tooltip
 *
 * @see androidx.compose.material.DropdownMenu
 * @see androidx.compose.material.DropdownMenuPositionProvider
 * @see androidx.compose.ui.window.Popup
 *
 * @author Artyom Krivolapov
 */
@Composable
fun Tooltip(
  expanded: MutableState<Boolean>,
  modifier: Modifier = Modifier,
  timeoutMillis: Long = TooltipTimeout,
  backgroundColor: Color = Color.Black,
  offset: DpOffset = DpOffset(0.dp, 0.dp),
  properties: PopupProperties = PopupProperties(focusable = true),
  content: @Composable ColumnScope.() -> Unit,
) {
  val expandedStates = remember { MutableTransitionState(false) }
  expandedStates.targetState = expanded.value

  if (expandedStates.currentState || expandedStates.targetState) {
    if (expandedStates.isIdle) {
      LaunchedEffect(timeoutMillis, expanded) {
        delay(timeoutMillis)
        expanded.value = false
      }
    }

    Popup(
      onDismissRequest = { expanded.value = false },
      popupPositionProvider = DropdownMenuPositionProvider(offset, LocalDensity.current),
      properties = properties,
    ) {
      Box(
        // Add space for elevation shadow
        modifier = Modifier.padding(TooltipElevation),
      ) {
        TooltipContent(expandedStates, backgroundColor, modifier, content)
      }
    }
  }
}


/** @see androidx.compose.material.DropdownMenuContent */
@Composable
private fun TooltipContent(
  expandedStates: MutableTransitionState<Boolean>,
  backgroundColor: Color,
  modifier: Modifier,
  content: @Composable ColumnScope.() -> Unit,
) {
  // Tooltip open/close animation.
  val transition = updateTransition(expandedStates, "Tooltip")

  val alpha by transition.animateFloat(
    label = "alpha",
    transitionSpec = {
      if (false isTransitioningTo true) {
        // Dismissed to expanded
        tween(durationMillis = InTransitionDuration)
      } else {
        // Expanded to dismissed.
        tween(durationMillis = OutTransitionDuration)
      }
    }
  ) { if (it) 1f else 0f }

  Card(
    backgroundColor = backgroundColor.copy(alpha = 0.75f),
    contentColor = MaterialTheme.colors.contentColorFor(backgroundColor)
      .takeOrElse { backgroundColor.onColor() },
    modifier = Modifier.alpha(alpha),
    elevation = TooltipElevation,
  ) {
    val p = TooltipPadding
    Column(
      modifier = modifier
        .padding(start = p, top = p * 0.5f, end = p, bottom = p * 0.7f)
        .width(IntrinsicSize.Max),
      content = content,
    )
  }
}

private val TooltipElevation = 16.dp
private val TooltipPadding = 16.dp

// Tooltip open/close animation duration.
private const val InTransitionDuration = 64
private const val OutTransitionDuration = 240

// Default timeout before tooltip close
private const val TooltipTimeout = 2_000L - OutTransitionDuration


// Color utils

/**
 * Calculates an 'on' color for this color.
 *
 * @return [Color.Black] or [Color.White], depending on [isLightColor].
 */
fun Color.onColor(): Color {
  return if (isLightColor()) Color.Black else Color.White
}

/**
 * Calculates if this color is considered light.
 *
 * @return true or false, depending on the higher contrast between [Color.Black] and [Color.White].
 *
 */
fun Color.isLightColor(): Boolean {
  val contrastForBlack = calculateContrastFor(foreground = Color.Black)
  val contrastForWhite = calculateContrastFor(foreground = Color.White)
  return contrastForBlack > contrastForWhite
}

fun Color.calculateContrastFor(foreground: Color): Double {
  return ColorUtils.calculateContrast(foreground.toArgb(), toArgb())
}

使用 AndroidX Jetpack Compose 版本进行测试

1.1.0-alpha06

查看带有完整示例的要点:
https://gist.github.com/amal/aad53791308e6edb055f3cf61f881451


11
投票

使用 M3,您可以使用

PlainTooltipBox
可组合项:

类似:

    PlainTooltipBox(
        tooltip = { Text("Add to favorites" ) },
        contentColor = White,
    ) {
        IconButton(
            onClick = { /* Icon button's click event */ },
            modifier = Modifier.tooltipAnchor()
        ) {
            Icon(
                imageVector = Icons.Filled.Favorite,
                contentDescription = "Localized Description"
            )
        }
    }

长按

anchor
时会调用工具提示。

enter image description here

如果您想通过事件单击显示工具提示,您可以使用:

    val tooltipState = remember { PlainTooltipState() }
    val scope = rememberCoroutineScope()

    PlainTooltipBox(
        tooltip = { Text("Add to favorites" ) },
        tooltipState = tooltipState
    ) {
        IconButton(
            onClick = { /* Icon button's click event */ },
            modifier = Modifier.tooltipAnchor()
        ) {
            Icon(
                imageVector = Icons.Filled.Favorite,
                contentDescription = "Localized Description"
            )
        }
    }

    Spacer(Modifier.requiredHeight(30.dp))
    OutlinedButton(
        onClick = { scope.launch { tooltipState.show() } }
    ) {
        Text("Display tooltip")
    }

5
投票

截至 2024 年 7 月,所有当前答案均已过时。

这是我发现的一种制作正确工具提示的简单方法:

val tooltipPosition = TooltipDefaults.rememberPlainTooltipPositionProvider()
val tooltipState = rememberBasicTooltipState(isPersistent = false)

...

BasicTooltipBox(
    positionProvider = tooltipPosition,
    state = tooltipState,
    tooltip = {
        ElevatedCard {
            Text(
                text = stringResource(R.string.go_back),
                modifier = Modifier.padding(SMALL_PADDING),
            )
        }
    },
) {
    IconButton(
        onClick = onClickBack,
    ) {
        Icon(
            Icons.AutoMirrored.Outlined.ArrowBack,
            contentDescription = stringResource(R.string.go_back),
        )
    }
}

2
投票

Jetpack Compose
尚无官方 tooltip 支持。

你可能可以在

androidx.compose.ui.window.Popup(...)

之上构建一些东西

我还会查看 TextDelegate,测量文本,以便了解工具提示/弹出窗口的位置和方式。


1
投票

我改编了 Luchi 的答案,使其在较新的 Jetpack Compose 中工作。

工具提示.kt

@Composable
fun Tooltip(text: String) {
    val color = Color.Gray

    Row(
        modifier = Modifier
            .padding(horizontal = 16.dp, vertical = 16.dp)
            .fillMaxWidth(),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.Center
    ) {
        Column {
            Box(modifier = Modifier
                .padding(PaddingValues(start = 12.dp))
                .background(
                    color = color,
                    shape = TriangleShape()
                )
                .width(20.dp)
                .height(10.dp)
            )

            Box(modifier = Modifier
                .background(
                    color = color,
                    shape = RoundedCornerShape(size = 6.dp)
                )
            ) {
                Text(
                    text = text,
                    modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
                    textAlign = TextAlign.Center,
                    color = Color.White
                )
            }
        }
    }
}

TriangleShape.kt

class TriangleShape : Shape {
    override fun createOutline(
        size: Size,
        layoutDirection: LayoutDirection,
        density: Density
    ): Outline {

        val path = Path()
        path.apply {
            moveTo(x = size.width / 2, y = 0f)
            lineTo(x = size.width, y = size.height)
            lineTo(x = 0f, y = size.height)
        }

        return Outline.Generic(path = path)
    }
}

结果

enter image description here

您可以进一步自定义代码以满足您的需要。


0
投票

我找到了一个在屏幕中央显示工具提示的解决方案。如果不需要三角形,只需删除它的行即可。向工具提示添加一个表面会很好。 https://i.sstatic.net/1WY6i.png

@ExperimentalComposeUiApi
@ExperimentalAnimationApi
@Composable
fun tooltip(text: String) {

   Row(
            modifier = Modifier
                .padding(horizontal = 16.dp, vertical = 16.dp)
                .fillMaxWidth(),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.Center
        ) {
            Column {

                Row(modifier = Modifier
                    .padding(PaddingValues(start = 12.dp))
                    .background(
                        color = colors.xxx,
                        shape = TriangleShape(arrowPosition)
                    )
                    .width(arrowSize.width)
                    .height(arrowSize.height)
                ) {}

                Row(modifier = Modifier
                    .background(
                        color = colors.xxx,
                        shape = RoundedCornerShape(size = 3.dp)
                    )
                ) {
                    Text(
                        modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
                        text = text,
                        alignment = TextAlign.Center,
                    )
                }

            }
        }
}

绘制三角形的函数:

class TriangleEdge(val position: ArrowPosition) : Shape {

override fun createOutline(
    size: Size,
    layoutDirection: LayoutDirection,
    density: Density
): Outline {

val trianglePath = Path()
trianglePath.apply {
            moveTo(x = size.width/2, y = 0f)
            lineTo(x = size.width, y = size.height)
            lineTo(x = 0f, y = size.height)
        }

return Outline.Generic(path = trianglePath)

}


0
投票

纠正某人的答案

@Róbert Nagy 的回答

根据如何在 Jetpack Compose 中使用工具提示

现在有

TooltipBox
功能(在
1.2.1
依赖中的
androidx.compose.material3
版本中添加)

TooltipBox
函数定义如下。

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

Art Shendrik 的回答

PlainTooltipBox
依赖中的
1.3.1
版本没有
androidx.compose.material3
功能。

PlainTooltipBox
依赖项中的
1.3.1
版本中没有
androidx.compose.material3
功能。

这里是

libs.version.toml
的代码片段(我使用
Maven

libs.version.toml

[versions]
// ...
material3 = "1.3.1"

[libraries]
// ...

androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" }
© www.soinside.com 2019 - 2024. All rights reserved.