Непрерывная рекомпозиция в Jetpack Compose

Я пытаюсь создать вид неба в своем приложении для Android, используя Jetpack Compose. Я хочу отобразить его внутриCardс фиксированнымheight. Ночью фон карты становится темно-синим, и я бы хотел, чтобы по небу были разбросаны мигающие звезды.

Чтобы создать анимацию мерцания звезд, я используюInfiniteTransitionобъект и недвижимость сanimateFloatчто я применяю к нескольким s. ТеIcons создаются внутри 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 мигающую звезду.