-
-
Save art-shen/aad53791308e6edb055f3cf61f881451 to your computer and use it in GitHub Desktop.
| @file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") | |
| // Tooltip implementation for AndroidX Jetpack Compose | |
| // See usage example in the next file | |
| // Tested with Compose version **1.1.0-alpha06** | |
| // Based on material DropdownMenu implementation. | |
| 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 = TooltipOffset, | |
| properties: PopupProperties = TooltipPopupProperties, | |
| 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) | |
| } | |
| } | |
| } | |
| } | |
| /** | |
| * Simple text version of [Tooltip] | |
| */ | |
| @Composable | |
| fun Tooltip( | |
| expanded: MutableState<Boolean>, | |
| text: String, | |
| modifier: Modifier = Modifier, | |
| timeoutMillis: Long = TooltipTimeout, | |
| backgroundColor: Color = Color.Black, | |
| offset: DpOffset = TooltipOffset, | |
| properties: PopupProperties = TooltipPopupProperties, | |
| ) { | |
| Tooltip(expanded, modifier, timeoutMillis, backgroundColor, offset, properties) { | |
| Text(text) | |
| } | |
| } | |
| /** @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) | |
| .verticalScroll(rememberScrollState()), | |
| content = content, | |
| ) | |
| } | |
| } | |
| private val TooltipElevation = 16.dp | |
| private val TooltipPadding = 16.dp | |
| private val TooltipPopupProperties = PopupProperties(focusable = true) | |
| private val TooltipOffset = DpOffset(0.dp, 0.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 helpers | |
| /** | |
| * 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()) | |
| } |
| 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 | |
| /** | |
| * How to show a Tooltip in AndroidX Jetpack Compose on long click. | |
| * Usage example. | |
| */ | |
| @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!!") | |
| } | |
| } | |
| } |
@amal thanks a lot
@amal @file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") This is not working for me. Error still remains in file like for @camper9993
@himanshufoodpanda it should work. Checked with many different versions of compose.
Please check that source code is copied exactly as is. @file:Suppress should be exactly on the first line, before all the imports.
Thanks!!!
How can I keep the popup when I click outside? It dismisses when I click outside. In my case, it must stay in place and move up and down. And I need to show and dismiss at certain events.
I think this should be changed:
properties: PopupProperties = PopupProperties(focusable = false),
In that way the tooltip is not catching screen touches so you can keep interacting with the rest of the widgets while the tooltip is shown.

@camper9993 no, this solution uses internal part of compose framework, so you need this in code to have the access:
@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")It's probably lost in copy-paste, see here: #file-tooltip-kt-L1