Jetpack Compose 中的持续重组

Pio*_*per 5 kotlin android-jetpack-compose jetpack-compose-animation compose-recomposition

我正在尝试使用 Jetpack Compose 在我的 Android 应用程序中创建天空视图。我想将它显示在Card带有固定的height. 在晚上,卡片背景变成深蓝色,我希望天空中散布一些闪烁的星星。

为了创建星星闪烁动画,我使用了一个InfiniteTransition对象和一个应用于多个s 的scale属性。这些s 在 a 内部创建,然后使用循环随机传播。我正在使用的完整代码如下所示:animateFloatIconIconBoxWithConstraintsfor

@Composable
fun NightSkyCard() {
    Card(
        modifier = Modifier
            .height(200.dp)
            .fillMaxWidth(),
        elevation = 2.dp,
        shape = RoundedCornerShape(20.dp),
        backgroundColor = DarkBlue
    ) {
        val infiniteTransition = rememberInfiniteTransition()
        val scale by infiniteTransition.animateFloat(
            initialValue = 1f,
            targetValue = 0f,
            animationSpec = infiniteRepeatable(
                animation = tween(1000),
                repeatMode = RepeatMode.Reverse
            )
        )
        
        BoxWithConstraints(
            modifier = Modifier.fillMaxSize()
        ) {
            for (n in 0..20) {
                val size = Random.nextInt(3, 5)
                val start = Random.nextInt(0, maxWidth.value.toInt())
                val top = Random.nextInt(10, maxHeight.value.toInt())
                
                Icon(
                    imageVector = Icons.Filled.Circle,
                    contentDescription = null,
                    modifier = Modifier
                        .padding(start = start.dp, top = top.dp)
                        .size(size.dp)
                        .scale(scale),
                    tint = Color.White
                )
            }
            
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这段代码的问题是 的BoxWithConstraints范围不断重组,所以我得到很多点从屏幕上快速出现和消失。我希望范围只运行一次,以便第一次创建的点会使用scale属性动画闪烁。我怎样才能做到这一点?

Thr*_*ian 7

您应该寻找最少量的重组来实现您的目标,而不是持续重组。

\n

Compose 有 3 个阶段。构图、布局和绘图,官方文档中有解释。当您使用 lambda 时,您会将状态读取从组合阶段推迟到布局或绘制阶段。

\n

如果使用Modifier.scale()Modifier.offset()以上三个阶段都被调用。如果您使用Modifier.graphicsLayer{scale}Modifier.offset{}推迟状态读取到布局阶段。最好的部分是,如果您使用Canvas,这是一个在引擎盖下的Spacerwith ,您可以推迟状态读取到绘制阶段,如下例所示,并且您只需使用 1 个构图即可实现目标,而不是在每一帧上重新构图。Modifier.drawBehind{}

\n

例如来自官方文档

\n
// Here, assume animateColorBetween() is a function that swaps between\n// two colors\nval color by animateColorBetween(Color.Cyan, Color.Magenta)\n\nBox(Modifier.fillMaxSize().background(color))\n
Run Code Online (Sandbox Code Playgroud)\n
\n

这里盒子的背景颜色在两种颜色之间快速切换。因此,这种状态变化非常频繁。然后可组合项在后台修改器中读取此状态。因此,框\n必须在每一帧上重新组合,因为颜色在每一帧上\n都在变化。

\n

为了改善这一点,我们可以使用基于 lambda 的修饰符\xe2\x80\x93(在本例中为\ndrawBehind)。这意味着颜色状态仅在绘制阶段读取。因此,Compose 可以完全跳过合成和布局阶段\n\n\xe2\x80\x93,当颜色发生变化时,Compose 直接进入绘制\n阶段。

\n
\n
val color by animateColorBetween(Color.Cyan, Color.Magenta)\nBox(\n   Modifier\n      .fillMaxSize()\n      .drawBehind {\n         drawRect(color)\n      }\n)\n
Run Code Online (Sandbox Code Playgroud)\n

以及如何实现您的成果

\n
@Composable\nfun NightSkyCard2() {\n    Card(\n        modifier = Modifier\n            .height(200.dp)\n            .fillMaxWidth(),\n        elevation = 2.dp,\n        shape = RoundedCornerShape(20.dp),\n        backgroundColor = Color.Blue\n    ) {\n        val infiniteTransition = rememberInfiniteTransition()\n        val scale by infiniteTransition.animateFloat(\n            initialValue = 1f,\n            targetValue = 0f,\n            animationSpec = infiniteRepeatable(\n                animation = tween(1000),\n                repeatMode = RepeatMode.Reverse\n            )\n        )\n\n        val stars = remember { mutableStateListOf<Star>() }\n\n\n        BoxWithConstraints(\n            modifier = Modifier\n                .fillMaxSize()\n                .background(Color.Blue)\n        ) {\n\n            SideEffect {\n                println(" Recomposing")\n            }\n            \n            LaunchedEffect(key1 = Unit) {\n                repeat(20) {\n                    stars.add(\n                        Star(\n                            Random.nextInt(2, 5).toFloat(),\n                            Random.nextInt(0, constraints.maxWidth).toFloat(),\n                            Random.nextInt(10, constraints.maxHeight).toFloat()\n                        )\n                    )\n                }\n            }\n            \n            Canvas(modifier = Modifier.fillMaxSize()) {\n               if(stars.size == 20){\n                   stars.forEach { star ->\n                       drawCircle(\n                           Color.White,\n                           center = Offset(star.xPos, star.yPos),\n                           radius = star.radius *(scale)\n                       )\n                   }\n               }\n            }\n        }\n    }\n}\n\n@Immutable\ndata class Star(val radius: Float, val xPos: Float, val yPos: Float)\n
Run Code Online (Sandbox Code Playgroud)\n