Android12——Launcher3文件夹布局修改调整
文章声明:本文是笔者参考良心大佬作品后结合实际需求进行相应的定制,本篇主要是笔者记录一次解析bug笔记,文中可能会引用大佬文章中的部分图片在此声明,并非盈利目的,如涉嫌侵权请私信,谢谢!
大佬原文:
安卓开发- 安卓13 Launcher3文件夹预览图、文件夹展开后布局修改-CSDN博客文章浏览阅读305次,点赞5次,收藏8次。Android 13 Launcher3 文件夹预览图标溢出、文件夹展开后布局的修改https://blog.csdn.net/qq_44458837/article/details/141129356
客户需求:
主屏幕Google文件夹-文件夹内的应用显示超出Google文件夹大小,不美观请结合实际进行修正和优化。
需求分析:
对比大佬文章中的图片,我们的需求类似,都是属于Launcher3中的桌面文件夹溢出的现象,其解决方法大同小异。
//本文涉及代码(vendor/packages)
~/Launcher3/src/com/android/launcher3/folder/FolderIcon.java
~/packages/apps/Launcher3/src/com/android/launcher3/folder/ClippedFolderIconLayoutRule.java
~/packages/apps/Launcher3/src/com/android/launcher3/folder/PreviewItemManager.java
问题思路逻辑分析:
1.首先定位到FolderIcon.java文件,其主要负责管理文件夹图标的显示,确保用户能够方便地识别和访问文件夹中的内容,显示文件夹未打开时的图标。
// Launcher3/src/com/android/launcher3/folder/FolderIcon.java
public FolderIcon(Context context, AttributeSet attrs) {
super(context, attrs);
// 调用init()方法初始化
init();
}
public FolderIcon(Context context) {
super(context);
init();
}
private void init() {
mLongPressHelper = new CheckLongPressHelper(this);
// 创建ClippedFolderIconLayoutRule和PreviewItemManager对象,这两个很重要,后面会说到
//mPreviewLayoutRule定义为一个规则或者布局参数,用于控制文件夹中图标预览的布局。
mPreviewLayoutRule = new ClippedFolderIconLayoutRule();
//mPreviewItemManager为了管理文件夹中应用程序、图标或者其他文件的预览显示
mPreviewItemManager = new PreviewItemManager(this);
mDotParams = new DotRenderer.DrawParams();
}
下文分析dispatchDraw方法(虽然笔者也不是很清楚怎么定位到该方法)只能说沿着大佬的步骤走着先。大佬所说绘制图标前会调用这个方法。
// Launcher3/src/com/android/launcher3/folder/FolderIcon.java
// 在绘制文件夹图标之前会调用这个方法,从这里开始分析
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (!mBackgroundIsVisible) return;
// 重新计算图标的各项参数
mPreviewItemManager.recomputePreviewDrawingParams();
if (!mBackground.drawingDelegated()) {
mBackground.drawBackground(canvas);
}
if (mCurrentPreviewItems.isEmpty() && !mAnimating) return;
// 调用PreviewItemManager.draw方法绘制文件夹预览图
mPreviewItemManager.draw(canvas);
if (!mBackground.drawingDelegated()) {
mBackground.drawBackgroundStroke(canvas);
}
drawDot(canvas);
}
// ...其他代码
PreviewItemManager.java类是 用来管理FolderIcon(文件夹预览图)中PreviewItemDrawingParams(文件夹中应用预览项)绘图和动画参数的。
看下PreviewItemManager的recomputePreviewDrawingParams()方法:
// Launcher3/src/com/android/launcher3/folder/PreviewItemManager.java
public void recomputePreviewDrawingParams() {
if (mReferenceDrawable != null) {
// 调用computePreviewDrawingParams()方法
computePreviewDrawingParams(mReferenceDrawable.getIntrinsicWidth(), mIcon.getMeasuredWidth());
}
}
private void computePreviewDrawingParams(int drawableSize, int totalSize) {
if (mIntrinsicIconSize != drawableSize || mTotalWidth != totalSize ||
mPrevTopPadding != mIcon.getPaddingTop()) {
mIntrinsicIconSize = drawableSize;
mTotalWidth = totalSize;
mPrevTopPadding = mIcon.getPaddingTop();
mIcon.mBackground.setup(mIcon.getContext(), mIcon.mActivity, mIcon, mTotalWidth,
mIcon.getPaddingTop());
// 调用了FolderIcon.mPreviewLayoutRule.init方法做一些初始化工作(这个方法要留意下,后面会讲到)
mIcon.mPreviewLayoutRule.init(mIcon.mBackground.previewSize, mIntrinsicIconSize,
Utilities.isRtl(mIcon.getResources()));
// 调用updatePreviewItems,注意参数传的是false
updatePreviewItems(false);
}
}
void updatePreviewItems(boolean animate) {
int numOfPrevItemsAux = mFirstPageParams.size();
// 调用buildParamsForPage方法,第三个参数是false
buildParamsForPage(0, mFirstPageParams, animate);
mNumOfPrevItems = numOfPrevItemsAux;
}
void buildParamsForPage(int page, ArrayList<PreviewItemDrawingParams> params, boolean animate) {
// 这个items是用来存储当前应用图标预览项列表的
List<WorkspaceItemInfo> items = mIcon.getPreviewItemsOnPage(page);
// We adjust the size of the list to match the number of items in the preview.
while (items.size() < params.size()) {
// 先移除params中的items
params.remove(params.size() - 1);
}
while (items.size() > params.size()) {
// 再一个个添加进去
params.add(new PreviewItemDrawingParams(0, 0, 0));
}
int numItemsInFirstPagePreview = page == 0 ? items.size() : MAX_NUM_ITEMS_IN_PREVIEW;
for (int i = 0; i < params.size(); i++) {
// 拿到单个PreviewItemDrawingParams对象,这里的PreviewItemDrawingParams就是单个应用图标预览项
PreviewItemDrawingParams p = params.get(i);
setDrawable(p, items.get(i));
// 上面传递第三个参数animate为false,所以会进入这个if判断
if (!animate) {
if (p.anim != null) {
p.anim.cancel();
}
// 调用computePreviewItemDrawingParams()方法
computePreviewItemDrawingParams(i, numItemsInFirstPagePreview, p);
if (mReferenceDrawable == null) {
mReferenceDrawable = p.drawable;
}
} else {
// ...
}
}
}
PreviewItemDrawingParams computePreviewItemDrawingParams(int index, int curNumItems,
PreviewItemDrawingParams params) {
// We use an index of -1 to represent an icon on the workspace for the destroy and create animations
if (index == -1) {
return getFinalIconParams(params);
}
// index为文件夹中应用预览项的下标,一般来说>=0, 所以走下面逻辑
// 调用了FolderIcon.mPreviewLayoutRule.computePreviewItemDrawingParams方法
return mIcon.mPreviewLayoutRule.computePreviewItemDrawingParams(index, curNumItems, params);
}
// mIcon类型是FolderIcon
private final FolderIcon mIcon;
// ...其他代码
如大佬所罗列的调用顺序,其逻辑都在dispatchDraw方法之后一步一步推理出来的,至于怎么从原本的逻辑定位到dispatchDraw,笔者尚不可知,可能这就是经验吧。
Launcher3/src/com/android/launcher3/folder/PreviewItemManager.java#recomputePreviewDrawingParams()
Launcher3/src/com/android/launcher3/folder/PreviewItemManager.java#computePreviewDrawingParams()
Launcher3/src/com/android/launcher3/folder/PreviewItemManager.java#updatePreviewItems(false)
Launcher3/src/com/android/launcher3/folder/PreviewItemManager.java#buildParamsForPage()
Launcher3/src/com/android/launcher3/folder/PreviewItemManager.java#computePreviewItemDrawingParams()
mIcon.mPreviewLayoutRule.computePreviewItemDrawingParams
上面代码中注释也写得比较清楚,到最后是调用了FolderIcon.mPreviewLayoutRule.computePreviewItemDrawingParams方法,我们回到FolderIcon中看下:就是刚刚初始化的地方
// Launcher3/src/com/android/launcher3/folder/FolderIcon.java
ClippedFolderIconLayoutRule mPreviewLayoutRule;
private void init() {
mLongPressHelper = new CheckLongPressHelper(this);
mPreviewLayoutRule = new ClippedFolderIconLayoutRule();
mPreviewItemManager = new PreviewItemManager(this);
mDotParams = new DotRenderer.DrawParams();
}
我们直接定位到ClippedFolderIconLayoutRule.java类中进行分析
// Launcher3/src/com/android/launcher3/folder/ClippedFolderIconLayoutRule.java
private float[] mTmpPoint = new float[2];
/**
* 这个方法实际上就是计算每个应用图标预览项的位置和大小的
* index是应用图标在文件夹内的下标,从零开始
* curNumItems是文件夹内应用图标的总数
* params是对应应用图标的相关参数
*/
public PreviewItemDrawingParams computePreviewItemDrawingParams(int index, int curNumItems, PreviewItemDrawingParams params) {
float totalScale = scaleForItem(curNumItems);
float transX;
float transY;
if (index == EXIT_INDEX) {
// 0 1 * <-- Exit position (row 0, col 2)
// 2 3
getGridPosition(0, 2, mTmpPoint);
} else if (index == ENTER_INDEX) {
// 0 1
// 2 3 * <-- Enter position (row 1, col 2)
getGridPosition(1, 2, mTmpPoint);
} else if (index >= MAX_NUM_ITEMS_IN_PREVIEW) {
// Items beyond those displayed in the preview are animated to the center
mTmpPoint[0] = mTmpPoint[1] = mAvailableSpace / 2 - (mIconSize * totalScale) / 2;
} else {
// 0 <= index <= 3
// 应用图标相关参数的计算,会走到这里,mTmpPoint是一个float数组,计算完成后会把transX、transY数据存在里面
// 计算过程这里先不展开,等下再详细分析
getPosition(index, curNumItems, mTmpPoint);
}
// 读取mTmpPoint中的数据
transX = mTmpPoint[0];
transY = mTmpPoint[1];
// 更新数据到PreviewItemDrawingParams中
if (params == null) {
params = new PreviewItemDrawingParams(transX, transY, totalScale);
} else {
params.update(transX, transY, totalScale);
}
return params;
}
从上面代码可以看出,在computePreviewItemDrawingParams()方法中,先调用了getPosition()方法来计算各个预览项的排列顺序和位置,然后保存在名称为mTmpPoint的Float数组中,接着从数组总读取出数据,更新数据到各个PreviewItemDrawingParams中
// Launcher3/src/com/android/launcher3/folder/PreviewItemDrawingParams.java
class PreviewItemDrawingParams {
float index;
float transX;
float transY;
float scale;
public FolderPreviewItemAnim anim;
public boolean hidden;
public Drawable drawable;
public WorkspaceItemInfo item;
PreviewItemDrawingParams(float transX, float transY, float scale) {
this.transX = transX;
this.transY = transY;
this.scale = scale;
}
public void update(float transX, float transY, float scale) {
// We ensure the update will not interfere with an animation on the layout params
// If the final values differ, we cancel the animation.
if (anim != null) {
if (anim.finalState[1] == transX || anim.finalState[2] == transY
|| anim.finalState[0] == scale) {
return;
}
anim.cancel();
}
this.transX = transX;
this.transY = transY;
this.scale = scale;
}
}
可以看到在PreviewItemDrawingParams对象中定义了一些跟图标绘制相关的参数,比如index(图标索引)、tarnsX(X轴的偏移量)、scale(缩放系数)等;所以在computePreviewItemDrawingParams()方法中,更新数据到PreviewItemDrawingParams中之后,后面在绘制的时候就可以直接通过PreviewItemDrawingParams对象来获取相关数据来进行绘制了。
现在,我们回到FolderIcon.java的dispatchDraw()方法中继续往下分析:
// Launcher3/src/com/android/launcher3/folder/FolderIcon.java
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (!mBackgroundIsVisible) return;
// 重新计算图标的各项参数
mPreviewItemManager.recomputePreviewDrawingParams();
if (!mBackground.drawingDelegated()) {
mBackground.drawBackground(canvas);
}
if (mCurrentPreviewItems.isEmpty() && !mAnimating) return;
// 调用PreviewItemManager.draw方法绘制文件夹预览图
mPreviewItemManager.draw(canvas);
if (!mBackground.drawingDelegated()) {
mBackground.drawBackgroundStroke(canvas);
}
drawDot(canvas);
}
// ...其他代码
计算完图标的各项参数之后,调用PreviewItemManager.draw()方法将各应用图标预览项绘制出来:
// Launcher3/src/com/android/launcher3/folder/PreviewItemManager.java
public void draw(Canvas canvas) {
int saveCount = canvas.getSaveCount();
// The items are drawn in coordinates relative to the preview offset
PreviewBackground bg = mIcon.getFolderBackground();
Path clipPath = bg.getClipPath();
float firstPageItemsTransX = 0;
if (mShouldSlideInFirstPage) {
PointF firstPageOffset = new PointF(bg.basePreviewOffsetX + mCurrentPageItemsTransX,
bg.basePreviewOffsetY);
boolean shouldClip = mCurrentPageItemsTransX > mClipThreshold;
drawParams(canvas, mCurrentPageParams, firstPageOffset, shouldClip, clipPath);
firstPageItemsTransX = -ITEM_SLIDE_IN_OUT_DISTANCE_PX + mCurrentPageItemsTransX;
}
PointF firstPageOffset = new PointF(bg.basePreviewOffsetX + firstPageItemsTransX,
bg.basePreviewOffsetY);
boolean shouldClipFirstPage = firstPageItemsTransX < -mClipThreshold;
// 调用drawParams()方法
drawParams(canvas, mFirstPageParams, firstPageOffset, shouldClipFirstPage, clipPath);
canvas.restoreToCount(saveCount);
}
public void drawParams(Canvas canvas, ArrayList<PreviewItemDrawingParams> params,
PointF offset, boolean shouldClipPath, Path clipPath) {
// 这里传进来的ArrayList<PreviewItemDrawingParams> params就是每个文件夹中应用图标的List
// The first item should be drawn last (ie. on top of later items)
for (int i = params.size() - 1; i >= 0; i--) {
PreviewItemDrawingParams p = params.get(i);
if (!p.hidden) {
// Exiting param should always be clipped.
boolean isExiting = p.index == EXIT_INDEX;
// 遍历文件夹中每个图标,进行绘制
drawPreviewItem(canvas, p, offset, isExiting | shouldClipPath, clipPath);
}
}
}
/**
* Draws each preview item.
* 关键方法:绘制文件夹预览图中每个图标
* @param offset The offset needed to draw the preview items.
* @param shouldClipPath Iff true, clip path using {@param clipPath}.
* @param clipPath The clip path of the folder icon.
*/
private void drawPreviewItem(Canvas canvas, PreviewItemDrawingParams params, PointF offset,
boolean shouldClipPath, Path clipPath) {
canvas.save();
if (shouldClipPath) {
canvas.clipPath(clipPath);
}
canvas.translate(offset.x + params.transX, offset.y + params.transY);
canvas.scale(params.scale, params.scale);
Drawable d = params.drawable;
if (d != null) {
Rect bounds = d.getBounds();
canvas.save();
canvas.translate(-bounds.left, -bounds.top);
canvas.scale(mIntrinsicIconSize / bounds.width(), mIntrinsicIconSize / bounds.height());
d.draw(canvas);
canvas.restore();
}
canvas.restore();
}
问题解决方法:
到这里,文件夹预览图的计算和绘制流程就走完了。由上面的分析过程可以知道,想要修改文件夹预览图的绘制效果,我们可以在两个地方入手:
- 在预览图中各应用图标位置的计算时做处理
- 在预览图各应用图标的绘制时做处理
思路:在预览图中各应用图标位置的计算时做处理
绘制逻辑详解:
// Launcher3/src/com/android/launcher3/folder/PreviewItemManager.java
public void drawParams(Canvas canvas, ArrayList<PreviewItemDrawingParams> params,
PointF offset, boolean shouldClipPath, Path clipPath) {
// 这里传进来的ArrayList<PreviewItemDrawingParams> params,就是一个文件夹内部需要预览的应用图标列表
// 正常来说,文件夹预览图中只有三种情况:两个应用、三个应用、四个及以上应用(默认的预览图最多就显示四个)
for (int i = params.size() - 1; i >= 0; i--) {
// 遍历文件夹预览图中需要显示的应用图标,调用drawPreviewItem()方法进行绘制
PreviewItemDrawingParams p = params.get(i);
if (!p.hidden) {
// Exiting param should always be clipped.
boolean isExiting = p.index == EXIT_INDEX;
// 遍历文件夹中每个图标,进行绘制
drawPreviewItem(canvas, p, offset, isExiting | shouldClipPath, clipPath);
}
}
}
/**
* Draws each preview item. 绘制每个预览项
*
* @param offset The offset needed to draw the preview items.
* @param shouldClipPath Iff true, clip path using {@param clipPath}.
* @param clipPath The clip path of the folder icon.
*/
private void drawPreviewItem(Canvas canvas, PreviewItemDrawingParams params, PointF offset,
boolean shouldClipPath, Path clipPath) {
// 创建一个保存点,保存当前的Canvas状态
canvas.save();
if (shouldClipPath) {
// 如果有必要,裁剪出一个新区域clipPath作为新的canvas对象绘制的区域
canvas.clipPath(clipPath);
}
// 将canvas的原点移动到指定位置,由offset和params的x、y坐标共同决定
canvas.translate(offset.x + params.transX, offset.y + params.transY);
// 缩放canvas的x、y轴比例
canvas.scale(params.scale, params.scale);
// 获取预览项的Drawable对象
Drawable d = params.drawable;
if (d != null) {
// 获取预览项绘制的边界
Rect bounds = d.getBounds();
// 在前面进行平移和缩放基础上,再次创建一个保存点
canvas.save();
// 再次进行平移,平移的坐标由bounds.left和bounds.top
canvas.translate(-bounds.left, -bounds.top);
// 再次进行缩放,由mIntrinsicIconSize、bounds.width()和bounds.height()
canvas.scale(mIntrinsicIconSize / bounds.width(), mIntrinsicIconSize / bounds.height());
// 进行绘制
d.draw(canvas);
// 回到第二次保存点之前的状态
canvas.restore();
}
// 回到最初始的状态
canvas.restore();
}
上面注释写得比较清楚,在drawParams()方法中可以拿到每个文件夹的预览项列表,通过for循环,调用drawPreviewItem()方法绘制每一个预览项。
在drawPreviewItem()方法中,canvas根据一系列的平移和缩放,最终调用预览项的Drawable.draw(canvas)来进行绘制。这里为了方便分析,我们加一些打印来看看相关的数据:
private void drawPreviewItem(Canvas canvas, PreviewItemDrawingParams params, PointF offset,
boolean shouldClipPath, Path clipPath) {
canvas.save();
if (shouldClipPath) {
canvas.clipPath(clipPath);
}
android.util.Log.d(TAG, "drawPreviewItem: ------------------------");
android.util.Log.d(TAG, "drawPreviewItem: offset.x=" + offset.x + "; offset.y=" + offset.y);
android.util.Log.d(TAG, "drawPreviewItem: params.transX=" + params.transX + "; params.transY=" + params.transY);
android.util.Log.d(TAG, "drawPreviewItem: params.scale=" + params.scale);
canvas.translate(offset.x + params.transX, offset.y + params.transY);
canvas.scale(params.scale, params.scale);
Drawable d = params.drawable;
if (d != null) {
Rect bounds = d.getBounds();
canvas.save();
canvas.translate(-bounds.left, -bounds.top);
canvas.scale(mIntrinsicIconSize / bounds.width(), mIntrinsicIconSize / bounds.height());
android.util.Log.d(TAG, "drawPreviewItem: getIntrinsicWidth=" + d.getIntrinsicWidth());
android.util.Log.d(TAG, "drawPreviewItem: bounds=" + bounds);
d.draw(canvas);
canvas.restore();
}
canvas.restore();
}
相同位置打的log打出结果:可见
offset.x和offset.y以及params.scale为定值
params.transX和params.transY值在改变(重点关注其为负数的值)
大佬文章里有个解释说明
根据大佬所描述的重点为
对于文件夹图标的left边界,如果预览项的params.transX是负数,就可能引发越界;
对于文件夹图标的top边界,如果预览项的params.transY是负数,就可能引发越界;
对于文件夹图标的right边界,如果右侧预览项的params.transX过大,可能引发越界问题;
对于文件夹图标的bottom边界,如果底部预览项params.transY过大,可能引发越界问题。
问题修改:这里知道是由params.transX和params.transY引起的位置偏移,所以改动方式也很简单,就是哪个地方溢出,就针对那个参数做修改:
以三个应用的情况来说:上面的图标顶部溢出,是由于params.transY为负数引起的,所以可以在绘制前判断一下,如果params.transY为负数,就将其设为0或者取反,让它在绘制时往下移动,同时为了避免与下面的图标出现重叠,下面的两个图标也要跟着改变params.transY的值(与第一个应用偏移量相同)。
第二行的两个应用,与文件夹左右两边距离文件夹边框太近,是因为左边图标的params.transX为负数(向左偏移),而右边图标的params.transX又过大(向右偏移)导致的,这种情况下就可以增加判断,如果params.transX是负数(说明是第二行的左边应用),那么可以将params.transX设为0或者取反;如果params.transX>0(说明是第二行的右边应用),则将params.transX的值适当减少一些,这样就可以使三个应用比较均匀地分布在文件夹图标中了。
结合上面内容分析,可以知道,在文件夹中包含两个和三个应用预览项时,下面这几项的数据都是一直不变的:
// 本项目中设定的FolderIcon的宽高:316x271
// 文件夹预览图在FolderIcon中的偏移量
offset.x=75.0; offset.y=15.0
// 预览项默认的缩放比例
params.scale=0.4675
// 预览项Drawable对象的固有宽度(这里应该是该项目设定了普通应用图标的size=60dp,且dip=480,所以获取到的固有宽度是(480/160)*60=180px)
getIntrinsicWidth=180
// 预览项Deawable对象的边界,这里可以得知bounds.left和bounds.top都是0
bounds=Rect(0, 0 - 180, 180)
接下来还有个问题,绘制PreviewItemDrawingParams时都是单独绘制自己的,那么怎么知道当前是两个或者三个应用的情况且需要修改呢?可以看下drawParams方法:
// Launcher3/src/com/android/launcher3/folder/PreviewItemManager.java
public void drawParams(Canvas canvas, ArrayList<PreviewItemDrawingParams> params,
PointF offset, boolean shouldClipPath, Path clipPath) {
// 这里传进来的ArrayList<PreviewItemDrawingParams> params,就是一个文件夹内部需要预览的应用图标列表
// 正常来说,文件夹预览图中只有三种情况:两个应用、三个应用、四个及以上应用(默认的预览图最多就显示四个)
for (int i = params.size() - 1; i >= 0; i--) {
// 遍历文件夹预览图中需要显示的应用图标,调用drawPreviewItem()方法进行绘制
PreviewItemDrawingParams p = params.get(i);
if (!p.hidden) {
// Exiting param should always be clipped.
boolean isExiting = p.index == EXIT_INDEX;
// 遍历文件夹中每个图标,进行绘制
drawPreviewItem(canvas, p, offset, isExiting | shouldClipPath, clipPath);
}
}
}
在这个方法中,其实是有一个for循环在遍历ArrayList<PreviewItemDrawingParams> params的内容,这个params就是一个文件夹中所需要绘制的预览项,而在这个类里面,drawPreviewItem()方法又只有这一个入口(没有其他地方会调用,所以是唯一的入口),所以我们可以在这里,自定义一个方法(或者在原来的drawPreviewItem方法中增加多一个参数),将params的size传递进去,根据params的size值来处理不同情况:
// Launcher3/src/com/android/launcher3/folder/PreviewItemManager.java
public void drawParams(Canvas canvas, ArrayList<PreviewItemDrawingParams> params,
PointF offset, boolean shouldClipPath, Path clipPath) {
// Solves the problem of icon boundary overflow when the number of ICONS in the folder is 2 or 3.
// add by 20240717
int size = params.size();
if (size <= 3){
// 当文件夹中的应用预览项为2或者3的时候,调用自定义的方法
for (int i = params.size() - 1; i >= 0; i--) {
PreviewItemDrawingParams p = params.get(i);
if (!p.hidden) {
boolean isExiting = p.index == EXIT_INDEX;
drawPreviewItemOfCustomer(canvas, p, offset, isExiting | shouldClipPath, clipPath, size);
}
}
} else {
// 其他情况不变,还是调用之前的方法
for (int i = params.size() - 1; i >= 0; i--) {
PreviewItemDrawingParams p = params.get(i);
if (!p.hidden) {
// Exiting param should always be clipped.
boolean isExiting = p.index == EXIT_INDEX;
drawPreviewItem(canvas, p, offset, isExiting | shouldClipPath, clipPath);
}
}
}
}
/**
* 自定义的方法,逻辑与drawPreviewItem()方法类似,只是在params.size <= 3时,对params.transX和params.transY做一些调整
* Solves the problem of icon boundary overflow when the number of ICONS in the folder is 2 or 3.
* add by 20240717
*/
private void drawPreviewItemOfCustomer(Canvas canvas, PreviewItemDrawingParams params, PointF offset,
boolean shouldClipPath, Path clipPath, int size) {
canvas.save();
if (shouldClipPath) {
canvas.clipPath(clipPath);
}
// Recalculate the starting point of the drawing
if (size == 2){
// 当文件夹内有两个应用时,只需要调整两个应用的params.transX即可
float finallyX = 0;
if (params.transX > 0) {
// 右边的应用就向左偏移7
finallyX = params.transX - 7;
}
// 左边的params.transX原本为负数,这里直接处理变成0
android.util.Log.d(TAG, "drawPreviewItem: finallyX=" + finallyX);
canvas.translate(offset.x + finallyX, offset.y + params.transY);
} else if (size == 3) {
float finallyY = 0;
if (params.transY > 0) {
// 第二行的应用,就往下偏移12
finallyY = params.transY + 12;
}
// 第一行应用的params.transY原本为负数,这里直接处理变成0
float finallyX = 0;
if (params.transX > 0 && params.transY > 0) {
// 第二行右边的应用, 向左偏移
finallyX = params.transX - 6;
} else if (params.transX > 0 && params.transY < 0) {
// 第一行的应用,已经居中了,不需要处理
finallyX = params.transX;
}
// 第二行左边应用的params.transX原本为负数,这里直接处理变成0
android.util.Log.d(TAG, "drawPreviewItem: finallyX=" + finallyX);
android.util.Log.d(TAG, "drawPreviewItem: finallyY=" + finallyY);
canvas.translate(offset.x + finallyX, offset.y + finallyY);
} else {
// size不等于2或者3的情况,就正常处理
canvas.translate(offset.x + params.transX, offset.y + params.transY);
}
// 下面的缩放和绘制流程不变
canvas.scale(params.scale, params.scale);
Drawable d = params.drawable;
if (d != null) {
Rect bounds = d.getBounds();
canvas.save();
canvas.translate(-bounds.left, -bounds.top);
canvas.scale(mIntrinsicIconSize / bounds.width(), mIntrinsicIconSize / bounds.height());
d.draw(canvas);
canvas.restore();
}
canvas.restore();
}
可以看到,修改完之后,预览图标都比较均匀地分布在文件夹中了。