最近在给一台 Android 15 的定制设备做桌面二次开发,客户提了一个听起来很简单的需求:把 Launcher3 原生文件夹的 icon 换成他们自己的品牌图标。我当时心想,这无非就是改一张图片资源的事,结果真正动手才发现,Launcher3 里的文件夹图标根本不是一张普通位图,而是一个由FolderIcon这个自定义 View 实时绘制的复合组件。你要是不理解它内部的绘制时序,改出来的效果要么不生效,要么能把文件夹预览搞成一团浆糊。
这篇文章不是教科书式的源码分析,而是把我这几周踩坑、试错后的完整思路和可复现步骤整理出来。内容主要围绕 Android 15 上的 AOSP Launcher3 展开,适合正在做系统 ROM 定制、桌面二次开发,或者需要在 SDK 里模拟类似效果的工程师。不管你是想直接换一张静态图片,还是想给文件夹加一个动态角标,下面这套方案都够用。
1. 项目概述与需求拆解
1.1 原生文件夹图标到底长什么样
要改东西,得先知道原厂做的什么。Android 桌面(Launcher3)里,“文件夹”不是一个普通应用图标,而是一个独立的自定义 View。你在桌面长按一个图标拖到另一个图标上,系统会创建FolderIcon,默认在桌面上显示为一个半透明的圆角背景,背景里错落堆叠着几个子图标的小预览,下方跟着“文件夹”三个字的文本标签。
所以用户所说的“文件夹的 icon”,严格讲包含两层含义:一是外层那个圆角背景形状,二是背景里那一组子图标的实时预览。我们做定制时经常遇到的诉求也分三种:
- 只换背景形状和颜色,保留子图标预览;
- 把整个文件夹图标替换成一张独立品牌图(比如静态 PNG);
- 在原有预览基础上增加角标、动态时钟、数字未读等附加信息。
这三种诉求的改动深度完全不一样。第一种用资源覆盖就能解决,第二种可能需要动绘制逻辑,第三种则需要理解 View 的生命周期和绘制顺序。很多刚开始接触 Launcher3 的开发者,一上来就想把FolderIcon直接替换成ImageView,结果文件夹打不开了,因为长按、拖拽、开合动画全都挂在这个自定义 View 上。
1.2 动手前先回答三个问题
我建议在写任何代码之前,先拿下面这张表给需求方对一遍。很多时候客户嘴上说“换个 icon”,实际心里想要的是“icon 上带个角标”,或者“文件夹不要显示预览只显示 logo”。
| 需求描述 | 真实改动 | 推荐实现方式 |
|---|---|---|
| 换背景颜色/圆角 | 替换 drawable | RRO overlay |
| 换一张固定 logo | 修改 FolderIcon 绘制资源 | RRO overlay 或布局覆盖 |
| 加未读角标 | 改动 FolderIcon 绘制逻辑 | 源码修改 |
| 不要预览,只显示单图 | 禁用预览层 | 源码修改 + 布局覆盖 |
| 预览图和背景都要适配暗色模式 | 需要多套资源 | overlay + night qualifier |
回答这三个问题基本能定方案:
- 文件夹是否需要保留“子图标实时预览”?如果保留,就不能简单用普通
ImageView盖掉; - 图标是静态的,还是跟着系统动态变化的(比如应用内数字、时间)?动态需求必须走代码;
- 是单独一台调试机,还是要批量出厂的设备?批量场景强烈建议用 RRO overlay,不用改 AOSP 原包。
2. Launcher3 文件夹图标的生成机制解析
2.1 FolderIcon 和 PreviewBackground 的分工
AOSP 里 Launcher3 的相关代码在packages/apps/Launcher3目录,文件夹这块核心有两个类:com.android.launcher3.folder.FolderIcon和com.android.launcher3.folder.PreviewBackground。
FolderIcon是整个文件夹的入口,它负责处理触摸事件、长按拖拽、打开动画、数据绑定。你可以理解成它就是一个容器,本身不“画”文件夹的视觉内容,真正的背景绘制由PreviewBackground(一个自定义 Drawable 实现)完成,子图标的排列则依赖FolderPreview那套逻辑。
这个分工非常关键。因为当你想要改图标的时候,不能只盯着FolderIcon,还要知道哪个阶段画背景、哪个阶段画子图标。一般的绘制顺序是:先画PreviewBackground的圆角背景,再计算子图标的缩放和位置并绘制,最后画文件夹选中态、焦点态之类的状态图层。如果自定义绘制代码插在错误的位置,轻则被背景盖住,重则把触摸区域弄乱。
2.2 资源与布局:folder_icon.xml 拆解
除了 Java 代码,Launcher3 的文件夹图标也离不开布局资源。系统默认的布局文件在packages/apps/Launcher3/res/layout/folder_icon.xml附近,里面通常是一个自定义FolderIcon标签,内部会包含一个用于显示文本的FolderNameView或BubbleTextView,以及存放背景资源的 ImageView 逻辑。
这里最容易踩的坑是:很多新手想当然地认为folder_icon.xml里应该有一个可以直接换 src 的ImageView,于是把FolderIcon标签改成ImageView,结果编译出来桌面直接崩溃。原因前面也说了,FolderIcon是一个带有完整触摸反馈和文件夹逻辑的自定义 View,不是普通图片容器。真要替换布局,也必须保留FolderIcon根标签,只去动它内部的子元素或 drawable 引用。
另外要注意 density 目录。Launcher3 自带的背景资源分布在drawable-anydpi、drawable-mdpi、drawable-xxxhdpi等目录,如果你随随便便放一张 48x48 的 PNG 到drawable-xxxhdpi,在低分辨率设备上会显得很突兀。系统里的图标网格尺寸是按 dp 算的,最好用 vector drawable 或者按不同 density 出一套位图。
2.3 Android 15 的图标约束:adaptive icon 与主题图标
Android 15 已经全面普及了 Adaptive Icon(自适应图标)机制,系统桌面会给每个应用图标套一个前景、背景分层模型。文件夹作为内部的组合图标,虽然不直接参与 adaptive icon 动态遮罩,但它会读取子应用图标的 adaptive 图层,用来做预览。换句话说,文件夹预览里的子图标长什么样,取决于那些子应用本身是否提供了适配层。
如果你的定制方案里,某些应用没有自适应图标,只是老式 PNG,那么文件夹预览里那个图标可能会被放大、裁切,或者出现背景色不统一的情况。这个问题不一定是你改文件夹代码导致的,而是上游图标数据有问题。排查时要先分清。
另外,Android 15 对主题图标(themed icon)越来越激进,如果系统开启主题化图标,桌面上很多图标会变成单色雾面样式。文件夹背景如果用了固定颜色资源,在主题化模式下可能和附近图标格格不入。所以做定制 UI 时,我习惯让背景颜色跟随系统强调色或壁纸取色,而不是写死一个品牌色。
3. 修改文件夹图标的三种落地方式
3.1 方案 A:RRO overlay 替换静态资源(推荐给批量设备)
RRO(Runtime Resource Overlay)是 Android 官方推荐的资源覆盖方式,特别适合不想改 AOSP 原生包、希望产品线单独定制桌面风格的场景。它能在不修改Launcher3.apk的前提下,替换其中的资源 ID 所指向的 drawable、layout、color 甚至字符串。
在 Android 15 的设备上,overlay 的编译通常用 Soong 的新写法,核心是一个runtime_resource_overlay模块,再配一个 AndroidManifest,声明targetPackage="com.android.launcher3"。我的经验是,优先覆盖folder_background这类明确的 drawable 资源,而不是覆盖整个folder_icon.xml布局。因为布局里包含太多 View 的引用和 id,一旦覆盖文件版本和原固件对不上,很容易出现 id 错乱、空指针。
overlay 的另一个好处是能动态禁用、启用。测试阶段出问题,一条命令就能摘掉 overlay,不必重新编译整个系统。这对多机种并行调试帮助很大。
3.2 方案 B:直接改 FolderIcon 绘制逻辑(适合动态/品牌需求)
如果要做的是带未读数字的角标,或者根据不同商业模式切换文件夹封面的联动效果,资源 overlay 就无能为力了。这时候只能改源码。
改代码不要一上来就重写FolderIcon。正确做法是先找到它的onDraw(Canvas)或draw(Canvas)方法,看清现有绘制链路,然后在“最后一层”追加自己的绘制内容。比如你画一个品牌 logo 或者角标,只需要在super.onDraw(canvas)之后,手工设置Drawable.setBounds再draw(canvas)即可。如果渠道包要动态控制显示哪张 logo,可以给FolderIcon增加一个入口方法,由Launcher的配置层传入参数。
这里要提醒一句:请不要在这个类里写太复杂的业务逻辑。FolderIcon是桌面进程里高频绘制的对象,桌面滑动、文件夹开关、图标拖拽都会触发它重绘。任何耗时操作放在onDraw里都会卡桌面。
3.3 方案 C:在布局层插入自定义 View(兜底方案)
有时候既不想大改源码,又不想单变量整包 overlay,可以走“半资源”路线:通过 overlay 覆盖folder_icon.xml布局,在不改变FolderIcon根标签的前提下,往布局里多塞一个自定义 View 或ViewStub。这个 View 专门负责盖一层自定义图标在预览之上。
这个方案比改绘制逻辑安全,因为不动 Java 代码,只在布局上叠加 UI。但坑也很明显:View 之间的层级、触摸穿透、事件分发必须处理对。如果你加的 View 盖得比原生FolderIcon还高,又默认接收了点击事件,那用户点击文件夹图标可能触发你自定义 View 的点击事件,而不是打开文件夹。解决办法是在布局中给自定义 View 设置android:clickable="false",并确保它不属于触摸目标。
3.4 选型对比
| 实现方式 | 改动范围 | 开发成本 | 灵活度 | 风险 |
|---|---|---|---|---|
| RRO overlay 覆盖 drawable | 只动资源 | 低 | 中 | 低 |
| 修改 FolderIcon 代码 | 动源码 | 中高 | 高 | 中 |
| 覆盖布局+自定义 View | 资源和布局 | 中 | 中高 | 中高 |
| 替换为 ImageView | 大改布局 | 低 | 低 | 高 (不推荐) |
我个人标准:能 overlay 解决的,尽量不碰源码;碰源码之前,先看绘制链路;实在不行,才考虑塞布局层。
4. 实操过程:从代码到效果的完整步骤
4.1 准备 AOSP 与 Launcher3 工程
这一步的前提是你已经有一个可编译的 AOSP Android 15 源码环境,并且能单独编译 Launcher3。如果还没准备好,可以先在已 checkout 源码的机器上执行一次source build/envsetup.sh和lunch <your_product>,然后单独编译 Launcher3 验证环境。
source build/envsetup.sh lunch <your_product> make Launcher3 -j$(nproc)编译产物一般会输出到out/target/product/<your_product>/system/对应的 APK 路径。这里注意:Launcher3 的包名通常是com.android.launcher3,但有些厂商会改成别的包名,比如com.example.launcher3。你后面做 overlay 时,targetPackage必须写对,否则 overlay 直接不生效。
4.2 通过 overlay 落地静态品牌文件夹图标
我在这次的定制项目里,最稳的方案就是 RRO overlay。假设我们想给文件夹换一个新的背景 drawable,操作如下。
先建 overlay 工程目录:
vendor/yourcompany/overlay/Launcher3FolderIconOverlay/ ├── AndroidManifest.xml ├── Android.bp └── res/ └── drawable/ ├── folder_background.xml └── folder_icon_default.xmlAndroidManifest.xml 里核心是声明 overlay:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.yourcompany.overlay.launcher3foldericon"> <application android:hasCode="false" /> <overlay android:targetPackage="com.android.launcher3" android:isStatic="true" android:priority="1" /> </manifest>Android.bp 按当前 Soong 语法写。注意不要照抄旧版的android_app写法,AOSP 新版本里更推荐 runtime resource overlay:
runtime_resource_overlay { name: "Launcher3FolderIconOverlay", package: "com.yourcompany.overlay.launcher3foldericon", sdk_version: "current", }如果你的产品线需要产物放在 product 分区,可以加上product_specific: true之类的属性,具体以你那套源码的 Soong 文档为准。
drawable 部分,假如系统原来用的背景是folder_background,你就用同名资源覆盖它。这里我用一个简单 vector 示例:
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="48dp" android:height="48dp" android:viewportWidth="48" android:viewportHeight="48"> <path android:fillColor="#1A73E8" android:pathData="M8,4 L40,4 Q44,4 44,8 L44,40 Q44,44 40,44 L8,44 Q4,44 4,40 L4,8 Q4,4 8,4 Z" /> <path android:fillColor="#FFFFFF" android:pathData="M12,12 L22,12 L24,16 L36,16 L36,34 L12,34 Z" /> </vector>编译 overlay 后,需要把它装到设备的/system/overlay、/product/overlay或/vendor/overlay下,具体目录取决于你的编译配置。装完重启桌面进程:
adb shell am force-stop com.android.launcher3如果 overlay 优先级没错,桌面重启后就能看到文件夹背景被替换成了自定义样式。
4.3 修改 FolderIcon 的绘制逻辑,加入自定义角标
当需求变成“文件夹背景之上还要显示一个公司 logo 角标”时,overlay 能力就不够了。我这次的场景是需要在文件夹预览图右下角放一个动态数据角标,因此要改FolderIcon的绘制代码。
先定位到packages/apps/Launcher3/src/com/android/launcher3/folder/FolderIcon.java,找到类似onDraw的方法。不要把逻辑写在onLayout之外的地方太靠前。大概的做法是这样:
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (mBadgeDrawable != null) { int left = getMeasuredWidth() - mBadgeSize; int top = getMeasuredHeight() - mBadgeSize; mBadgeDrawable.setBounds(left, top, left + mBadgeSize, top + mBadgeSize); mBadgeDrawable.draw(canvas); } }mBadgeSize要按屏幕 density 换算,不要写死像素:
mBadgeSize = getResources().getDimensionPixelSize(R.dimen.folder_badge_size);这里有两个关键点。第一,mBadgeDrawable的创建和更新不要在onDraw里做,最好在数据绑定或 UI 刷新入口统一更新,否则每帧都更新对象会造成严重的 GC。第二,如果你画的角标要跟随子应用图标变化(比如未读数量),记得在更新完数据后调用invalidate(),不然角标不会重绘。
4.4 编译、烧录与 Launcher 缓存处理
改完源码后重新编译 Launcher3:
make Launcher3 -j$(nproc)如果只是单独验证,可以只 push APK:
adb root adb remount adb push out/target/product/<your_product>/system/priv-app/Launcher3/Launcher3.apk /system/priv-app/Launcher3/ adb shell am force-stop com.android.launcher3这里我不建议只在应用管理器里点“清除数据”,因为那样会重置桌面布局,用户已经排好的图标全没了。正确做法是先强制停止进程,必要时再重启系统 UI:
adb shell pkill -f com.android.launcher3如果图标还是老样子,第一怀疑对象是 Launcher3 内部的资源缓存。桌面为了性能会把很多 drawable 缓存成内存态,改资源后必须等桌面进程完全杀死再重启。有时am force-stop触发不了 Launcher 自身崩溃重启,需要配合adb shell am start -n com.android.launcher3/.Launcher手动拉起。
5. 常见问题与排查技巧实录
5.1 问题速查表
| 现象 | 最可能原因 | 排查方向 |
|---|---|---|
| overlay 编译通过但不生效 | targetPackage 写错或优先级冲突 | 检查 AndroidManifest.xml 中的包名 |
| 桌面图标没有变化 | Launcher 仍在内存缓存 | 彻底 kill 桌面进程再重启 |
| 文件夹打开后背景异常 | 背景资源和预览尺寸不匹配 | 检查 PreviewBackground 的圆角参数 |
| 自定义 View 挡住点击 | 布局层拦截了触摸事件 | 给自定义 View 设置 clickable=false |
| 修改代码后桌面闪退 | FolderIcon 空指针 | 看 logcat 堆栈定位是哪个字段未初始化 |
| 图标显示模糊 | 位图密度资源不对 | 使用 vector drawable 或补齐 density 目录 |
| 深色模式下图标刺眼 | 未配置 night 资源 | 增加drawable-night/覆盖 |
5.2 几个实操中反复踩到的“坑”
先说图标缓存。Launcher3 内部有一整套 icon cache,各类图标都会被 KeyedCache 存起来。我之前改完folder_background.xml后直接往系统里 push 资源,桌面进程没完全重启,图标始终是旧的。后来发现不能只am force-stop,在某些版本上这个命令不会立刻触发 Launcher 的冷启动,必须再把 Launcher 进程杀干净。排查时先确认这一点,能省很多时间。
再说布局覆盖。如果你选择覆盖folder_icon.xml,原文件里的所有@id/引用都必须存在,否则 inflate 阶段直接抛异常。我遇到过一次folder_icon里引用了folder_icon_name这个 id,但我覆盖时把它漏了,结果整个桌面崩溃,logcat 里只有ResourceNotFound,非常难找。
最后一个坑是 PreviewBackground 的圆角设置。Android 15 的桌面文件夹背景是有圆角的,你自定义 drawable 时如果也写了圆角,一定要确保两处圆角半径一致,否则会出现“背景圆角变了,但预览图还是原来圆角”的割裂感。这块不在folder_background.xml里,而在PreviewBackground类里的loadXYZ逻辑中,视觉不统一时去那边找。
5.3 提交前最后检查
集成完功能后,我建议至少做一遍下面这组检查:
- 在横屏、竖屏各切换一次,文件夹图标是否正常重绘;
- 创建一个新文件夹、删除一个旧文件夹,预览刷新是否正常;
- 把两个带 adaptive icon 的应用拖入文件夹,确认预览层没有裁切;
- 在深色模式、高对比度模式下分别看一遍,背景和文字是否可辨识;
- 连续开合文件夹 20 次以上,确认没有内存泄漏或明显掉帧;
- 如果用了 overlay,执行
adb shell cmd overlay list确认 item 状态是[x]。
这些检查看起来基础,但很能暴露问题。文件夹图标这种高频 UI 改动,最怕只在你测试的那一条路径上看起来正常,换个场景就翻车。
最后再分享一个个人心得:改 Launcher3 文件夹图标,难点不在“会画一张图”,而在“搞清楚这张图是哪个环节画出来的”。AOSP Launcher3 经过这么多版本迭代,绘制链路已经非常稳定,它不会因为你要换一张 icon 就给你一张简单的替换口子。遇到需求,先按“资源覆盖 → 绘制追加 → 布局叠加”的顺序评估,能少走很多弯路。按这个顺序动手,大部分定制需求都能在一天内完成第一版可演示效果,后面再慢慢打磨细节。