安卓13 Launcher3深度定制:从源码到实战,打造优雅的文件夹预览图布局方案
最近在做一个平板Launcher的深度定制项目,客户反馈文件夹预览图的图标布局有点别扭——两个图标时贴边太紧,三个图标时顶部图标直接“越狱”了。这问题看似不大,但实际体验很影响质感。我花了几天时间把Launcher3的文件夹绘制逻辑彻底梳理了一遍,发现这背后其实是一套相当精巧的几何布局算法。
如果你也在做Launcher定制,特别是安卓13的版本,这篇文章应该能帮你少走不少弯路。我会从源码分析入手,给出两种不同层级的解决方案,最后还会分享一些参数化设计的思路,让你的修改更加健壮。
1. 理解Launcher3文件夹预览图的绘制架构
要调整文件夹预览图的布局,首先得搞清楚它是怎么画出来的。Launcher3的文件夹图标绘制是个典型的分层架构,理解这个架构比直接改代码更重要。
1.1 核心类的作用与协作关系
Launcher3中负责文件夹图标绘制的核心类主要有三个:FolderIcon、PreviewItemManager和ClippedFolderIconLayoutRule。它们的分工很明确:
- FolderIcon:文件夹图标的View容器,负责整体的绘制调度
- PreviewItemManager:管理预览项(就是那些小图标)的绘制参数和动画
- ClippedFolderIconLayoutRule:计算每个预览项的位置和大小
这个架构的设计思路很清晰——计算与绘制分离。ClippedFolderIconLayoutRule负责“怎么摆”,PreviewItemManager负责“怎么画”,FolderIcon负责“什么时候画”。
// FolderIcon的初始化过程
private void init() {
mLongPressHelper = new CheckLongPressHelper(this);
// 创建布局规则和预览项管理器
mPreviewLayoutRule = new ClippedFolderIconLayoutRule();
mPreviewItemManager = new PreviewItemManager(this);
mDotParams = new DotRenderer.DrawParams();
}
这里有个关键点:ClippedFolderIconLayoutRule在初始化时就被创建,但它的参数配置是在PreviewItemManager.recomputePreviewDrawingParams()中完成的。这种延迟初始化的设计是为了适应不同设备尺寸的动态调整。
1.2 绘制流程的完整调用链
当文件夹需要绘制时,整个调用链是这样的:
FolderIcon.dispatchDraw()
↓
PreviewItemManager.recomputePreviewDrawingParams()
↓
PreviewItemManager.computePreviewDrawingParams()
↓
ClippedFolderIconLayoutRule.init() // 初始化布局参数
↓
PreviewItemManager.updatePreviewItems()
↓
PreviewItemManager.buildParamsForPage()
↓
PreviewItemManager.computePreviewItemDrawingParams()
↓
ClippedFolderIconLayoutRule.computePreviewItemDrawingParams() // 核心计算
↓
PreviewItemManager.draw() // 实际绘制
这个流程中,计算位置和实际绘制是两个独立的阶段。理解这一点很重要,因为我们的修改方案也会基于这两个不同的切入点。
提示:在调试时,可以在
FolderIcon.dispatchDraw()方法开头加Log,观察绘制频率。你会发现文件夹图标的绘制是相当频繁的,特别是在拖拽、动画过程中。
1.3 预览项的数据结构
每个预览项的信息都封装在PreviewItemDrawingParams对象中:
class PreviewItemDrawingParams {
float transX; // X轴平移量
float transY; // Y轴平移量
float scale; // 缩放比例
Drawable drawable; // 图标Drawable
WorkspaceItemInfo item; // 应用信息
// 构造函数和更新方法
PreviewItemDrawingParams(float transX, float transY, float scale) {
this.transX = transX;
this.transY = transY;
this.scale = scale;
}
}
这个类的设计体现了Android图形系统的典型模式:变换参数(transX, transY, scale)与绘制内容(drawable)分离。调整布局本质上就是调整这些变换参数。
2. 布局计算的核心算法解析
现在进入最核心的部分——ClippedFolderIconLayoutRule如何计算每个图标的位置。这部分涉及一些几何知识,但我会用尽量直观的方式解释。
2.1 圆形布局的基本原理
Launcher3的文件夹预览图采用了一种圆形布局算法。简单说,就是把图标均匀分布在一个虚拟的圆环上。这个设计的巧妙之处在于,无论图标数量是2个、3个还是4个,都能保持视觉上的平衡。
关键参数定义:
public static final int MAX_NUM_ITEMS_IN_PREVIEW = 4;
private static final int MIN_NUM_ITEMS_IN_PREVIEW = 2;
private static final float MIN_SCALE = 0.44f;
private static final float MAX_SCALE = 0.51f;
private static final float MAX_RADIUS_DILATION = 0.25f;
private static final float ITEM_RADIUS_SCALE_FACTOR = 1.15f;
这些常量决定了布局的基本行为:
MAX_NUM_ITEMS_IN_PREVIEW:最多显示4个预览图标MIN_SCALE/MAX_SCALE:图标的缩放范围ITEM_RADIUS_SCALE_FACTOR:控制图标分布半径的系数
2.2 getPosition()方法的数学逻辑
getPosition()方法是整个布局计算的核心,它的计算过程可以分为几个步骤:
第一步:确定起始角度
// 从左往右布局时,起始角度为π(180度)
double theta0 = mIsRtl ? 0 : Math.PI;
int direction = mIsRtl ? 1 : -1;
double thetaShift = 0;
// 根据图标数量调整起始角度
if (curNumItems == 3) {
thetaShift = Math.PI / 2; // 90度
} else if (curNumItems == 4) {
thetaShift = Math.PI / 4; // 45度
}
theta0 += direction * thetaShift;
这个调整是为了让布局在不同数量下都看起来自然。比如3个图标时,起始角度设为90度,这样第一个图标就在正上方。
第二步:计算每个图标的角度
// 将圆环等分,计算每个图标的角度
double theta = theta0 + index * (2 * Math.PI / curNumItems) * direction;
这里用到了极坐标的思想。如果curNumItems=3,那么每个图标间隔120度(2π/3)。
第三步:计算分布半径
// 半径随图标数量线性增加
float radius = mRadius * (1 + MAX_RADIUS_DILATION *
(curNumItems - MIN_NUM_ITEMS_IN_PREVIEW) /
(MAX_NUM_ITEMS_IN_PREVIEW - MIN_NUM_ITEMS_IN_PREVIEW));
半径的计算公式很有意思:图标越多,半径越大。这是为了给更多图标腾出空间。
第四步:转换为直角坐标
// 计算图标左上角坐标
float halfIconSize = (mIconSize * scaleForItem(curNumItems)) / 2;
result[0] = mAvailableSpace / 2 + (float) (radius * Math.cos(theta) / 2) - halfIconSize;
result[1] = mAvailableSpace / 2 + (float) (-radius * Math.sin(theta) / 2) - halfIconSize;
这里有几个关键点:
radius * Math.cos(theta)得到的是图标中心相对于圆心的X坐标- 除以2是为了让图标更靠近中心,避免贴边
- 减去
halfIconSize是因为Canvas绘制时用的是左上角坐标
2.3 缩放系数的动态调整
图标的大小不是固定的,而是根据数量动态调整:
public float scaleForItem(int numItems) {
final float scale;
if (numItems <= 3) {
scale = MAX_SCALE; // 2-3个图标用最大缩放
} else {
scale = MIN_SCALE; // 4个及以上用最小缩放
}
return scale * mBaselineIconScale;
}
这个设计很合理:图标少时可以大一些,图标多时就得小一些,否则放不下。
2.4 问题定位:为什么会出现布局异常
通过分析源码,我发现布局问题的根源在于几何约束条件被破坏。以3个图标为例,理想情况下它们应该构成一个等边三角形,且都在文件夹边界内。
但实际计算时,如果图标尺寸(mIconSize * scale)过大,或者分布半径(radius)不合适,就会导致图标超出边界。具体来说:
- 顶部图标溢出:Y坐标
result[1]为负值,说明图标跑到了文件夹上方 - 左右图标贴边:X坐标
result[0]

&spm=1001.2101.3001.5002&articleId=151469400&d=1&t=3&u=6a8059e7c16a4127a719471244b3a320)

被折叠的 条评论
为什么被折叠?



