Skip to content

Instantly share code, notes, and snippets.

@SG-K
Created September 1, 2024 08:09
Show Gist options
  • Save SG-K/a0978281cf26712cde5f3fe6a299ff7e to your computer and use it in GitHub Desktop.
Save SG-K/a0978281cf26712cde5f3fe6a299ff7e to your computer and use it in GitHub Desktop.
@Composable
fun Modifier.tooltip(
title: String? = null,
text: String,
arrowAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
textAlign: TextAlign = TextAlign.Center,
maxWidth: Dp = Dp.Unspecified,
enabled: Boolean = true,
paddingValues: PaddingValues = PaddingValues(),
showOverlay: Boolean = true,
highlightComponent: Boolean = true,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
verticalAlignment: Alignment.Vertical = Alignment.Top,
onDismiss: () -> Unit = {}
): Modifier {
val configuration = LocalConfiguration.current
val density = LocalDensity.current
val screenWidthPx = remember { with(density) { configuration.screenWidthDp.dp.roundToPx() } }
val screenHeightPx = remember { with(density) { configuration.screenHeightDp.dp.roundToPx() } }
var positionInRoot by remember { mutableStateOf(IntOffset.Zero) }
var tooltipSize by remember { mutableStateOf(IntSize(0, 0)) }
var componentSize by remember { mutableStateOf(IntSize(0, 0)) }
val tooltipOffset by remember(positionInRoot, componentSize, tooltipSize) {
derivedStateOf {
calculateOffset(
positionInRoot, componentSize, tooltipSize, screenWidthPx, screenHeightPx, horizontalAlignment, verticalAlignment
)
}
}
if (enabled) {
Popup(
alignment = Alignment.TopEnd
) {
Box(
modifier = Modifier
.fillMaxSize()
.drawOverlayBackground(
showOverlay = showOverlay,
highlightComponent = highlightComponent,
positionInRoot = positionInRoot,
componentSize = componentSize,
backgroundColor = MaterialTheme.colorScheme.surfaceContainerLowest,
backgroundAlpha = 0.8f
)
.clickable(
onClick = {
onDismiss()
}
)
) {
ArrowTooltip(
modifier = Modifier
.widthIn(max = maxWidth)
.onSizeChanged { tooltipSize = it }
.offset { tooltipOffset }
.padding(paddingValues),
title = title,
text = text,
arrowAlignment = arrowAlignment,
textAlign = textAlign
)
}
}
}
return this then Modifier.onPlaced {
componentSize = it.size
positionInRoot = it.positionInRoot().round()
}
}
@Composable
fun ArrowTooltip(
modifier: Modifier = Modifier,
title: String? = null,
text: String,
arrowAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
textAlign: TextAlign = TextAlign.Center,
) {
Column(
modifier = modifier,
horizontalAlignment = arrowAlignment,
) {
Icon(
modifier = Modifier
.padding(
start = if (arrowAlignment != Alignment.CenterHorizontally) 6.dp else 0.dp,
end = if (arrowAlignment != Alignment.CenterHorizontally) 6.dp else 0.dp
),
painter = painterResource(id = R.drawable.ic_arrow_up),
contentDescription = "Tool tip Arrow",
tint = MaterialTheme.colorScheme.surfaceContainer,
)
Column(
modifier = Modifier
.clip(RoundedCornerShape(8.dp))
.background(MaterialTheme.colorScheme.surfaceContainer)
.padding(16.dp),
) {
Text(
modifier = Modifier
.fillMaxWidth(),
text = title ?: "",
color = MaterialTheme.colorScheme.primary,
textAlign = textAlign,
style = MaterialTheme.typography.bodyMedium.copy(
fontWeight = FontWeight.Medium
)
)
Spacer(modifier = Modifier.height(4.dp))
Text(
modifier = Modifier
.fillMaxWidth(),
text = text,
textAlign = textAlign,
style = MaterialTheme.typography.bodySmall
)
}
}
}
private fun calculateOffset(
positionInRoot: IntOffset,
componentSize: IntSize,
tooltipSize: IntSize,
screenWidthPx: Int,
screenHeightPx: Int,
horizontalAlignment: Alignment.Horizontal,
verticalAlignment: Alignment.Vertical
): IntOffset {
val horizontalAlignmentPosition = when (horizontalAlignment) {
Alignment.Start -> positionInRoot.x
Alignment.End -> positionInRoot.x + componentSize.width - tooltipSize.width
else -> positionInRoot.x + (componentSize.width / 2) - (tooltipSize.width / 2)
}
val verticalAlignmentPosition = when (verticalAlignment) {
Alignment.Top -> positionInRoot.y - tooltipSize.height
Alignment.Bottom -> positionInRoot.y + componentSize.height
else -> positionInRoot.y + (componentSize.height / 2)
}
val reult = IntOffset(
x = min(screenWidthPx - tooltipSize.width, horizontalAlignmentPosition),
y = min(screenHeightPx - tooltipSize.height, verticalAlignmentPosition)
)
return reult
}
private fun Modifier.drawOverlayBackground(
showOverlay: Boolean,
highlightComponent: Boolean,
positionInRoot: IntOffset,
componentSize: IntSize,
backgroundColor: Color,
backgroundAlpha: Float
) : Modifier {
return if (showOverlay) {
if (highlightComponent) {
drawBehind {
val highlightPath = Path().apply {
addRect(Rect(positionInRoot.toOffset(), componentSize.toSize()))
}
clipPath(highlightPath, clipOp = ClipOp.Difference) {
drawRect(SolidColor(backgroundColor.copy(alpha = backgroundAlpha)))
}
}
} else {
background(backgroundColor.copy(alpha = backgroundAlpha))
}
} else {
this
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment