Android Compose中的动画与队列处理:让你的应用动起来!

587 阅读7分钟

嗨,朋友们!今天我要和大家聊聊Android Compose中一个超酷的话题——如何用动画和队列让你的应用界面变得生动有趣。这次我们要围绕两段代码展开:AnimatedQueueSimpleQueue。别担心,我会用最接地气的语言把它们讲明白,保证你不仅能看懂,还会觉得有点意思!

开篇先看 Gif 效果(吐槽掘金上传视频太麻烦),动画的配置完全自定义,实现不同的效果关键是需求和你的想象力!

IMG_0899.gif

背景:为什么需要动画和队列?

想象一下,你在看直播,每当有人进入房间,屏幕上就会弹出一个小提示:“XXX进入了房间”。如果同时进来好几个人,这些提示不会一股脑儿全挤在屏幕上,而是像排队一样,一个接一个地出现。这种“有条不紊”的效果,就是队列的功劳。

再比如,直播间顶部经常会飘过一些特效通知,比如“XXX送了一个火箭!”这些通知不仅要按顺序显示,还要带点动画效果,比如从右边滑进来,再从左边滑出去,这样看起来才够炫酷。

今天的主角AnimatedQueueSimpleQueue就是实现这种效果的利器。一个带动画,一个不带动画,但都能帮你按顺序处理和展示内容。接下来,我们就来拆解它们的代码,看看是怎么工作的,顺便聊聊用到的几个Compose神器。

代码速览

先简单认识一下我们的两个主角:

  • AnimatedQueue:带动画的队列组件,能按顺序显示内容,还支持在显示前后跑一些任务(比如加载数据),并且有进入和退出动画。
  • SimpleQueue:无动画的队列组件,功能类似,但去掉了动画部分,适合简单粗暴的场景。

它们都用了一个叫SnapshotStateList的东西来存待显示的内容,像个智能小本子,记下所有任务,随时准备按顺序处理。

核心API:Compose的四件套

在深入代码之前,我们先来认识几个关键的Compose API,它们是这段代码的灵魂:

1. LaunchedEffect :随时待命的调度员

LaunchedEffect就像一个警觉的调度员,能在Compose里启动协程。它会盯着你指定的“信号”(比如队列大小或动画状态),一旦信号变了,它就立刻开工。在我们的代码里,它负责监控队列有没有新任务,以及当前有没有任务在忙,确保一切按部就班。

2. rememberCoroutineScope() :任务指挥中心

这个API提供了一个协程作用域,专门用来在Compose里跑异步任务。它跟当前的Composable绑定在一起,像个指挥中心,负责调度前置任务(比如下载资源)。简单来说,它让你的异步操作既安全又高效。

3. SnapshotStateList :会喊人的任务清单

SnapshotStateList是一个会自动通知UI更新的列表。往里面加东西或者删东西时,它会大喊一声:“嘿,界面该更新啦!”在我们的代码里,它用来存待显示的内容队列,保证UI随时跟得上变化。

4. AnimatedVisibility :舞台幕布大师

AnimatedVisibility是动画界的明星,能让内容在出现和消失时带上特效,比如淡入淡出、放大缩小。在AnimatedQueue里,它负责让每个项目的登场和谢幕都优雅无比。

AnimatedQueue :动画队列的魔法

好了,认识了工具,我们来看看AnimatedQueue是怎么把这些组合起来,变成一个有动画的队列系统的。

功能亮点

  • 按顺序显示:从队列里一个一个取出内容展示。
  • 动画加持:内容出现时可以淡入+展开,消失时淡出+缩小(默认效果,可自定义)。
  • 前置任务:显示之前可以跑个异步任务,比如检查数据是否就绪。
  • 灵活回调:提供进入动画和退出动画完成后的钩子,方便外部接手。

代码拆解

/**
 * 通用动画队列组件,负责按顺序显示内容
 *
 * @param queue 待显示的内容队列
 * @param modifier 修饰符
 * @param enter 进入动画
 * @param exit 退出动画
 * @param preTaskTimeoutMillis 前置任务超时时间
 * @param onPreTask 前置任务(返回 true 表示继续显示,false 表示跳过)
 * @param onEnterAnimationComplete 进入动画完成回调
 * @param onExitAnimationComplete 退出动画完成回调
 * @param content 内容展示的 Composable,接收结束回调
 */
@Composable
fun<T>AnimatedQueue(
    queue: SnapshotStateList<T>,
    modifier: Modifier = Modifier,
    enter: EnterTransition = fadeIn() + expandIn(),
    exit: ExitTransition = fadeOut() + shrinkOut(),
    preTaskTimeoutMillis: Long = TIME_MILLIS,
    onPreTask: suspend (item: T) -> Boolean = { true },
    onEnterAnimationComplete: () -> Unit = {},
    onExitAnimationComplete: () -> Unit = {},
    content: @Composable (item: T, onDisplayEnd: () -> Unit) -> Unit,
) {
    var currentItem by remember { mutableStateOf<T?>(null) } // 当前显示的内容
    val visibleState = remember { MutableTransitionState(false) } // 动画开关
    var isAnimating by remember { mutableStateOf(false) } // 动画进行中标记
    val scope = rememberCoroutineScope() // 协程作用域

    // 监听队列和动画状态
    LaunchedEffect(queue.size, isAnimating) {
        if (queue.isNotEmpty() && !isAnimating) {
            try {
                val item = queue.removeAt(0)
                val shouldProceed = withContext(scope.coroutineContext) {
                    withTimeout(preTaskTimeoutMillis) { onPreTask(item) }
                }
                if (shouldProceed) {
                    currentItem = item
                    visibleState.targetState = true// 触发进入动画
                    isAnimating = true
                } else {
                    isAnimating = false
                }
            } catch (e: Exception) {
                isAnimating = false
                handleException(e, currentItem)
            }
        }
    }

    // 监听动画完成
    LaunchedEffect(visibleState.currentState, visibleState.isIdle) {
        if (visibleState.isIdle) {
            if (visibleState.currentState) {
                onEnterAnimationComplete() // 进入动画结束
            } else {
                currentItem = null
                isAnimating = false// 退出动画结束,重置
            }
        }
    }

    currentItem?.let { item ->
        AnimatedVisibility(
            visibleState = visibleState, modifier = modifier, enter = enter, exit = exit
        ) {
            content(item) {
                visibleState.targetState = false// 触发退出动画
                onExitAnimationComplete()
            }
        }
    }
}

工作流程

  1. 盯着队列LaunchedEffect盯着队列大小和动画状态。只要队列有货且空闲(!isAnimating),就立刻开工。
  2. 干前置活儿:从队列里拿出一个项目,跑onPreTask(有20秒超时)。如果任务通过(返回true),就准备展示;否则跳过。
  3. 开演:把项目塞进currentItem,把visibleState.targetState设为true,触发AnimatedVisibility的进入动画。
  4. 谢幕:动画跑完后(通过另一个LaunchedEffect监听),如果内容还在舞台上,就等外部调用onDisplayEnd触发退出动画;退出动画结束后,清空舞台,准备下一场。
  5. 异常处理:如果中途出错(比如超时),handleException会记录日志,打印堆栈,保持程序不崩。

使用场景

  • 直播间用户进入:每次有人进房间,把用户名加到队列里,一个个显示“XXX进入了房间”,带点淡入淡出的效果,看着就舒服。
  • 顶部特效通知:比如“XXX送了一个火箭”,从右边滑进来,停留几秒,再滑出去,酷炫又不乱。

我写了一个 Demo 工程让大家参考,复制代码到你的项目里打开预览就能看到效果!当然你不可能懒到 UserEnterInfo 都要我给你代码吧?🐶

@Preview(locale = "en")
@Composable
fun UserEnterRoomWidgetPreview() {
    // 用户队列
    val userQueue = remember { mutableStateListOf<UserEnterInfo>() }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.White)
    ) {
        Spacer(Modifier.height(40.dp))
        Button(onClick = {
            // 模拟用户进入,添加到队列
            userQueue.add(UserEnterInfo("User ${(0..100).random()}", "", "", "", ""))
        }) {
            Text("模拟用户进入")
        }

        val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
        AnimatedQueue(
            queue = userQueue,
            onPreTask = {
               
                true
            },
            onEnterAnimationComplete = {
             
            },
            onExitAnimationComplete = {
               
            },
            // 尝试自己更改动画效果!
            enter = slideInHorizontally(
                animationSpec = tween(500),
                initialOffsetX = { if (isRtl) -it else it }),
            exit = slideOutHorizontally(
                animationSpec = tween(500),
                targetOffsetX = { if (isRtl) it else -it })
        ) { user, onDisplayEnd ->
            // 自定义显示逻辑,可以:
            // 1. 根据用户属性设置不同显示时长
            LaunchedEffect(user) {
                delay(1000L)
                onDisplayEnd()
            }

            // 2. 或者通过按钮点击结束
            UserEnterRoomWidget(
                userName = user.userName,
                avatarUrl = user.avatarUrl,
                vipUrl = user.vipUrl,
                wealthUrl = user.wealthUrl,
                charmUrl = user.charmUrl,
//                    onCloseClick = onDisplayEnd,
                // 其他参数...
            )
        }

//        SimpleQueue(
//            queue = userQueue,
//            onPreTask = {
//                // 前置任务,返回 false 表示跳过
//
//
////                delay(1000)
//                true
//            }
//        ) { user, onDisplayEnd ->
//            // 自动关闭示例
//            LaunchedEffect(user) {
//                delay(1000L)
//                onDisplayEnd()
//            }
//
//            // 或者手动关闭示例
//            UserEnterRoomWidget(
//                userName = user.userName,
//                avatarUrl = user.avatarUrl,
//                vipUrl = user.vipUrl,
//                wealthUrl = user.wealthUrl,
//                charmUrl = user.charmUrl,
////                onCloseClick = onDisplayEnd,
//                // 其他参数...
//            )
//        }
    }
}

SimpleQueue :简单粗暴的队列

SimpleQueueAnimatedQueue的“低配版”,去掉了动画,但保留了按顺序处理的核心逻辑。

功能亮点

  • 按顺序显示:跟AnimatedQueue一样,逐个处理队列里的内容。
  • 前置任务:支持异步预处理,决定是否显示。
  • 无动画:直接上,直接下,效率至上。

代码拆解

/**
 * 通用无动画队列组件,负责按顺序显示内容
 *
 * @param queue 待显示的内容队列
 * @param preTaskTimeoutMillis 前置任务超时时间
 * @param onPreTask 前置任务(返回 true 表示继续显示,false 表示跳过)
 * @param content 内容展示的 Composable,接收结束回调
 */
@Composable
fun<T>SimpleQueue(
    queue: SnapshotStateList<T>,
    preTaskTimeoutMillis: Long = TIME_MILLIS,
    onPreTask: suspend (item: T) -> Boolean = { true },
    content: @Composable (item: T, onDisplayEnd: () -> Unit) -> Unit,
) {
    var currentItem by remember { mutableStateOf<T?>(null) } // 当前内容
    var isProcessing by remember { mutableStateOf(false) } // 处理中标记
    val scope = rememberCoroutineScope() // 协程作用域

    LaunchedEffect(queue.size, isProcessing) {
        if (queue.isNotEmpty() && !isProcessing) {
            try {
                isProcessing = true
                val item = queue.removeAt(0)
                val shouldProceed = withContext(scope.coroutineContext) {
                    withTimeout(preTaskTimeoutMillis) { onPreTask(item) }
                }
                if (shouldProceed) {
                    currentItem = item
                } else {
                    isProcessing = false
                }
            } catch (e: Exception) {
                isProcessing = false
                handleException(e, currentItem)
            }
        }
    }

    currentItem?.let { item ->
        content(item) {
            currentItem = null
            isProcessing = false// 结束当前项
        }
    }
}

工作流程

  1. 盯着队列:LaunchedEffect盯着队列和处理状态,有任务且空闲时就开工。
  2. 干前置活儿:取出一个项目,跑onPreTask。通过就显示,不通过就跳过。
  3. 直接展示:把项目塞进currentItem,交给content展示。外部调用onDisplayEnd后,清空并准备下一项。
  4. 异常处理:出错时记录日志,打印堆栈,保证健壮性。

SimpleQueue 的Sample案例

提供了几个SimpleQueue的使用示例,来看看它们怎么玩:

场景1:强制前置任务

SimpleQueue(
    queue = queue,
    onPreTask = { item ->
        try {
            downloadEssentialResource(item.resUrl) // 必须下载资源
            true
        } catch (e: Exception) {
            logError(e)
            queue.add(item) // 下载失败,重新加回队列
            false
        }
    }
) { item, onDisplayEnd ->
    Text("显示内容: $item")
    Button(onClick = { onDisplayEnd() }) { Text("下一项") }
}

场景:显示内容前必须下载资源,比如图片。下载失败就跳过,还能选择重新加回队列。

场景2:可选预处理

SimpleQueue(
    queue = queue,
    onPreTask = { item ->
        prefetchOptionalData(item) // 预加载非必要数据
        true// 成功失败都继续
    }
) { item, onDisplayEnd ->
    Text("内容: $item")
    Button(onClick = { onDisplayEnd() }) { Text("下一项") }
}

场景:预加载些锦上添花的数据(比如用户头像),不管成不成功都显示内容。

场景3:条件过滤

SimpleQueue(
    queue = queue,
    onPreTask = { item -> isValid(item) } // 检查有效性
) { item, onDisplayEnd ->
    Text("有效内容: $item")
    Button(onClick = { onDisplayEnd() }) { Text("下一项") }
}

场景:只显示符合条件的内容,比如过滤掉过期消息。

总结:动起来or稳下来,随你挑!

AnimatedQueueSimpleQueue就像一对好搭档,一个负责花式表演,一个专注高效干活。它们用LaunchedEffect盯着队列,rememberCoroutineScope跑前置任务,SnapshotStateList管好任务清单,再加上AnimatedVisibility的动画魔法,让你的应用既能动起来,又能稳得住。

试试把它们加到你的项目里吧!是让用户进入房间的提示跳个舞,还是让消息通知老老实实排队,全看你的创意。有什么想法或问题,欢迎在评论区聊聊,咱们一起折腾出更多好玩的东西!