修饰符工厂函数不应标记为 @Composable

can*_*eru 8 android kotlin android-jetpack-compose

我为 Modifier 创建了一个扩展函数,单击时将向用户显示 BalloonPopup。我为此函数使用 @Composable 标签,因为我将在气球内显示的内容也是 @Composable。然而编译器给了我以下警告;

修饰工厂函数不应标记为@Composable,而应使用组合

我还应用了建议的更改,但是当用户单击视图时,气球根本没有显示。

我的问题是;

  1. 为什么修饰符工厂函数不应标记为 @Composable
  2. 对于这样的扩展函数使用 @Composable 和组合 { ... } 有什么区别。因为目前我还没有看到使用 @Composable 标签有任何缺点
  3. 为什么不显示气球,即使代码在调试代码时通过了 if (showTooltip) 条件。

以下是我在应用建议之前和之后使用的函数的代码;

前:

@Composable
fun Modifier.setPopup(enabled: Boolean = false, content: @Composable BoxScope.() -> Unit): Modifier {
    if (enabled) {
        var anchorOffset by remember { mutableStateOf<LayoutCoordinates?>(null) }
        var showTooltip by remember { mutableStateOf(false) }
        if (showTooltip) {
            BalloonPopup(
                onDismissRequest = {
                    showTooltip = false
                },
                content = content,
                anchorCoordinates = anchorOffset
            )
        }
        return this.clickable {
            showTooltip = true
        }.onGloballyPositioned {
            anchorOffset = it
        }
    } else {
        return this
    }
}
Run Code Online (Sandbox Code Playgroud)

后:

fun Modifier.setPopup(enabled: Boolean = false, content: @Composable BoxScope.() -> Unit): Modifier = composed {
    if (enabled) {
        var anchorOffset by remember { mutableStateOf<LayoutCoordinates?>(null) }
        var showTooltip by remember { mutableStateOf(false) }
        if (showTooltip) {
            BalloonPopup(
                onDismissRequest = {
                    showTooltip = false
                },
                content = content,
                anchorCoordinates = anchorOffset
            )
        }
        this.clickable {
            showTooltip = true
        }.onGloballyPositioned {
            anchorOffset = it
        }
    } else {
        this
    }
}
Run Code Online (Sandbox Code Playgroud)

这就是我调用扩展函数的方式;

Image(
   modifier = Modifier
                .setPopup(enabled = true) {
                    Text(
                         modifier = Modifier.padding(4.dp),
                         text = "-30 rssi",
                         fontSize = 13.sp
                    )
                },
   painter = painterResource(id = android.R.drawable.ic_secure),
   contentDescription = "Signal Strength"
)
Run Code Online (Sandbox Code Playgroud)

这是扩展函数内部使用的 BalloonPopup 类;

suspend fun initTimer(time: Long, onEnd: () -> Unit) {
    delay(timeMillis = time)
    onEnd()
}

@Composable
fun BalloonPopup(
    cornerRadius: Float = 8f,
    arrowSize: Float = 32f,
    dismissTime: Long = 3,
    onDismissRequest: (() -> Unit)? = null,
    anchorCoordinates: LayoutCoordinates? = null,
    content: @Composable BoxScope.() -> Unit
) {
    if (anchorCoordinates != null) {
        var arrowPosition by remember { mutableStateOf(BalloonShape.ArrowPosition.TOP_RIGHT) }

        /**
         * copied from AlignmentOffsetPositionProvider of android sdk and added the calculation for
         * arrowPosition
         * */
        class BalloonPopupPositionProvider(
            val alignment: Alignment,
            val offset: IntOffset
        ) : PopupPositionProvider {
            override fun calculatePosition(
                anchorBounds: IntRect,
                windowSize: IntSize,
                layoutDirection: LayoutDirection,
                popupContentSize: IntSize
            ): IntOffset {
                // TODO: Decide which is the best way to round to result without reimplementing Alignment.align
                var popupPosition = IntOffset(0, 0)

                // Get the aligned point inside the parent
                val parentAlignmentPoint = alignment.align(
                    IntSize.Zero,
                    IntSize(anchorBounds.width, anchorBounds.height),
                    layoutDirection
                )
                // Get the aligned point inside the child
                val relativePopupPos = alignment.align(
                    IntSize.Zero,
                    IntSize(popupContentSize.width, popupContentSize.height),
                    layoutDirection
                )

                // Add the position of the parent
                popupPosition += IntOffset(anchorBounds.left, anchorBounds.top)

                // Add the distance between the parent's top left corner and the alignment point
                popupPosition += parentAlignmentPoint

                // Subtract the distance between the children's top left corner and the alignment point
                popupPosition -= IntOffset(relativePopupPos.x, relativePopupPos.y)

                // Add the user offset
                val resolvedOffset = IntOffset(
                    offset.x * (if (layoutDirection == LayoutDirection.Ltr) 1 else -1),
                    offset.y
                )
                popupPosition += resolvedOffset

                arrowPosition =
                    if (anchorBounds.left > popupPosition.x) {
                        BalloonShape.ArrowPosition.TOP_RIGHT
                    } else {
                        BalloonShape.ArrowPosition.TOP_LEFT
                    }

                return popupPosition
            }
        }

        var isVisible by remember { mutableStateOf(false) }

        AnimatedVisibility(visible = isVisible) {
            Popup(
                popupPositionProvider = BalloonPopupPositionProvider(
                    alignment = Alignment.TopCenter,
                    offset = IntOffset(
                        x = anchorCoordinates.positionInParent().x.roundToInt(),
                        y = anchorCoordinates.positionInParent().y.roundToInt() + anchorCoordinates.size.height
                    )
                ),
                onDismissRequest = onDismissRequest
            ) {
                Box(
                    modifier = Modifier
                        .wrapContentSize()
                        .shadow(
                            dimensionResource(id = R.dimen.cardview_default_elevation),
                            shape = BalloonShape(
                                cornerRadius = cornerRadius,
                                arrowSize = arrowSize,
                                arrowPosition = arrowPosition,
                                anchorCoordinates = anchorCoordinates
                            )
                        )
                        .border(
                            Dp.Hairline, CCTechAppDefaultTheme.primary,
                            shape = BalloonShape(
                                cornerRadius = cornerRadius,
                                arrowSize = arrowSize,
                                arrowPosition = arrowPosition,
                                anchorCoordinates = anchorCoordinates
                            )
                        )
                        .background(
                            shape = BalloonShape(
                                cornerRadius = cornerRadius,
                                arrowSize = arrowSize,
                                arrowPosition = arrowPosition,
                                anchorCoordinates = anchorCoordinates
                            ),
                            color = CCTechAppDefaultTheme.surface
                        )
                        .padding(top = arrowSize.toDP())
                ) {
                    content()
                }
            }
        }

        val coroutineScope = rememberCoroutineScope()
        LaunchedEffect(key1 = Unit, block = {
            isVisible = true
            coroutineScope.launch {
                initTimer(dismissTime * 1000) {
                    isVisible = false
                    (onDismissRequest ?: {}).invoke()
                }
            }
        })

    }
}

class BalloonShape(
    private val cornerRadius: Float,
    private val arrowSize: Float,
    var arrowPosition: ArrowPosition = ArrowPosition.TOP_RIGHT,
    private val anchorCoordinates: LayoutCoordinates
) : Shape {

    enum class ArrowPosition {
        TOP_LEFT, TOP_RIGHT
    }

    override fun createOutline(
        size: Size,
        layoutDirection: LayoutDirection,
        density: Density
    ): Outline {
        val balloonLeft = 0f
        val balloonRight = size.width
        val balloonTop = 0f + arrowSize
        val balloonBottom = size.height

        val arrowTopY = 0f
        val arrowTopX =
            if (arrowPosition == ArrowPosition.TOP_LEFT) balloonLeft + anchorCoordinates.size.width / 2
            else balloonRight - anchorCoordinates.size.width / 2
        val arrowLeftX = arrowTopX - (arrowSize / 2f)
        val arrowLeftY = arrowTopY + arrowSize
        val arrowRightX = arrowTopX + (arrowSize / 2f)
        val arrowRightY = arrowTopY + arrowSize


        val path = Path().apply {
            moveTo(
                x = arrowTopX,
                y = arrowTopY
            )           // Start point on the top of the arrow
            lineTo(
                x = arrowLeftX,
                y = arrowLeftY
            )     // Left edge of the arrow
            lineTo(
                x = balloonLeft,
                y = balloonTop
            )                         // TopLeft edge of the rectangle
            arcTo(
                rect = Rect(
                    left = balloonLeft,
                    top = balloonTop,
                    right = balloonLeft + cornerRadius * 2f,
                    bottom = balloonTop + cornerRadius * 2f
                ),
                startAngleDegrees = 270f,
                sweepAngleDegrees = -90f,
                forceMoveTo = false
            )
            lineTo(
                x = balloonLeft,
                y = balloonBottom
            )                     // Left edge of the rectangle
            arcTo(
                rect = Rect(
                    left = balloonLeft,
                    top = balloonBottom - cornerRadius * 2f,
                    right = balloonLeft + cornerRadius * 2f,
                    bottom = balloonBottom
                ),
                startAngleDegrees = 180f,
                sweepAngleDegrees = -90f,
                forceMoveTo = false
            )
            lineTo(
                x = balloonRight,
                y = balloonBottom
            )                 // Bottom edge of the rectangle
            arcTo(
                rect = Rect(
                    left = balloonRight - cornerRadius * 2f,
                    top = balloonBottom - cornerRadius * 2f,
                    right = balloonRight,
                    bottom = balloonBottom
                ),
                startAngleDegrees = 90f,
                sweepAngleDegrees = -90f,
                forceMoveTo = false
            )
            lineTo(
                x = balloonRight,
                y = balloonTop
            )                     // Right edge of the rectangle
            arcTo(
                rect = Rect(
                    left = balloonRight - cornerRadius * 2f,
                    top = balloonTop,
                    right = balloonRight,
                    bottom = balloonTop + cornerRadius * 2f
                ),
                startAngleDegrees = 0f,
                sweepAngleDegrees = -90f,
                forceMoveTo = false
            )
            lineTo(
                x = arrowRightX,
                y = arrowRightY
            )     //  TopRight edge of the rectangle
            close()
        }
        return Outline.Generic(path)
    }
}
Run Code Online (Sandbox Code Playgroud)

提前致谢。

Phi*_*hov 9

composable不再建议使用构建修饰符,因此此警告不应出现在新的 Compose 版本中。

请注意,您仍然应该小心,不要添加任何视图来查看层次结构,并且仅使用可组合范围来存储/动画数据。


根据 Compose 修饰符指南:

因此,Jetpack Compose 框架开发和库开发应该用于Modifier.composed {}实现组合感知修饰符,并且不应该将修饰符扩展工厂函数声明为@Composable函数本身。

为什么

组合修饰符可以在组合之外创建,在元素之间共享,并声明为顶级常量,这使得它们比只能通过函数@Composable调用创建的修饰符更灵活,并且更容易避免意外地在元素之间共享状态。

在修改器中使用视图有点hacky。修改器应该改变它所应用到的视图的状态,并且不应该影响环境。

您的代码可以使用,@Composable因为它是立即调用的,并且弹出窗口会添加到视图树中,就像它是在调用视图之前添加的一样。

使用composed,内容稍后会被调用,当它开始在渲染之前测量视图的位置时 - 由于此代码不是视图树的一部分,因此您的弹出窗口不会添加到其中。

使用该composed代码是为了让您可以保存状态并remember使用本地值,例如LocalDensity,但不能用于添加视图。

您几乎可以在代码库中做任何您想做的事情,毕竟,您可以消除此警告,但大多数看到您的代码的人不会期望这样的修饰符在视图树中添加视图 - 这就是指南的目的。

我认为实现这样一个功能的预期方式如下(不确定命名,通过):

@Composable
fun BalloonPopupRequester(
    requesterView: @Composable (Modifier) -> Unit,
    popupContent: @Composable BoxScope.() -> Unit
) {
    var anchorOffset by remember { mutableStateOf<LayoutCoordinates?>(null) }
    var showTooltip by remember { mutableStateOf(false) }
    if (showTooltip) {
        BalloonPopup(
            onDismissRequest = {
                showTooltip = false
            },
            content = popupContent,
            anchorCoordinates = anchorOffset
        )
    }
    requesterView(
        Modifier
            .clickable {
                println("clickable")
                showTooltip = true
            }
            .onGloballyPositioned {
                anchorOffset = it
            }
    )
}
Run Code Online (Sandbox Code Playgroud)

用法:

BalloonPopupRequester(
    requesterView = { modifier ->
        Image(
            modifier = modifier,
            painter = painterResource(id = android.R.drawable.ic_secure),
            contentDescription = "Signal Strength"
        )
    },
    popupContent = {
        Text(
            modifier = Modifier.padding(4.dp),
            text = "-30 rssi",
            fontSize = 13.sp
        )
    }
)
Run Code Online (Sandbox Code Playgroud)