Непрерывная рекомпозиция в Jetpack Compose
Я пытаюсь создать вид неба в своем приложении для Android, используя Jetpack Compose. Я хочу отобразить его внутриCard
с фиксированнымheight
. Ночью фон карты становится темно-синим, и я бы хотел, чтобы по небу были разбросаны мигающие звезды.
Чтобы создать анимацию мерцания звезд, я используюInfiniteTransition
объект и недвижимость сanimateFloat
что я применяю к нескольким s. ТеIcon
s создаются внутри a , чтобы распространяться затем случайным образом с помощью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
анимация свойств. Как я мог этого добиться?
2 ответа
Вместо непрерывной рекомпозиции вы должны искать наименьшее количество рекомпозиций для достижения своей цели.
Compose состоит из 3 фаз. Композиция, макет и отрисовка, поясняются в официальном документе . Когда вы используете лямбду, вы откладываете чтение состояния от композиции до фазы макета или рисования.
Если вы используетеModifier.scale()
илиModifier.offset()
обе из трех фаз выше называются. Если вы используетеModifier.graphicsLayer{scale}
илиModifier.offset{}
вы откладываете чтение состояния до фазы компоновки. И самое приятное, если вы используетеCanvas
, который являетсяSpacer
сModifier.drawBehind{}
под капотом вы откладываете чтение состояния до фазы рисования, как в примере ниже, и вы достигаете своей цели только с 1 композицией вместо перекомпоновки на каждом кадре.
Например, из официального документа
// Here, assume animateColorBetween() is a function that swaps between
// two colors
val color by animateColorBetween(Color.Cyan, Color.Magenta)
Box(Modifier.fillMaxSize().background(color))
Здесь цвет фона окна быстро переключается между двумя цветами. Таким образом, это состояние меняется очень часто. Затем компонуемый считывает это состояние в модификаторе фона. В результате коробка должна перекомпоновываться в каждом кадре, поскольку цвет меняется в каждом кадре.
Чтобы улучшить это, мы можем использовать модификатор на основе лямбда — в данном случае, 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 мигающую звезду.