Android Launcher图标定制
原理
利用Android多用户的特性,对于不同的用户展示不同的桌面。所以需要在Launcher初始化的时候判断当前user。这个需要在framework中添加接口判断,不详说。
这样,就有了两套桌面主题,一套的要求是六边形的图标展示,文件夹预览是3个图标拼接(见下图),另一套就是比较常见的圆角图标展示加上文件夹9个预览图排列。
先上效果图压阵
六边形图标桌面 圆角图标桌面需求分析
- 首先是系统预置应用,这个UI的同事会提供相关的美化过的图片,我们需要做的就是用这些图标替换apk中的图片。
- 其次是用户安装的第三方应用,那么我们需要把第三方的应用转换成和主题(圆角或者六角)匹配的样子。
具体实现
桌面图标的加载
通过Launcher源码分析,加载桌面图标在IconCache(packages/apps/Launcher3/src/com/android/launcher3/IconCache.java)文件中的cacheLocked函数,这个函数做的是:1.先查询IconCache的缓存中是否存在,如果存在就直接从缓存中取出图标,如果不存在,则读取Launcher数据库,如果数据库存在,则把相关图标从数据库中读出来并保存到IconCache的缓存中,如果数据库不存在,则从系统中的packageManager取相关的图标信息。
根据我们的需求,我们需要在从系统中取相关图标信息的地方改动。分两种情况:
- 预置应用:我们会把UI改动好的图标放到Launcher res目录下面,通过包名匹配,直接替换(当然也要区分user,不用用户取不同的图标)。
private Bitmap getImdataDefaultThemeIcon(ComponentName componentName, UserHandleCompat user) {
// We don't keep icons for other profiles in persistent cache.
// We should change the icon according to cloudminds ui design.
defaultThemeIcon = null;
Resources r = mContext.getResources();
String resName = getImdataDefaultThemeResId(componentName, user);
String packageName = mContext.getPackageName();
final int resId = r.getIdentifier(resName, "drawable", packageName);
defaultThemeIcon = BitmapFactory.decodeResource(r, resId);
if (defaultThemeIcon == null) {
Log.w(TAG, "getImdataDefaultThemeIcon no find default theme icon for " + resName);
}
return defaultThemeIcon;
}
private static String getImdataDefaultThemeResId(ComponentName component, UserHandleCompat user) {
String resourceName = component.flattenToShortString();
String filename = resourceName.replace(File.separatorChar, '_');
String filePrefix = RESOURCE_FILE_PREFIX;
filename = filename.replace('.', '_');
filename = filename.toLowerCase();
if (user.isEodUser()) {
filePrefix = EOS_RES_FILE_PREFIX;
}
return filePrefix + filename;
}
- 第三方应用:
通过在Utilities.java中添加createNonPreloadAppIconBitmap函数来统一处理。思路是在原apk的图标上添加蒙版,针对两个用户,分别是圆角蒙版或者六角蒙版。
圆角蒙版
圆角的比较简单,网上也有好几种版本,我们这里采用的是利用PorterDuff.Mode.SRC_IN来叠加生成新的图标。
mask = BitmapFactory.decodeResource(context.getResources(), R.drawable.mask);
int width = mask.getWidth();
int height = mask.getHeight();
Bitmap bitmapScale = Bitmap.createScaledBitmap(bitmap, width, height, true);
result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
canvas.setBitmap(result);
canvas.drawBitmap(mask, 0, 0, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(bitmapScale, 0, 0, paint);
六角的话则要麻烦点,因为现在的应用图标大部分是方形的,不能只是简单的叠加,所以我们采用的方法是先在原图标中截取一个圆形区域出来,然后把这个圆形区域放到六角形蒙蔽中间合成一个新的图标。
final int iconBitmapSize = DEFAULT_ICON_SIZE;
result = Bitmap.createBitmap(iconBitmapSize, iconBitmapSize,
Bitmap.Config.ARGB_8888);
canvas.setBitmap(result);
mask = BitmapFactory.decodeResource(context.getResources(),
R.drawable.thirdapp_bg_gray);
int backWidth = MASK_ICON_WIDTH;
int backHeight = MASK_ICON_HEIGHT;
Rect dst = new Rect();
dst.left = ICON_BACKGROUND_PADDING_LEFT;
dst.top = 0;
dst.right = backWidth;
dst.bottom = backHeight - ICON_BACKGROUND_PADDING_BOTTOM;
canvas.setDrawFilter(new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG));
canvas.drawBitmap(mask, null, dst, null);
Bitmap bmp = toRoundBitmap(drawableToBitmap(icon));
Drawable drawableIcon = new BitmapDrawable(context.getResources(), bmp);
drawableIcon.setBounds(ICON_LEFT_APP, ICON_TOP_APP, ICON_RIGHT_APP, ICON_BOTTOM_APP);
drawableIcon.draw(canvas);
toRoundBitmap函数主要是截取圆形区域,主要用canvas的drawCircle和之前圆角的PorterDuff.Mode.SRC_IN来实现,剩下的就是根据图标的宽高来定圆的半径。(参看效果图中的讯飞输入法图标)
文件夹的显示
Launcher中FolderIcon.java负责文件夹的显示,针对我们的场景,我们定义了两种不同的folder layout(folder_icon_3x3 -- 9图标圆角文件夹, folder_icon_eod_3 -- 3图标六角文件夹)
3x3这个布局的核心就是3个LinearLayout,每个里面横行3个图标。
eod_3这个布局的核心是2个LinearLayout纵向排列,第一个LinearLayout居中放置一个图标,第二个LinearLayout水平放置两个图标。
当然,在核心布局外面要包裹一个设置了背景图片的LinearLayout,来实现文件夹是圆角边框还是六角边框。
对于3x3的圆角显示这个比较简单不做详说,下面具体说一下六角这个预览3图标的展示。这个需要把3个图标拼装到一起,达到效果图的样子。
FolderIcon中dispatchDraw方法负责复制文件夹图标,其中调用针对文件夹预览图标的个数,循环调用drawPreviewItem方法,我们要做的就是针对不同的图标的index,调整显示图标的坐标。即调整预览图标的其实x/y,最后调用translate函数移动到画布的合的位置canvas.translate(transX, transY)。
//margin between hexagons
float margin = mViewHeight * (1 - CUSTOM_HEXAGON_W_H_RATIO);
//offset to the centre
float offset = (float) (margin/2 / Math.sin(Math.PI/3));
if (index == 0) {//上面的一个图标的x,y偏移
transX = mTotalWidth/2 - mPreviewIconSize/2;
transY = (int) (getPaddingTop() + mLauncher.getDeviceProfile().iconSizePx/2 - mViewHeight - offset);
} else {//下面两个图标的x,y偏移
transX = mTotalWidth/2 + mPreviewIconSize * (index - 2);
transY = (int) (getPaddingTop() + mLauncher.getDeviceProfile().iconSizePx/2 - mViewHeight/4 + offset);
}
说明:
- 这个CUSTOM_HEXAGON_W_H_RATIO = 0.9137f是UI给我们的六级文件夹的宽高比,为了显示效果,给的不是一个正方形的图,而是高要比宽大些,即瘦长些。所以margin主要是算垂直y方向的偏移。
- offset的计算:在预览图中3个图标相交的边,我们把它们认为是包裹在一个圆中的3个连接圆心的点,那么offset就是这个圆的半径,margin是横向宽少对高的长度,因为是按照中间对称,所以除以2,在除上sin60就是半径,画个图就清楚了,我这里偷懒了哈。
- 其实就是一个公式值,最后还是要在这个基础上,根据实际的图微调一下偏移,但是思路是这样的。有了思路就慢慢调整即可。