我已经为Modifier创建了一个扩展函数,当用户点击它时,它会显示一个Balloon弹出窗口。我使用@Composable标签来实现这个函数,因为我将在Balloon中显示的内容也是@Composable的。但是编译器给了我下面的警告:
- 修饰符工厂函数不应标记为@Composable,而应使用composed *
我还应用了建议的更改,但当用户单击视图时,气球根本不显示。
我的问题是:
1.为什么 * 修改器工厂函数不应该标记为@Composable*?
1.对于这样的扩展函数,使用@Composable和composed {...}有什么区别?因为目前为止,我还没有看到使用@Composable标签的任何缺点
1.为什么气球没有显示,即使代码在调试代码时也会传递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
}
}
字符串
之后:
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
}
}
型
这就是我如何调用扩展函数;
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"
)
型
这是在扩展函数中使用的PaddonPopup类;
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)
}
}
型
先谢了。
1条答案
按热度按时间iaqfqrcu1#
不再建议使用
composable
构建修饰符,因此此警告不应出现在新的Compose版本中。请注意,您仍然应该小心,不要向视图层次结构添加任何视图,并且只使用可组合范围来存储/动画数据。
根据“合成”修改器准则:
因此,Jetpack Compose框架开发和Library开发SHOULD使用
Modifier.composed {}
来实现组合感知修饰符,而SHOULD NOT将修饰符扩展工厂函数声明为@Composable
函数本身。为什么
复合修饰符可以在复合之外创建,在元素之间共享,并声明为顶级常量,使它们比只能通过
@Composable
函数调用创建的修饰符更灵活,更容易避免意外地在元素之间共享状态。在修改器中使用视图是一种很奇怪的做法,修改器应该改变它所应用的视图的状态,而不应该影响环境。
您的代码可以与
@Composable
一起工作,因为它是即时调用的,并且弹出窗口被添加到视图树中,就像它被添加到调用视图之前一样。对于
composed
,当它在渲染之前开始测量视图的位置时,内容会在稍后被调用-因为这段代码不是视图树的一部分,所以你的弹出窗口不会被添加到其中。使用
composed
代码是为了让您可以使用remember
保存状态,也可以使用本地值(如LocalDensity
),但不能用于添加视图。你可以在你的代码库中做任何你想做的事情,毕竟,你可以让这个警告静音,但是大多数看到你的代码的人不会想到这样的修饰符在视图树中添加一个视图--这就是指南的目的。
我认为实现这样一个功能的预期方式如下(不确定命名,通过):
字符串
使用方法:
型