返回

Jetpack Compose LazyGrid Item动画只执行一次的解决方案

Android

LazyGrid Item 动画只执行一次的解决方案

在 Jetpack Compose 中使用 LazyGrid 构建界面时,实现 Item 首次出现动画是一个常见的需求。但是,由于 Compose 的重组机制,简单地使用一个标志来判断 Item 是否已经出现过并执行动画,往往会因为重组发生多次而失效。本文将深入分析这个问题,并提供几种有效的解决方案。

问题分析

代码示例中,开发者尝试使用 animatedIndices 集合来记录已播放动画的 Item 索引,以避免重复播放动画。但是,LazyGriditems 构建器会多次调用 Lambda 表达式,导致 animatedIndices 集合被多次更新,动画触发条件失效。

具体来说,Compose 的重组机制会在状态变化时重新执行 Composable 函数。对于 LazyGrid ,当 Item 可见性发生变化或者布局发生变化时,items 构建器内的代码会重新执行,导致 animatedIndices 被清空或重复添加。

解决方案

以下提供几种解决方案来确保 Item 动画只执行一次:

1. 使用 Key

利用 LazyGriditems 构建器的 key 参数,为每个 Item 提供一个唯一的、稳定的标识符。当 key 不变时,Compose 会认为该 Item 保持不变,不会触发不必要的重组,从而保证动画只执行一次。

代码示例:

@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(
                    count = config.size,
                    key = { index -> config[index] } // 使用 Item 数据本身作为 key
                ) { idx ->
                    val item: String = config[idx]
                    val (scale, alpha) = scaleAndAlpha(idx, columns)

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

操作步骤:

  1. items 构建器中添加 key 参数。
  2. key 参数接收一个 Lambda 表达式,该表达式返回一个唯一标识 Item 的值。
  3. 确保 key 返回的值在 Item 的生命周期内保持稳定。例如,可以使用 Item 的数据模型中的唯一 ID 或者 Item 的索引作为 key

原理:
通过为每个 Item 提供唯一的 key, Compose 可以跟踪 Item 的状态,避免因为重组而导致动画重复执行。当 key 不变时, Compose 会认为该 Item 没有发生变化,不会重新创建 Item 的 Composable,从而避免动画重复执行。

安全性建议:

  • key 必须唯一且稳定。 如果 key 不稳定,会导致 Item 状态混乱,可能导致动画错乱或者其他问题。
  • 避免使用可能发生变化的值作为 key 。 例如,如果 Item 数据会更新,则不应该直接使用 Item 数据作为 key ,而是使用一个唯一的、不变的 ID。
  • 当 Item 数据更新时,应该更新 key 值,以触发 Item 的重组和动画。

2. 利用 rememberUpdatedState

rememberUpdatedState 会创建一个对值的引用,这个引用在 recomposition 期间保持不变,但它会始终指向最新的值。结合 LaunchedEffect 可以实现仅在 Item 首次出现时触发动画。

代码示例:

@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 shouldAnimate = remember { mutableStateOf(true) }
                    val currentShouldAnimate by rememberUpdatedState(shouldAnimate.value)
                    val (scale, alpha) = if (currentShouldAnimate) {
                        scaleAndAlpha(idx, columns)
                    } else {
                        1f to 1f
                    }

                    LaunchedEffect(key1 = idx) {
                      if (shouldAnimate.value) {
                        shouldAnimate.value = false
                      }
                    }

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

操作步骤:

  1. 使用remember { mutableStateOf(true) } 为每个 Item 创建一个 shouldAnimate 的状态,表示是否应该播放动画。
  2. 使用 rememberUpdatedState 创建一个 currentShouldAnimate 变量,用来跟踪最新的 shouldAnimate 值。
  3. LaunchedEffect 块中使用 key1 = idx 保证 LaunchedEffect 仅在第一次 index 值可见时执行,并在 LaunchedEffect 中设置 shouldAnimatefalse
  4. 只有当 currentShouldAnimatetrue 时,才播放动画。

原理:
rememberUpdatedState 会创建一个对 shouldAnimate 值的引用,这个引用在 Composable 函数的 recomposition 之间保持不变, 但它会指向shouldAnimate 最新值。 这使得 LaunchedEffect 在首次创建和索引更新时捕获shouldAnimate 的正确状态, 然后可以在 LaunchedEffect 中安全地改变 shouldAnimate 值, 且不会触发不必要的重组,LaunchedEffectkey 也保证了这个副作用只会运行一次。这样,就保证了动画只在 Item 首次出现时执行。

安全性建议:

  • LaunchedEffect 中的代码会在 Composable 第一次 Composition 时执行,或者当 key 变化时执行。 注意控制好 key ,避免不必要的副作用。
  • LaunchedEffect 可能会因为其 key 值在 Composition 过程中发生改变,而在下一次 Composition 中再次运行。确保副作用的幂等性,或者做好相应的取消和重新执行的处理。
  • 由于副作用是在异步执行,需要确保 shouldAnimate 在合适的时机更新,避免出现动画错乱或者其他问题。

总结

以上两种方案都可以有效解决 LazyGrid Item 动画只执行一次的问题。选择哪种方案取决于具体的需求和场景。如果 Item 的数据模型中有唯一的 ID,或者可以使用索引作为唯一标识符,则使用 key 参数的方案更简单直接。如果需要更精细的控制动画的执行时机,或者 Item 没有稳定的唯一标识符,则可以使用 rememberUpdatedStateLaunchedEffect 结合的方案。

通过合理地利用 Jetpack Compose 提供的工具,可以构建出高效流畅的用户界面,并避免因为重组机制导致的一些问题。

相关资源