Jetpack Compose PopUp удаляет содержимое за пределы границ после 16.dp

Я работаю над созданием всплывающей подсказки с курсором, который может иметь разную высоту. Однако Popup обрезает что-либо за пределами своих границ, если размеры превышают 16.dp по вертикали. Я также попробовал установить PopProperties(clipEnabled = false).

Я начал с ToolTipBox, а затем понял, что проблема связана с самим PopUp. Почему это происходит и есть ли способ снять это ограничение?

Если вы измените высоту курсора выше 16.dp, вы увидите, что часть, превышающая эту высоту, обрезается.

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

    }
}

Чтобы отследить, в чем проблема, я также поэкспериментировал с самим PopUp, где он обрезает синие и желтые круги и прямоугольник с обводкой, которые я собираюсь заменить своей собственной реализацией курсора или стрелки.

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

И демо для воспроизведения, чтобы выпустить 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)
        )

    }
}
2
0
191
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Popup использует PopupLayout для отображения своего контента. Если вы изучите реализацию PopupLayout, пользовательского представления Android, созданного с использованием пользовательского интерфейса Jetpack Compose, вы обнаружите следующий код:

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
            }
        }
    }
}

Код говорит сам за себя, но вот мое объяснение. Каждое представление по умолчанию обрезается до размера своего содержимого, поскольку любое расстояние, превышающее фактический размер, является неожиданным. Однако Popup обеспечивает 8.dp интервал, эквивалентный 2x пикселям на xhdpi экранах, для рисования теней. Вы рисуете внутри этой области, но не можете выйти за ее пределы, поскольку значение maxSupportedElevation в PopupLayout жестко закодировано в 8.dp.

Вот результаты с повышением 50.dp.

P.S. Поскольку maxSupportedElevation является частной собственностью, доступной только для чтения, в PopupLayout. Я скопировал его реализацию, чтобы проверить ответ.

Спасибо за ваш ответ. Что вы подразумеваете под This same offset is applied in your PopupTest example, making it appear clipped.?

Thracian 12.07.2024 19:42

Ваш ответ не решает эту проблему. Вы помещаете Column внутрь контента, и он уже находится внутри Popup. Если вы попытаетесь щелкнуть слева или справа от курсора, вы увидите, что он неактивен. Вы также можете проверить свое решение с помощью рамки вокруг Колонны. То, что я прошу, все еще актуально: вы не можете вытянуть Popup после 16dp в вертикальном направлении.

Thracian 12.07.2024 19:47

Вы можете убедиться в этом, нарисовав что-нибудь внутри холста, например drawCircle(color = Color.Green, radius = 40.dp.toPx(), center = Offset(center.x, size.height)).

Thracian 12.07.2024 19:51

Я проголосовал за этот ответ, потому что именно так я думал, что решу проблему, и я реализовал это в своем руководстве, за исключением того, что я использовал Modifier.layout вместо Column и думал об использовании пользовательского модификатора щелчка, чтобы закрыть Popup, когда пользователь нажимает где-нибудь внутри Popup, но не в контенте, но не отвечает, почему контент из Popup обрезается после 16.dp. И вы можете легко проверить, что на Canvas нарисовано что-либо, выходящее за эту границу.

Thracian 12.07.2024 19:58

Мое решение по преодолению этой проблемы с зажимом находится здесь github.com/SmartToolFactory/Jetpack-Compose-Tutorials/blob/…

Thracian 12.07.2024 19:59

И причина PlaintToolTip каретки обрезается после 16dp та же самая причина, по которой она не увеличивает Popup высоту в зависимости от каретки при рисовании ее за ее пределами, и поэтому она обрезается.

Thracian 12.07.2024 20:11

@Thracian Ответ обновлен. Дайте мне знать, если вам нужен код Popup, который я скопировал из реализации Compose, для проверки ответа.

Adnan Habib 13.07.2024 08:27

Спасибо, Аднан. Это отличный ответ. Не могли бы вы опубликовать всю реализацию с помощью PopupLayout для второго изображения. Я уже принимаю ответ, потому что вопрос в том, почему контент обрезается, и на него очень хорошо отвечают.

Thracian 13.07.2024 08:46

@Thracian Всегда пожалуйста! Используйте Popup в своем PopUpBox из этого файла вместо материала.

Adnan Habib 13.07.2024 09:14

Другие вопросы по теме