如何在 android jetpack compose 中显示工具提示
How to show tooltip in android jetpack compose
我正在尝试在我的应用 UI 中为 FAB
、IconButton
、Menu
等添加一个简单的工具提示
如何在 jetpack compose 中添加这个?
我熟悉如何使用 XML 和编程方式添加 here.
这些方法的局限性 - 尽量避免 XML 并且对于编程方法,出于显而易见的原因,compose 中没有 findViewById。
引用 Jetpack Docs, Codelabs and Samples.
没有与工具提示相关的内容。
感谢任何帮助。
备注
不寻找任何定制,简单明了的工具提示就可以了。
最好没有第 3 方库。
更新
有同样需求的朋友,请顶一下这个issue created.
tooltip
Jetpack Compose 中尚无官方支持。
您或许可以在 androidx.compose.ui.window.Popup(...)
之上构建一些东西
另外,我会查看 TextDelegate,以测量文本,以便了解 tooltip/popup 的位置和方式。
我找到了在屏幕中央显示工具提示的解决方案。如果不需要三角形,只需删除它的行。在工具提示中添加一个表面会很好。
https://i.stack.imgur.com/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)
}
目前,Jetpack Compose 中仍然没有官方工具提示可组合。
但是可以使用 androidx.compose.ui.window.Popup
.
有效地构造一个漂亮的工具提示
我们可以以 material DropdownMenu 实施为起点。
结果示例(见下面的源代码):
如何在长按时显示工具提示(使用示例):
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
查看带有完整示例的 Gist:
https://gist.github.com/amal/aad53791308e6edb055f3cf61f881451
我正在尝试在我的应用 UI 中为 FAB
、IconButton
、Menu
等添加一个简单的工具提示
如何在 jetpack compose 中添加这个?
我熟悉如何使用 XML 和编程方式添加 here.
这些方法的局限性 - 尽量避免 XML 并且对于编程方法,出于显而易见的原因,compose 中没有 findViewById。
引用 Jetpack Docs, Codelabs and Samples.
没有与工具提示相关的内容。
感谢任何帮助。
备注
不寻找任何定制,简单明了的工具提示就可以了。
最好没有第 3 方库。
更新
有同样需求的朋友,请顶一下这个issue created.
tooltip
Jetpack Compose 中尚无官方支持。
您或许可以在 androidx.compose.ui.window.Popup(...)
另外,我会查看 TextDelegate,以测量文本,以便了解 tooltip/popup 的位置和方式。
我找到了在屏幕中央显示工具提示的解决方案。如果不需要三角形,只需删除它的行。在工具提示中添加一个表面会很好。 https://i.stack.imgur.com/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)
}
目前,Jetpack Compose 中仍然没有官方工具提示可组合。
但是可以使用 androidx.compose.ui.window.Popup
.
有效地构造一个漂亮的工具提示
我们可以以 material DropdownMenu 实施为起点。
结果示例(见下面的源代码):
如何在长按时显示工具提示(使用示例):
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
查看带有完整示例的 Gist:
https://gist.github.com/amal/aad53791308e6edb055f3cf61f881451