ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Android 15 Launcher3文件夹图标定制:从FolderIcon绘制到RRO overlay实践

Android 15 Launcher3文件夹图标定制:从FolderIcon绘制到RRO overlay实践

最近在给一台 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”。

需求描述真实改动推荐实现方式
换背景颜色/圆角替换 drawableRRO overlay
换一张固定 logo修改 FolderIcon 绘制资源RRO overlay 或布局覆盖
加未读角标改动 FolderIcon 绘制逻辑源码修改
不要预览,只显示单图禁用预览层源码修改 + 布局覆盖
预览图和背景都要适配暗色模式需要多套资源overlay + night qualifier

回答这三个问题基本能定方案:

  1. 文件夹是否需要保留“子图标实时预览”?如果保留,就不能简单用普通ImageView盖掉;
  2. 图标是静态的,还是跟着系统动态变化的(比如应用内数字、时间)?动态需求必须走代码;
  3. 是单独一台调试机,还是要批量出厂的设备?批量场景强烈建议用 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.xml

AndroidManifest.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 提交前最后检查

集成完功能后,我建议至少做一遍下面这组检查:

  1. 在横屏、竖屏各切换一次,文件夹图标是否正常重绘;
  2. 创建一个新文件夹、删除一个旧文件夹,预览刷新是否正常;
  3. 把两个带 adaptive icon 的应用拖入文件夹,确认预览层没有裁切;
  4. 在深色模式、高对比度模式下分别看一遍,背景和文字是否可辨识;
  5. 连续开合文件夹 20 次以上,确认没有内存泄漏或明显掉帧;
  6. 如果用了 overlay,执行adb shell cmd overlay list确认 item 状态是[x]。

这些检查看起来基础,但很能暴露问题。文件夹图标这种高频 UI 改动,最怕只在你测试的那一条路径上看起来正常,换个场景就翻车。

最后再分享一个个人心得:改 Launcher3 文件夹图标,难点不在“会画一张图”,而在“搞清楚这张图是哪个环节画出来的”。AOSP Launcher3 经过这么多版本迭代,绘制链路已经非常稳定,它不会因为你要换一张 icon 就给你一张简单的替换口子。遇到需求,先按“资源覆盖 → 绘制追加 → 布局叠加”的顺序评估,能少走很多弯路。按这个顺序动手,大部分定制需求都能在一天内完成第一版可演示效果,后面再慢慢打磨细节。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进