嗨,朋友们!今天我要和大家聊聊Android Compose中一个超酷的话题——如何用动画和队列让你的应用界面变得生动有趣。这次我们要围绕两段代码展开:AnimatedQueue和SimpleQueue。别担心,我会用最接地气的语言把它们讲明白,保证你不仅能看懂,还会觉得有点意思!
开篇先看 Gif 效果(吐槽掘金上传视频太麻烦),动画的配置完全自定义,实现不同的效果关键是需求和你的想象力!
背景:为什么需要动画和队列?
想象一下,你在看直播,每当有人进入房间,屏幕上就会弹出一个小提示:“XXX进入了房间”。如果同时进来好几个人,这些提示不会一股脑儿全挤在屏幕上,而是像排队一样,一个接一个地出现。这种“有条不紊”的效果,就是队列的功劳。
再比如,直播间顶部经常会飘过一些特效通知,比如“XXX送了一个火箭!”这些通知不仅要按顺序显示,还要带点动画效果,比如从右边滑进来,再从左边滑出去,这样看起来才够炫酷。
今天的主角AnimatedQueue和SimpleQueue就是实现这种效果的利器。一个带动画,一个不带动画,但都能帮你按顺序处理和展示内容。接下来,我们就来拆解它们的代码,看看是怎么工作的,顺便聊聊用到的几个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()
}
}
}
}
工作流程
- 盯着队列:LaunchedEffect盯着队列大小和动画状态。只要队列有货且空闲(!isAnimating),就立刻开工。
- 干前置活儿:从队列里拿出一个项目,跑onPreTask(有20秒超时)。如果任务通过(返回true),就准备展示;否则跳过。
- 开演:把项目塞进currentItem,把visibleState.targetState设为true,触发AnimatedVisibility的进入动画。
- 谢幕:动画跑完后(通过另一个LaunchedEffect监听),如果内容还在舞台上,就等外部调用onDisplayEnd触发退出动画;退出动画结束后,清空舞台,准备下一场。
- 异常处理:如果中途出错(比如超时),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 :简单粗暴的队列
SimpleQueue是AnimatedQueue的“低配版”,去掉了动画,但保留了按顺序处理的核心逻辑。
功能亮点
- 按顺序显示:跟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// 结束当前项
}
}
}
工作流程
- 盯着队列:LaunchedEffect盯着队列和处理状态,有任务且空闲时就开工。
- 干前置活儿:取出一个项目,跑onPreTask。通过就显示,不通过就跳过。
- 直接展示:把项目塞进currentItem,交给content展示。外部调用onDisplayEnd后,清空并准备下一项。
- 异常处理:出错时记录日志,打印堆栈,保证健壮性。
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稳下来,随你挑!
AnimatedQueue和SimpleQueue就像一对好搭档,一个负责花式表演,一个专注高效干活。它们用LaunchedEffect盯着队列,rememberCoroutineScope跑前置任务,SnapshotStateList管好任务清单,再加上AnimatedVisibility的动画魔法,让你的应用既能动起来,又能稳得住。
试试把它们加到你的项目里吧!是让用户进入房间的提示跳个舞,还是让消息通知老老实实排队,全看你的创意。有什么想法或问题,欢迎在评论区聊聊,咱们一起折腾出更多好玩的东西!