我正试图使用Jetpack Compose在我的Android应用程序中创建一个天空视图。我想在具有固定height
的Card
中显示它。在晚上,卡片背景变成了深蓝色,我想让一些闪烁的星星在天空中散开。
为了创建星星闪烁的动画,我使用了一个InfiniteTransition
对象和一个带有animateFloat
的scale
属性,并将其应用于几个Icon
s。这些Icon
s在BoxWithConstraints
内部创建,然后使用for
循环随机展开。我使用的完整代码如下所示:
@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
)
}
}
}
}
这个代码的问题是BoxWithConstraints
的作用域是连续重新组合的,所以我很快就从屏幕上看到了很多点的出现和消失。我希望作用域只运行一次,这样第一次创建的点就会使用scale
属性动画闪烁。我怎样才能做到这一点?
为了实现目标,你应该寻找最少的重新组合,而不是连续的重新组合。
Compose有三个阶段。组成,布局和绘图,在官方文件中解释。当您使用lambda时,您可以将状态读取从合成推迟到布局或绘制阶段。
如果使用Modifier.scale()
或Modifier.offset()
,则会调用以上三个阶段。如果使用Modifier.graphicsLayer{scale}
或Modifier.offset{}
,则将状态读取推迟到布局阶段。最棒的是,如果您使用Canvas
,这是一个隐藏着Modifier.drawBehind{}
的Spacer
,您可以将状态读取推迟到绘制阶段,如下面的示例所示,并且您只需要一个构图而不是在每帧上重新构图即可实现目标。
例如,来自官方文件
// Here, assume animateColorBetween() is a function that swaps between
// two colors
val color by animateColorBetween(Color.Cyan, Color.Magenta)
Box(Modifier.fillMaxSize().background(color))
此处框的背景色在两种颜色之间快速切换颜色。因此,这种状态变化非常频繁。可堆肥的然后在后台修改器中读取该状态。因此,盒子必须在每帧上重新合成,因为每帧的颜色都在变化框架
为了改进这一点,我们可以使用基于lambda的修饰符——在这种情况下,drawBehind。这意味着颜色状态只在绘制过程中读取阶段因此,Compose可以跳过合成和布局阶段完全–当颜色发生变化时,Compose直接进入绘图阶段
val color by animateColorBetween(Color.Cyan, Color.Magenta)
Box(
Modifier
.fillMaxSize()
.drawBehind {
drawRect(color)
}
)
以及如何实现您的结果
@Composable
fun NightSkyCard2() {
Card(
modifier = Modifier
.height(200.dp)
.fillMaxWidth(),
elevation = 2.dp,
shape = RoundedCornerShape(20.dp),
backgroundColor = Color.Blue
) {
val infiniteTransition = rememberInfiniteTransition()
val scale by infiniteTransition.animateFloat(
initialValue = 1f,
targetValue = 0f,
animationSpec = infiniteRepeatable(
animation = tween(1000),
repeatMode = RepeatMode.Reverse
)
)
val stars = remember { mutableStateListOf<Star>() }
BoxWithConstraints(
modifier = Modifier
.fillMaxSize()
.background(Color.Blue)
) {
SideEffect {
println("🔥 Recomposing")
}
LaunchedEffect(key1 = Unit) {
repeat(20) {
stars.add(
Star(
Random.nextInt(2, 5).toFloat(),
Random.nextInt(0, constraints.maxWidth).toFloat(),
Random.nextInt(10, constraints.maxHeight).toFloat()
)
)
}
}
Canvas(modifier = Modifier.fillMaxSize()) {
if(stars.size == 20){
stars.forEach { star ->
drawCircle(
Color.White,
center = Offset(star.xPos, star.yPos),
radius = star.radius *(scale)
)
}
}
}
}
}
}
@Immutable
data class Star(val radius: Float, val xPos: Float, val yPos: Float)
一个解决方案是将代码包装在LaunchedEffect中,以便动画运行一次:
@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) {
var size by remember { mutableStateOf(0) }
var start by remember { mutableStateOf(0) }
var top by remember { mutableStateOf(0) }
LaunchedEffect(key1 = Unit) {
size = Random.nextInt(3, 5)
start = Random.nextInt(0, maxWidth.value.toInt())
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
)
}
}
}
}
然后你会得到21颗闪烁的星星。