Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Jetpack Compose PopUp clips content out of bounds after 16.dp

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.

enter image description here

@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.

enter image description here

@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)
        )

    }
}
like image 679
Thracian Avatar asked Oct 29 '25 15:10

Thracian


1 Answers

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.

tooltip

popup

P.S. Since maxSupportedElevation is private readonly property in PopupLayout. I copied it's implementation to validate the answer.

like image 108
Adnan Habib Avatar answered Nov 01 '25 05:11

Adnan Habib



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!