I'm working on building tooltip with caret that can has varying height. However Popup clips anything out of its bounds when the dimensions is bigger than 16.dp in vertical direction. I tried setting PopProperties(clipEnabled = false) as well.
I started with ToolTipBox then realized the issue comes from PopUp itself. Why does this happen and is there a way to remove this limitation?
If you change caret height above 16.dp you will see that part exceeding this height is clipped.

@Preview
@Composable
fun TooltipSample() {
var caretSize by remember {
mutableStateOf(0f)
}
Column(
modifier = Modifier.fillMaxSize()
) {
Text("Caret width: ${caretSize}dp", fontSize = 18.sp)
Slider(
value = caretSize,
onValueChange = {
caretSize = it
},
valueRange = 0f..100f
)
Spacer(modifier = Modifier.height(150.dp))
val state = rememberTooltipState(
isPersistent = true
)
val coroutineScope = rememberCoroutineScope()
Row {
Spacer(modifier = Modifier.width(200.dp))
TooltipBox(
positionProvider = rememberPlainTooltipPositionProvider(
spacingBetweenTooltipAndAnchor = caretSize.dp
),
state = state,
tooltip = {
PlainTooltip(
caretProperties = CaretProperties(
caretWidth = caretSize.dp,
caretHeight = caretSize.dp
),
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
containerColor = Color.Red
) {
Text(
text = "Tooltip Content for testing...",
modifier = Modifier.padding(16.dp)
)
}
},
content = {
Icon(
modifier = Modifier.clickable {
if (state.isVisible) {
state.dismiss()
} else {
coroutineScope.launch {
state.show()
}
}
},
imageVector = Icons.Default.Info,
contentDescription = null
)
}
)
}
}
}
To track where problem is i also experimented with PopUp itself where it clips blue and yellow circles and rectangle with stroke which i intend to replace with my own implementation of caret or arrow.

@Composable
private fun AnchorContent(
modifier: Modifier
) {
// This is content
Icon(
modifier = modifier,
imageVector = Icons.Default.Info,
contentDescription = null
)
}
@Composable
private fun PopUpBox(
isVisible: Boolean,
onDismissRequest: () -> Unit,
content: @Composable (LayoutCoordinates?) -> Unit,
anchor: @Composable () -> Unit
) {
var anchorBounds: LayoutCoordinates? by remember { mutableStateOf(null) }
val wrappedAnchor: @Composable () -> Unit = {
Box(
modifier = Modifier.onGloballyPositioned { anchorBounds = it }
) {
anchor()
}
}
Box(
modifier = Modifier.border(2.dp, Color.Blue)
) {
if (isVisible) {
Popup(
properties = PopupProperties(clippingEnabled = true),
popupPositionProvider = rememberPlainTooltipPositionProvider(
spacingBetweenTooltipAndAnchor = 20.dp
),
onDismissRequest = onDismissRequest
) {
content(anchorBounds)
}
}
Box {
wrappedAnchor()
}
}
}
And demo to reproduce to issue with Popup
@Preview
@Composable
private fun PopupTest() {
var showPopup by remember {
mutableStateOf(false)
}
var padding by remember {
mutableFloatStateOf(0f)
}
Column(
modifier = Modifier.fillMaxSize().background(backgroundColor)
) {
Slider(
value = padding,
onValueChange = {
padding = it
},
valueRange = 0f..350f
)
Spacer(modifier = Modifier.height(150.dp))
Row(
verticalAlignment = Alignment.CenterVertically
) {
Text("Info")
Spacer(modifier = Modifier.width(padding.dp))
Box {
PopUpBox(
onDismissRequest = {
showPopup = false
},
isVisible = showPopup,
anchor = {
AnchorContent(
modifier = Modifier
.size(80.dp)
.border(2.dp, Color.Green)
.clickable {
showPopup = showPopup.not()
}
)
},
content = { anchorLayoutCoordinates: LayoutCoordinates? ->
Box(
modifier = Modifier
.drawWithContent {
drawContent()
drawCircle(
color = Color.Blue,
radius = 60.dp.toPx(),
center = Offset(
x = size.width / 2,
y = 0f
)
)
drawCircle(
color = Color.Yellow,
radius = 60.dp.toPx(),
center = Offset(
x = size.width / 2,
y = size.height + 30.dp.toPx()
)
)
anchorLayoutCoordinates?.boundsInWindow()?.let {
val width = 60.dp.toPx()
drawRect(
color = Color.Red,
size = Size(width, width),
topLeft = Offset(
x = it.center.x - width / 2,
y = size.height
),
style = Stroke(
2.dp.toPx()
)
)
}
}
.padding(horizontal = 16.dp)
.fillMaxWidth()
.background(
Color.White, RoundedCornerShape(16.dp)
)
.padding(16.dp)
) {
Text(
"This is PopUp Content something something something",
fontSize = 16.sp
)
}
}
)
}
}
Box(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.background(Color.Gray)
)
}
}
Popup uses PopupLayout to display its content. If you examine the implementation of PopupLayout, a custom android view built using Jetpack Compose UI, you'll find the following code:
class PopupLayout {
...
// On systems older than Android S, there is a bug in the surface insets matrix math used by
// elevation, so high values of maxSupportedElevation break accessibility services: b/232788477.
private val maxSupportedElevation = 8.dp
...
init {
...
// Enable children to draw their shadow by not clipping them
clipChildren = false
// Allocate space for elevation
with(density) { elevation = maxSupportedElevation.toPx() }
// Simple outline to force window manager to allocate space for shadow.
// Note that the outline affects clickable area for the dismiss listener. In case of shapes
// like circle the area for dismiss might be to small (rectangular outline consuming clicks
// outside of the circle).
outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, result: Outline) {
result.setRect(0, 0, view.width, view.height)
// We set alpha to 0 to hide the view's shadow and let the composable to draw its
// own shadow. This still enables us to get the extra space needed in the surface.
result.alpha = 0f
}
}
}
}
The code speaks for itself, but here is my explanation. Every view clips by default to its content size because any spacing beyond the actual size is unexpected. However, Popup provides 8.dp spacing, which is equivalent to 2x pixels on xhdpi screens, for drawing the shadows. You are drawing within this area but cannot draw beyond it because the maxSupportedElevation value in PopupLayout is hardcoded to 8.dp.
Here are the results with 50.dp elevation.


P.S. Since maxSupportedElevation is private readonly property in PopupLayout. I copied it's implementation to validate the answer.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With