一、RecentsView 的整体架构
RecentsView,说白了就是 Android 桌面上那个「多任务切换」的界面。你双击最近任务键,或者从底部上滑悬停,看到的那一排卡片,就是它。
我个人习惯把 RecentsView 拆成三层来看:
- 数据层:负责收集当前运行的任务、Activity 栈信息
- 渲染层:把任务卡片画出来,包括缩略图、标题、图标
- 交互层:处理滑动删除、点击切换、手势动画
这三层各司其职,但真正难搞的是它们之间的联动。我刚开始做 Launcher 开发时,总觉得 RecentsView 就是个 RecyclerView 嘛,能有多复杂?结果一上手才发现,缩略图的生成时机、缓存策略、还有跟 Launcher 的 Activity 生命周期配合,处处是坑。
核心要点:RecentsView 不是独立存在的,它本质上是 Launcher 的一部分。它的显示、隐藏、动画,都跟 Launcher 的 onPause/onResume 深度绑定。
二、RecentsView 的实现细节
2.1 视图层级
RecentsView 本身是一个自定义的 ViewGroup,它内部包含一个 RecyclerView。每个 item 是一个 TaskView,用来展示单个任务卡片。
嗯,这里要注意:TaskView 不是简单的 ImageView + TextView。它需要承载缩略图、应用图标、应用名称,还要响应触摸事件。我见过不少开发者直接把缩略图塞进 ImageView,结果内存直接爆了。
// RecentsView 的核心结构(伪代码)
class RecentsView : FrameLayout {
private val taskList: RecyclerView
private val adapter: TaskAdapter
fun showRecentTasks() {
// 1. 从 SystemServices 获取任务列表
val tasks = activityManager.getRecentTasks()
// 2. 更新适配器
adapter.submitList(tasks)
// 3. 触发进入动画
animateIn()
}
}
2.2 任务数据的获取
获取最近任务,靠的是 ActivityManager 的 getRecentTasks() 方法。但这个方法在 Android 10 以后做了限制——非系统应用拿不到完整信息。
Launcher 作为系统级应用,有特权调用。但即便如此,我也踩过坑:
- 有些任务已经销毁了,但还在列表里
- 有些任务有多个 Activity 实例,需要去重
- 系统可能会随时清理后台任务,列表会动态变化
我的经验:注册 TaskStackListener 监听任务变化,不要只拉一次列表就完事。用户可能正在切换应用,列表随时会变。
三、缩略图的生成与缓存
缩略图,是 RecentsView 里最吃性能的部分。每个任务卡片都需要一张当前界面的截图。你想想看,如果用户开了 20 个应用,就要生成 20 张截图,还要实时更新——这可不是闹着玩的。
3.1 缩略图生成原理
Android 系统通过 SurfaceControl 来截取屏幕内容。具体来说,是调用 SurfaceControl.screenshot() 方法。但这个方法有几个限制:
- 只能截取当前可见的 Activity
- 如果应用处于后台,截到的是空白或者冻结画面
- 截图的尺寸需要适配屏幕密度
// 缩略图生成的核心代码
fun generateThumbnail(taskId: Int, width: Int, height: Int): Bitmap? {
// 获取任务的 SurfaceControl
val surfaceControl = WindowManager.globalGet(taskId)
// 截图
val screenshot = SurfaceControl.captureDisplay(surfaceControl, width, height)
// 缩放处理
return Bitmap.createScaledBitmap(screenshot, targetWidth, targetHeight, true)
}
注意:不要在 UI 线程执行截图操作!我曾经在项目里直接在主线程调 screenshot,结果界面直接卡死 500ms。一定要放到后台线程去处理。
3.2 缓存策略
缩略图不能每次都重新生成,必须缓存。我建议用三级缓存:
| 缓存层级 | 存储位置 | 生命周期 | 容量 |
|---|---|---|---|
| L1 - 内存缓存 | LruCache | 应用存活期间 | 20-30 张 |
| L2 - 磁盘缓存 | 应用内部存储 | 重启后仍有效 | 50-100 张 |
| L3 - 系统缓存 | SurfaceFlinger | 由系统管理 | 有限 |
为什么内存缓存只放 20-30 张?因为每张缩略图可能占 1-2MB,30 张就是 60MB。再大就容易 OOM 了。我见过一个厂商直接把所有任务缩略图都缓存,结果低端机直接崩溃。
避坑指南:缩略图缓存一定要绑定 taskId。任务被移除时,同步清理对应的缓存。否则会出现「任务已关闭,缩略图还在」的诡异现象。
四、与 Launcher 的联动
RecentsView 不是独立页面,它跟 Launcher 的交互非常紧密。说白了,它就是 Launcher 的一个「特殊模式」。
4.1 生命周期联动
当 RecentsView 显示时,Launcher 的 Activity 会进入 onPause 状态。但注意,Launcher 并没有真正销毁,只是把桌面内容隐藏了。
// Launcher 中的处理
override fun onPause() {
super.onPause()
// 如果 RecentsView 正在显示,不要销毁桌面数据
if (recentsView.isShowing) {
// 保留桌面状态,只隐藏视图
workspace.visibility = View.GONE
}
}
fun showRecents() {
// 1. 通知 Launcher 进入多任务模式
isInRecentsMode = true
// 2. 隐藏桌面
workspace.animateOut()
// 3. 显示 RecentsView
recentsView.show()
}
4.2 手势冲突处理
Launcher 本身有左右滑动切换桌面的手势,RecentsView 也有左右滑动删除任务的手势。这两个手势如果处理不好,就会打架。
我的做法是:
- RecentsView 显示时,Launcher 的手势全部禁用
- RecentsView 隐藏时,恢复 Launcher 的手势
- 在触摸事件的分发阶段做拦截
小技巧:用 GestureExclusionRect 来排除手势冲突区域。这样系统会自动帮你判断该谁响应。
4.3 动画过渡
从桌面到 RecentsView 的过渡动画,是用户体验的关键。我个人习惯用「缩放 + 平移」的组合动画:
- 桌面内容缩小到 80%,并向上平移
- RecentsView 从底部滑入
- 任务卡片依次展开,带一点错位效果
这个动画看似简单,但帧率要保持在 60fps 以上。我优化过的一个方案是把动画拆成两步:先做桌面缩放,再做卡片展开。这样每帧的计算量更小,不容易掉帧。
五、总结
RecentsView 的实现,说难不难,说简单也不简单。核心就三点:
- 数据要准:任务列表要实时更新,不能出现已关闭的应用
- 缩略图要快:生成和缓存策略要到位,不能卡 UI
- 联动要稳:跟 Launcher 的生命周期、手势、动画要无缝衔接
我在做第一个版本的 RecentsView 时,就是因为忽略了缓存策略,导致低端机频繁 OOM。后来改成三级缓存 + 懒加载,才把问题解决。嗯,有些坑,真的只有踩过才知道。
如果你正在做 Launcher 开发,建议先把 RecentsView 的缩略图缓存机制想清楚。这块搞定了,其他都是细节问题。