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.javapublic 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.javapublic 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,注意参数传的是falseupdatePreviewItems(false);}
}void updatePreviewItems(boolean animate) {int numOfPrevItemsAux = mFirstPageParams.size();// 调用buildParamsForPage方法,第三个参数是falsebuildParamsForPage(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中的itemsparams.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 animationsif (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.javaprivate 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 3getGridPosition(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 centermTmpPoint[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.javapublic void draw(Canvas canvas) {int saveCount = canvas.getSaveCount();// The items are drawn in coordinates relative to the preview offsetPreviewBackground 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.javapublic 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.topcanvas.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.javapublic 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.javapublic 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 20240717int 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 drawingif (size == 2){// 当文件夹内有两个应用时,只需要调整两个应用的params.transX即可float finallyX = 0;if (params.transX > 0) {// 右边的应用就向左偏移7finallyX = params.transX - 7;}// 左边的params.transX原本为负数,这里直接处理变成0android.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) {// 第二行的应用,就往下偏移12finallyY = params.transY + 12;}// 第一行应用的params.transY原本为负数,这里直接处理变成0float 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原本为负数,这里直接处理变成0android.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();
}

可以看到,修改完之后,预览图标都比较均匀地分布在文件夹中了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/421632.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于亲和性的 GPU 容器绑核策略 Copy

1.引言 在高性能计算和大规模并行任务处理中&#xff0c;GPU已经成为不可或缺的加速器。为了充分发挥GPU的计算能力&#xff0c;通过合理分配CPU核与GPU的绑定来优化CPU和GPU的关系至关重要。我们将探讨socket和NUMA&#xff08;非统一内存访问&#xff09;的概念&#xff0c;并…

力扣 — — 2555. 两个线段获得的最多奖品

力扣 — — 2555. 两个线段获得的最多奖品 一、题目描述 题目大意&#xff1a;给定一个数组prizePositions&#xff0c;数组中的值表示的是奖品的位置&#xff0c;每一个位置可以有多个奖品&#xff0c;并且设定一个线段的长度 K K K&#xff0c;要求从所有奖品位置中选择两个…

springboot 整合quartz定时任务

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言一、pom的配置1.加注解二、使用方法1.工程图2.创建工具类三、controller 实现前言 提示:这里可以添加本文要记录的大概内容: 提示:以下是本篇文章正文内容,下面案例可供参考 一、pom的配…

【RabbitMQ】工作模式

工作模式概述 简单模式 简单模式中只存在一个生产者&#xff0c;只存在一个消费者。生产者生产消息&#xff0c;消费者消费消息。消息只能被消费一次&#xff0c;也称为点对点模式。 简单模式适合在消息只能被单个消费者处理的场景下存在。 工作队列模式&#xff08;Work Qu…

Redisson分布式锁实现及原理详解

随着技术快速发展&#xff0c;数据规模增大&#xff0c;分布式系统越来越普及&#xff0c;一个应用往往会部署在多台机器上&#xff08;多节点&#xff09;&#xff0c;在有些场景中&#xff0c;为了保证数据不重复&#xff0c;要求在同一时刻&#xff0c;同一任务只在一个节点…

浏览器中的JavaScript核心BOM(浏览器对象模型)重点掌握对象之History对象的属性与方法

History对象是用来把网页浏览历史用类似栈的方式进行表示。 这定义听起来非常的抽象&#xff0c;其实History对象的作用就跟浏览器的前进和后退很像&#xff0c;我们来用几幅图来理解一下。首先我们先回顾一下浏览器的返回上一个页面 和 跳转到下一个页面 这两个功能。 就类似…

JDBC使用

7.2 创建JDBC应用 7.2.1 创建JDBC应用程序的步骤 使用JDBC操作数据库中的数据包括6个基本操作步骤&#xff1a; &#xff08;1&#xff09;载入JDBC驱动程序&#xff1a; 首先要在应用程序中加载驱动程序driver&#xff0c;使用Class.forName()方法加载特定的驱动程序&#xf…

【题解单调队列优化dp】划分

划分 分析&#xff1a; 首先&#xff0c;我们目光着眼于部分分 我们尝试用 O ( n 3 ) O(n^3) O(n3)的朴素dp去解决这个问题 f [ i ] [ j ] 表示划分到第 i 个位置&#xff0c;且上一个位置是 j 的最小运行时间是多少 f[i][j]表示划分到第i个位置&#xff0c;且上一个位置是j的…

erlang学习: Mnesia Erlang数据库3

Mnesia数据库删除实现和事务处理 -module(test_mnesia). -include_lib("stdlib/include/qlc.hrl").-record(shop, {item, quantity, cost}). %% API -export([insert/3, select/0, select/1, delete/1, transaction/1,start/0, do_this_once/0]). start() ->mnes…

自然语言处理系列六十九》搜索引擎项目实战》搜索框架技术选型

注&#xff1a;此文章内容均节选自充电了么创始人&#xff0c;CEO兼CTO陈敬雷老师的新书《自然语言处理原理与实战》&#xff08;人工智能科学与技术丛书&#xff09;【陈敬雷编著】【清华大学出版社】 文章目录 自然语言处理系列六十九搜索引擎项目实战》搜索框架技术选型搜索…

(k8s)kubernetes 挂载 minio csi 的方式

一、安装Minio&#xff08;Minio分布式集群搭建部署_minio集群最少几台-CSDN博客&#xff09; 生成accessKeyID和secretAccessKey&#xff1a; 二、安装csi-s3插件(在k8s集群上) 首先我们把插件的yaml文件都下载下来&#xff0c;为了保证版本测试的一致性&#xff0c;我们下载…

828华为云征文|基于华为云Flexus云服务器X搭建jumpserver堡垒机软件

文章目录 ❀前言❀jumpserver堡垒机概述❀环境准备❀部署说明❀在线安装❀浏览器访问❀资产添加❀资产授权❀资产登录❀总结 ❀前言 近期华为云推出了最新的华为云Flexus云服务器X&#xff0c;这款云主机在算柔性算力做出了重大变革。华为云Flexus云服务器X基于擎天QingTian架…

QT设置闹钟超时播报

头文件 #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include<QTimerEvent> #include<QTime> #include<QTextToSpeech>QT_BEGIN_NAMESPACE namespace Ui { class Widget; } QT_END_NAMESPACEclass Widget : public QWidget {Q_OBJECTpublic…

一个基于Spring Boot 3、Vue 3 和 Element-Plus 的中后台管理框架,流畅、直观且功能强大

前言 当前市面上的中后台管理系统虽然种类繁多&#xff0c;但在实际使用中仍存在不少痛点&#xff0c;比如技术栈陈旧、性能低下、扩展性差等问题。开发者们常常需要花费大量的时间和精力去处理这些问题&#xff0c;而不是专注于业务逻辑本身。 那么&#xff0c;有没有一个框…

计算赎金信

给你两个字符串&#xff1a;ransomNote 和 magazine &#xff0c;判断 ransomNote 能不能由 magazine 里面的字符构成。如果可以&#xff0c;返回 true &#xff1b;否则返回 false 。magazine 中的每个字符只能在 ransomNote 中使用一次。 示例 1&#xff1a; 输入&#xff…

使用3DUNet训练自己的数据集(pytorch)— 医疗影像分割

代码:lee-zq/3DUNet-Pytorch: 3DUNet implemented with pytorch (github.com) 文章<cicek16miccai.pdf (uni-freiburg.de)3D U-Net: Learning Dense Volumetric Segmentation

HarmonyOS学习(十)——网络编程

文章目录 1、通过HTTP请求网络2、Web组件2.1、加载本地网页2.2、加载在线网页2.3、网页缩放2.4、文本缩放2.5、web组件事件以及状态说明2.6、处理页面导航 1、通过HTTP请求网络 官方API文档地址&#xff1a;HTTP数据请求-Network Kit数据传输能力-Network Kit&#xff08;网络…

Linux 下 C/C++ 程序编译的过程

目录 一、GCC 工具链二、编译过程1、预处理2、编译3、汇编4、链接 本文将介绍如何将 C/C 语言编写的程序转换成为处理器能够执行的二进制代码的过程&#xff0c;包括四个步骤&#xff1a;预处理&#xff08;Preprocessing&#xff09;编译&#xff08;Compilation&#xff09;汇…

Qt_自定义信号

目录 1、自定义信号的规定 2、创建自定义信号 3、带参数的信号与槽 4、一个信号连接多个槽 5、信号与槽的断开 结语 前言&#xff1a; 虽然Qt已经内置了大量的信号&#xff0c;并且这些信号能够满足大部分的开发场景&#xff0c;但是Qt仍然允许开发者自定义信号&#…

ARMxy嵌入式边缘计算控制器支持Linux OS应用于AIOT

人工智能与物联网&#xff08;AIoT&#xff09;的融合正深刻改变着各个行业。而在这一变革中&#xff0c;ARMxy 嵌入式控制器以其卓越的性能和对 Linux OS 的支持&#xff0c;成为了 AIoT 应用的关键推动力量。 一、ARMxy 嵌入式控制器的优势 强大的处理能力 ARMxy 嵌入式控制…