kotlin Android JetPack组合-了解@可组合作用域

twh00eeo  于 2023-01-05  发布在  Kotlin
关注(0)|答案(2)|浏览(197)

我有点拉我的头发了一段时间,现在,我只是不能掌握的概念,无论有多少教程,我看和代码片段,我读..
我只是想把一个标记图像放在另一个图像的顶部,我点击它。

class MainActivity : ComponentActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    setContent {

        MyLayout() {
            PlaceMarkerOnImage(it)
        }
    }
}

@Composable
private fun MyLayout(
    placeMarker: (Offset) -> Unit
) {
    val painter: Painter = painterResource(id = R.drawable.image)

    Column(Modifier.fillMaxSize()) {

        Box(
            modifier = Modifier.weight(0.95f)
        ) {
            Image(
                contentScale = FillBounds,
                painter = painter,
                contentDescription = "",
                modifier = Modifier.pointerInput(Unit) {
                    detectTapGestures(
                        onTap = {
                            placeMarker(it)
                        }
                    )
                }
            )
        }
        Button(
            onClick = { },
            modifier = Modifier.weight(0.05f),
            shape = MaterialTheme.shapes.small
        ) {
            Text(text = "Edit Mode")
        }

    }
}

@Composable
private fun PlaceMarkerOnImage(offset: Offset) {
    Image(
        painter = painterResource(id = R.drawable.marker),
        contentScale = ContentScale.Crop,
        contentDescription = "",
        modifier = Modifier.offset(offset.x.dp, offset.y.dp)
    )
}
}

但这是错误的,因为我在调用PlaceMarkerOnImage时遇到了可怕的编译错误:@可组合调用只能从@可组合函数的上下文中发生
我不明白...我得到的是被覆盖的onCreate函数不是@Composable,因此不能从它调用@Composable函数,也不能向它添加@Composable注解。
但是我从setContent块调用了两个可组合函数,它调用MyLayout()没有问题,那么为什么调用PlaceMarkerOnImage(Offset)会有问题呢?

k2fxgqgv

k2fxgqgv1#

从机制上讲,你不能在传递给MyLayout的lambda中调用PlaceMarkerOnImage的原因是因为它没有标记为@Composable,因此lambda不被认为是可组合的,然而,这只是把问题推到了几英尺远的地方,因为一旦你做了那个修改,编译器就会抱怨在onTap中调用了placeMarker
这里的脱节是当你使用声明性框架时你是在强制性地思考。
在一个必要的框架内,通过创建UI树来构建UI的状态,然后解释该树以在屏幕上生成UI(传统上与布局和绘制或类似的命名阶段)。每次更改树时,布局和绘制步骤都会重复(通常在下一个绘图框中)。要更改UI,您可以创建新的树元素并将其放置在正确的位置,或者更改树中已有元素的属性。
以上似乎建议您将可组合函数视为生成新内容的函数,当调用这些函数时,将在调用它们的任何外部可组合函数中生成该内容。这不是Compose的工作方式,因为Compose是一个声明性框架,而不是命令式框架。
在声明性框架中,UI是通过将数据转换为用户界面的转换生成的。每当转换观察到的数据发生更改时,转换都会重新运行,并且结果中的任何更改都会反映在UI中。
在声明性框架中,转换描述了UI应该被给予什么一些数据,并且添加、移除或改变UI的唯一方式是通过改变由转换观察到的数据来修改由转换产生的内容。
换句话说,命令式框架是用动词(创建、修改、删除)来描述的。声明式框架是一个名词。也就是说,它描述了UI是什么,而不是如何创建它。当转换改变了对UI是什么的想法时,UI就会改变。不需要描述如何到达那里,这是框架的工作。
转换的编码方式、转换产生的结果、变化的检测方式以及转换的重新执行时间和方式在每个声明性框架中都有所不同。
在组合中,转换是函数,观察到的数据作为参数传递给这些函数。UI由可组合函数的调用控制,并且只能通过调用可组合函数来更改。
变换函数为(大多数情况下)同步执行,调用的结果是UI。在上面的示例中,您在组合完成后在回调函数中调用placeMarker。由于它不是作为组合的一部分调用的,这被编译器标记为错误。2一个可组合函数只能从另一个可组合函数调用,因为结果必须是组合的一部分。单独调用它就像是将两个数字相加,比如a + b,但是不会将结果存储在任何地方。当你调用一个组合函数时,你会说,“这个函数的内容放在这里”,这只有在从另一个组合函数调用时才有意义。因此,编译器检查并报告可组合函数何时在没有意义的上下文中被调用。
请记住,可组合函数可以任意运行多次,并且应该始终从相同的数据生成相同的结果。可以认为,随着数据的每次更改,所有可组合函数都将重新运行,并且在运行后,生成的UI就是您看到的UI。组合实际上并不运行所有可组合函数(出于性能原因),但它是一个很好的心理模型。
最简单的修改就是修改onTap来修改MyLayout正在观察的一些数据,然后基于这些数据,调用或不调用placeMarker。另外,由于可组合函数是名词,而不是动词,因此应该只调用marker。这意味着函数将类似于,

@Composable
private fun MyLayout(
    marker: @Composable (Offset) -> Unit
) {
    val painter: Painter = painterResource(id = R.drawable.image)

    Column(Modifier.fillMaxSize()) {

        Box(
            modifier = Modifier.weight(0.95f)
        ) {
            var showMarker by remember { mutableStateOf(false) }
            var markerOffset by remember { mutableStateOf(Offset.Zero) }
            Image(
                contentScale = FillBounds,
                painter = painter,
                contentDescription = "",
                modifier = Modifier.pointerInput(Unit) {
                    detectTapGestures(
                        onTap = {
                            showMarker = true
                            markerOffset = it
                        }
                    )
                }
            )
            if (showMarker) {
                marker(markerOffset)
            }
        }
        Button(
            onClick = { },
            modifier = Modifier.weight(0.05f),
            shape = MaterialTheme.shapes.small
        ) {
            Text(text = "Edit Mode")
        }

    }
}

一旦您熟悉了这个模型,那么添加一个事件处理程序来删除标记就相当简单了,例如,

...
    onDoubleTap = { showMarker = false },
    ...

在命令式框架中,这是一件非常棘手的事情,因为您需要在标记尚未显示时接收双击,或者在延迟接收事件时,在树的这一部分已被删除后等情况下进行处理。所有这些问题都由Compose运行时为您处理。

iqjalb3h

iqjalb3h2#

PlaceMarkerOnImage不是从setContent调用的,而是从MyLayout内部调用的。如果要将可组合函数作为参数传递给另一个函数,则必须使用@Composable注解该参数:

@Composable
private fun MyLayout(
    placeMarker: @Composable (Offset) -> Unit
)

但这并不能解决你的问题,它只会把它移到onTap,因为detectTapGesturesonTap参数也不接受可组合函数。
你要做的事情是这样的:

setContent {
    var markerOffset by remember { mutableStateOf<Offset?>(null) }

    MyLayout { markerOffset = it }
    
    markerOffset?.let {
        PlaceMarkerOnImage(it)
    }
}

相关问题