一、RecentsView 的整体架构

RecentsView,说白了就是 Android 桌面上那个「多任务切换」的界面。你双击最近任务键,或者从底部上滑悬停,看到的那一排卡片,就是它。

我个人习惯把 RecentsView 拆成三层来看:

  • 数据层:负责收集当前运行的任务、Activity 栈信息
  • 渲染层:把任务卡片画出来,包括缩略图、标题、图标
  • 交互层:处理滑动删除、点击切换、手势动画

这三层各司其职,但真正难搞的是它们之间的联动。我刚开始做 Launcher 开发时,总觉得 RecentsView 就是个 RecyclerView 嘛,能有多复杂?结果一上手才发现,缩略图的生成时机、缓存策略、还有跟 Launcher 的 Activity 生命周期配合,处处是坑。

核心要点:RecentsView 不是独立存在的,它本质上是 Launcher 的一部分。它的显示、隐藏、动画,都跟 Launcher 的 onPause/onResume 深度绑定。

RecentsView 三层架构 数据层 TaskStackListener · ActivityManager · 任务栈信息收集 渲染层 RecentsView · TaskView · 缩略图生成 · 卡片布局 交互层 手势处理 · 滑动删除 · 点击切换 · 动画过渡

二、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 的实现,说难不难,说简单也不简单。核心就三点:

  1. 数据要准:任务列表要实时更新,不能出现已关闭的应用
  2. 缩略图要快:生成和缓存策略要到位,不能卡 UI
  3. 联动要稳:跟 Launcher 的生命周期、手势、动画要无缝衔接

我在做第一个版本的 RecentsView 时,就是因为忽略了缓存策略,导致低端机频繁 OOM。后来改成三级缓存 + 懒加载,才把问题解决。嗯,有些坑,真的只有踩过才知道。

如果你正在做 Launcher 开发,建议先把 RecentsView 的缩略图缓存机制想清楚。这块搞定了,其他都是细节问题。

公众号:蓝海资料掘金营,微信deep3321