You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何消除列表元素轮播动画中的闪烁效果?

问题描述

需要实现一个支持n个元素的无限轮播动画,动画序列为AB→BC→CA→AB……循环往复。使用Jetpack Compose实现时,元素过渡存在明显闪烁,破坏了动画流畅性,寻求消除闪烁的方案或替代实现方式。

当前实现代码
@Composable
fun FollowEventCardHome() {
    val data = "{...}" 
    val eventData = Gson().fromJson(data, EventData::class.java)
    var currentEvent by remember { mutableStateOf(eventData.events?.getOrNull(0)) }
    var nextEvent by remember { mutableStateOf(eventData.events?.getOrNull(1)) }
    var currentIndex by remember { mutableIntStateOf(0) }
    var isAnimate by remember { mutableStateOf(true) }

    val configuration = LocalConfiguration.current
    val cardWidth = configuration.screenWidthDp.dp - 55.dp

    if (isAnimate) {
        StartEndInfo(
            nextEvent = nextEvent,
            currentEvent = currentEvent,
            isAnimate = true,
            isAnimationFinished = {
                isAnimate = false
                if (eventData.events?.size!! > 1) {
                    if (currentIndex == eventData.events?.size!! - 1) {
                        currentIndex = 0
                    } else {
                        currentIndex++
                    }
                }
                currentEvent = eventData.events?.getOrNull(currentIndex)
                nextEvent = if (currentIndex < eventData.events?.size!!) {
                    eventData.events?.getOrNull(0)
                } else {
                    eventData.events?.getOrNull(currentIndex + 1)
                }
            }
        )
    }

    if(!isAnimate){
        StartEndInfo(
            nextEvent = nextEvent,
            currentEvent = currentEvent,
            isAnimate = true,
            isAnimationFinished = {
                isAnimate = true
                if (eventData.events?.size!! > 1) {
                    if (currentIndex == eventData.events?.size!! - 1) {
                        currentIndex = 0
                    } else {
                        currentIndex++
                    }
                }
                currentEvent = eventData.events?.getOrNull(currentIndex)
                nextEvent = if (currentIndex < eventData.events?.size!!) {
                    eventData.events?.getOrNull(0)
                } else {
                    eventData.events?.getOrNull(currentIndex + 1)
                }
            }
        )
    }
}

@Composable
fun StartEndInfo(
    nextEvent: Event? = null,
    currentEvent: Event? = null,
    isAnimate: Boolean,
    isAnimationFinished: () -> Unit
) {
    Row(
        modifier = Modifier
            .padding(
                start = 16.dp,
                bottom = 16.dp
            )
            .fillMaxWidth()
    ) {
        Column(modifier = Modifier.fillMaxSize()) {
            StartEndWithProgress(
                nextEvent = nextEvent,
                currentEvent = currentEvent,
                isAnimate = isAnimate,
                isAnimationFinished = isAnimationFinished
            )
        }
    }
}

@Composable
fun StartEndWithProgress(
    nextEvent: Event?,
    currentEvent: Event?,
    isAnimate: Boolean,
    isAnimationFinished: () -> Unit
) {
    val scalePreviousEvent = remember { Animatable(1f) }
    val scaleCurrentEvent = remember { Animatable(0f ) }

    LaunchedEffect(isAnimate) {
        scalePreviousEvent.animateTo(
            targetValue = 0f,
            animationSpec = tween(durationMillis = 4000)
        )
    }

    LaunchedEffect(key1 = isAnimate) {
        scaleCurrentEvent.animateTo(
            targetValue = 1f,
            animationSpec = tween(durationMillis = 4000)
        )
    }

    if(isAnimate && scalePreviousEvent.value == 0f ||  !isAnimate && scalePreviousEvent.value == 1f){
        isAnimationFinished()
    }

    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = 30.dp)
            .padding(top = 150.dp),
        horizontalArrangement = Arrangement.SpaceBetween,
        verticalAlignment = Alignment.CenterVertically
    ) {
        Box {
            currentEvent?.start?.locationCode.let {
                Text(
                    text = currentEvent?.start?.locationCode ?: "",
                    fontSize = 20.sp,
                    modifier = Modifier.graphicsLayer {
                        scaleX = 1f
                        scaleY = if (isAnimate) {
                            scaleCurrentEvent.value
                        } else 1f
                        transformOrigin = TransformOrigin(0.5f, 1f)
                    }
                )
                Text(
                    text = nextEvent?.start?.locationCode ?: "",
                    fontSize = 20.sp,
                    modifier = Modifier.graphicsLayer {
                        scaleX = 1f
                        scaleY = if (isAnimate) {
                            scalePreviousEvent.value
                        } else 0f
                        transformOrigin = TransformOrigin(0.5f, 0f)
                    }
                )
            }
        }
        Box {
            currentEvent?.end?.locationCode.let {
                Text(
                    text = currentEvent?.end?.locationCode ?: "",
                    fontSize = 20.sp,
                    modifier = Modifier.graphicsLayer {
                        scaleX = 1f
                        scaleY = if (isAnimate) { scaleCurrentEvent.value } else 1f
                        transformOrigin = TransformOrigin(0.5f, 1f)
                    }
                )
            }
            Text(
                text = nextEvent?.end?.locationCode ?: "",
                fontSize = 20.sp,
                modifier = Modifier.graphicsLayer {
                    scaleX = 1f
                    scaleY = if (isAnimate) {
                        scalePreviousEvent.value
                    } else 0f
                    transformOrigin = TransformOrigin(0.5f, 0f)
                }
            )
        }
    }
}
问题分析
  1. 状态管理混乱:通过isAnimate的两个分支重复渲染StartEndInfo,导致组件频繁重组,触发不必要的状态重置,是闪烁的核心原因。
  2. 动画结束判断不严谨:用scalePreviousEvent.value == 0f这种浮点值精确匹配判断动画结束,由于浮点精度问题,可能导致触发时机异常,引发状态突变。
  3. nextEvent计算逻辑错误:当前代码中nextEvent的取值逻辑混乱,无法正确生成AB→BC→CA的序列。
  4. 动画同步问题:两个独立的Animatable没有做同步控制,可能导致缩放动画不同步,出现视觉闪烁。
解决方案与优化代码

优化思路

  • 用单个索引控制当前显示的元素对,通过取模运算实现无限循环。
  • 使用animateFloatAsState简化动画管理,避免手动操作Animatable的同步问题。
  • 在动画结束的回调中更新状态,确保状态切换时机准确。
  • 移除冗余的组件分支渲染,统一通过状态控制动画状态。

优化后的代码

@Composable
fun FollowEventCardHome() {
    val data = "{...}" 
    val eventData = Gson().fromJson(data, EventData::class.java)
    val events = eventData.events ?: emptyList()
    if (events.size < 2) return // 元素不足时不显示动画

    // 当前元素对的索引:0→AB,1→BC,2→CA,循环
    var currentPairIndex by remember { mutableIntStateOf(0) }
    // 控制动画播放状态
    var isAnimating by remember { mutableStateOf(true) }

    // 计算当前和下一个元素
    val currentEvent = events[currentPairIndex]
    val nextEvent = events[(currentPairIndex + 1) % events.size]

    // 动画进度:0f→初始状态,1f→动画完成
    val animationProgress by animateFloatAsState(
        targetValue = if (isAnimating) 1f else 0f,
        animationSpec = tween(durationMillis = 4000),
        finishedListener = {
            if (isAnimating) {
                // 动画完成后切换到下一组元素,并重置动画状态
                currentPairIndex = (currentPairIndex + 1) % events.size
                isAnimating = false
                // 延迟启动下一次动画(可选,根据需求调整)
                kotlinx.coroutines.delay(500)
                isAnimating = true
            }
        }
    )

    StartEndInfo(currentEvent = currentEvent, nextEvent = nextEvent, animationProgress = animationProgress)
}

@Composable
fun StartEndInfo(
    currentEvent: Event,
    nextEvent: Event,
    animationProgress: Float
) {
    Row(
        modifier = Modifier
            .padding(start = 16.dp, bottom = 16.dp)
            .fillMaxWidth()
    ) {
        Column(modifier = Modifier.fillMaxSize()) {
            StartEndWithProgress(currentEvent, nextEvent, animationProgress)
        }
    }
}

@Composable
fun StartEndWithProgress(
    currentEvent: Event,
    nextEvent: Event,
    animationProgress: Float
) {
    // 计算缩放值:当前元素从0→1,上一个元素从1→0
    val currentScale = animationProgress
    val previousScale = 1f - animationProgress

    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = 30.dp, top = 150.dp),
        horizontalArrangement = Arrangement.SpaceBetween,
        verticalAlignment = Alignment.CenterVertically
    ) {
        Box {
            // 上一组的起点(逐渐缩小消失)
            Text(
                text = currentEvent.start.locationCode,
                fontSize = 20.sp,
                modifier = Modifier.graphicsLayer {
                    scaleY = previousScale
                    transformOrigin = TransformOrigin(0.5f, 0f)
                }
            )
            // 当前组的起点(逐渐放大显示)
            Text(
                text = nextEvent.start.locationCode,
                fontSize = 20.sp,
                modifier = Modifier.graphicsLayer {
                    scaleY = currentScale
                    transformOrigin = TransformOrigin(0.5f, 1f)
                }
            )
        }
        Box {
            // 上一组的终点(逐渐缩小消失)
            Text(
                text = currentEvent.end.locationCode,
                fontSize = 20.sp,
                modifier = Modifier.graphicsLayer {
                    scaleY = previousScale
                    transformOrigin = TransformOrigin(0.5f, 0f)
                }
            )
            // 当前组的终点(逐渐放大显示)
            Text(
                text = nextEvent.end.locationCode,
                fontSize = 20.sp,
                modifier = Modifier.graphicsLayer {
                    scaleY = currentScale
                    transformOrigin = TransformOrigin(0.5f, 1f)
                }
            )
        }
    }
}

优化点说明

  1. 简化状态管理:用currentPairIndex直接控制当前显示的元素对,通过(currentPairIndex +1) % events.size实现无限循环,逻辑清晰。
  2. 动画控制更稳定:使用animateFloatAsState替代手动Animatable,自动处理动画的启动、结束和状态更新,避免同步问题。
  3. 准确的动画结束触发:通过finishedListener回调处理状态切换,避免浮点值精确匹配的问题。
  4. 移除冗余重组:只渲染一个StartEndInfo组件,通过animationProgress控制动画状态,减少不必要的组件销毁和重建,彻底消除闪烁。

内容的提问来源于stack exchange,提问作者pepperlove

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 15:54:57