Как сделать, чтоб анимация на LazyGrid для item производилась только первый раз?

Должно быть не сильно сложно, но по какой то причине рекомпозиция проходит 2 раза, что мешает добавить условие для выполнения анимиции только первый раз.

В общем хочу сделать так как в этом примере, но я немного изменил его, но суть такая же - https://yasinkacmaz.medium.com/simple-item-animation-with-jetpack-composes-lazygrid-78316992af22

Нужно, чтоб элементы грида появлялись бабл эфектом

Вот такой код

private val dataSet: List<String> = listOf("Item 1", "Item 2", "Item 3", "Item 4", "Item 5")
private val data: List<String> = List(5) { dataSet }.flatten()

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            Test_delete_itTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    Gallery(
                        paddingValues = innerPadding,
                        uiConfig = { data }
                    )
                }
            }
        }
    }
}

@Composable
private fun Gallery(
    paddingValues: PaddingValues,
    uiConfig: () -> List<String>
) {
    val config: List<String> = uiConfig()
    val columns = 2

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(paddingValues)
    ) {
        LazyVerticalGrid(
            columns = GridCells.Fixed(columns),
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.spacedBy(8.dp),
            horizontalArrangement = Arrangement.spacedBy(8.dp),
            content = {
                items(config.size) { idx ->
                    val item: String = config[idx]
                    val (scale, alpha) = scaleAndAlpha(idx, columns)

                    MyItem(
                        modifier = Modifier.graphicsLayer(alpha = alpha, scaleX = scale, scaleY = scale),
                        text = item
                    )
                }
            }
        )
    }
}

@Composable
private fun MyItem(
    modifier: Modifier = Modifier,
    text: String
) {
    Card(
        modifier = modifier.height(150.dp),
        shape = RoundedCornerShape(16.dp),
        elevation = CardDefaults.cardElevation(8.dp),
        colors = CardDefaults.cardColors(
            containerColor = Color.Blue,
        )
    ) {
        Box(
            modifier = Modifier
                .weight(1f)
                .height(150.dp)
                .clip(RoundedCornerShape(16.dp))
        ) {
            Text(
                text = text,
                color = Color.White
            )
        }
    }
}

@Immutable
private enum class State { PLACING, PLACED }

@Immutable
data class ScaleAndAlphaArgs(
    val fromScale: Float,
    val toScale: Float,
    val fromAlpha: Float,
    val toAlpha: Float
)

@OptIn(ExperimentalTransitionApi::class)
@Composable
fun scaleAndAlpha(
    args: ScaleAndAlphaArgs,
    animation: FiniteAnimationSpec<Float>
): Pair<Float, Float> {
    val transitionState = remember { MutableTransitionState(State.PLACING).apply { targetState = State.PLACED } }
    val transition = rememberTransition(transitionState, label = "")
    val alpha by transition.animateFloat(transitionSpec = { animation }, label = "") {
        if (it == State.PLACING) args.fromAlpha else args.toAlpha
    }
    val scale by transition.animateFloat(transitionSpec = { animation }, label = "") {
        if (it == State.PLACING) args.fromScale else args.toScale
    }
    return alpha to scale
}

val scaleAndAlpha: @Composable (idx: Int, columns: Int) -> Pair<Float, Float> = { idx, columns ->
    scaleAndAlpha(
        args = ScaleAndAlphaArgs(2f, 1f, 0f, 1f),
        animation = tween(300, delayMillis = (idx / columns) * 100)
    )
}

Можно попробовать изменить Gallery на вот такую, чтоб отслеживать если элемент уже был показан пользователю

@Composable
private fun Gallery(
    paddingValues: PaddingValues,
    uiConfig: () -> List<String>
) {
    val config: List<String> = uiConfig()
    val columns = 2

    // Remember a set of already animated indices
    val animatedIndices = remember { mutableSetOf<Int>() }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(paddingValues)
    ) {
        LazyVerticalGrid(
            columns = GridCells.Fixed(columns),
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.spacedBy(8.dp),
            horizontalArrangement = Arrangement.spacedBy(8.dp),
            content = {
                items(config.size) { idx ->
                    val item: String = config[idx]

                    // Determine if the item should animate
                    val shouldAnimate = !animatedIndices.contains(idx)

                    // If it should animate, mark it as animated
                    if (shouldAnimate) {
                        animatedIndices.add(idx)
                    }

                    val (scale, alpha) = if (shouldAnimate) {
                        scaleAndAlpha(idx, columns)
                    } else {
                        1f to 1f // No animation
                    }

                    MyItem(
                        modifier = Modifier.graphicsLayer(alpha = alpha, scaleX = scale, scaleY = scale),
                        text = item
                    )
                }
            }
        )
    }
}

Но проблема в том, что рекомпозиция вызывается дважды для каждого элемента вот тут - items(config.size) { idx ->, из за этого элемент появляется на экране как будто нет анимации

Что я пропускаю тут?


Ответы (1 шт):

Автор решения: Sirop4ik

На первой рекомпозиции items лямбды, когда shouldAnimate равно true, вызывается scaleAndAlpha. Это функция Compose, которая рекомпилируется на каждом кадре анимации и возвращает текущие значения scale и alpha при каждой рекомпозиции. Чтобы MyItem обновлялся соответствующим образом, вся лямбда items рекомпилируется при изменении scale и alpha.

Вторая рекомпозиция нежелательна, потому что теперь shouldAnimate установлено в false, и анимация, которая только что началась, полностью пропускается.

Простое решение – вынести scaleAndAlpha и MyItem в отдельный composable элемент, чтобы его рекомпозиции не зависели от shouldAnimate.

    @Composable
    private fun MyAnimatedItem(
        shouldAnimate: Boolean,
        idx: Int,
        columns: Int,
        item: String,
    ) {
        val (scale, alpha) = if (shouldAnimate) {
            scaleAndAlpha(idx, columns)
        } else {
            1f to 1f // No animation
        }
    
        MyItem(
            modifier = Modifier.graphicsLayer(
                alpha = alpha,
                scaleX = scale,
                scaleY = scale,
            ),
            text = item,
        )
    }

Simply called like this (in addition I simplified it to use `itemsIndexed` instead of  `items`):

    itemsIndexed(config) { idx, item ->
        // Determine if the item should animate
        val shouldAnimate = !animatedIndices.contains(idx)

        // If it should animate, mark it as animated
        if (shouldAnimate) {
            animatedIndices.add(idx)
        }

        MyAnimatedItem(shouldAnimate, idx, columns, item)
    }

Теперь рекомпозиции, которые происходят из-за анимации, ограничены MyAnimatedItem, лямбда itemsIndexed не затрагивается и рекомпилируется только тогда, когда элемент прокручивается за пределы области видимости и возвращается обратно. И только тогда shouldAnimate устанавливается в false, как и предполагалось.

→ Ссылка