文章目录
概要
本文记录Launcher的图标修改涉及到的一些技术点,如有错误还请指正,欢迎大家一起探讨。
例如:
如何替换个别图标?替换后图标被放大怎么处理?
如何更换图标圆角?
如何去掉”非自适应“图标默认的白色背景?
图标增加遥控选中状态
替换图标(前)
替换图标后:
一、更换图标圆角样式
修改文件:frameworks/base/core/res/res/values/config.xml
<string name="config_icon_mask" translatable="false">"M50,0 L85,0 C90.42,0 100,4.58 100,15 L100,85 C100,90.42 96.42,100 85,100 L15,100 C10.58,100 0,96.42 0,85 L0,15 C0,10.42 4.42,0 15,0 L50,0 Z"</string>
找到config_icon_mask属性,修改其中的vector资源(M50,0 L85 …这一串)就能改成任意形状。具体大家可以在Android Studio中制作vector矢量图,或者网络搜一搜。
二、替换图标
修改的类:
\packages\apps\Launcher3\src\com\android\launcher3\BubbleTextView.java
BubbleTextView就是图标类,applyIconAndLabel方法中直接替换要更换图标。具体怎么调用到这里的直接在Android Studio中点击相关的方法就能找的调用链,这里就不展开了。
@UiThread
protected void applyIconAndLabel(ItemInfoWithIcon info) {
boolean useTheme = mDisplay == DISPLAY_WORKSPACE || mDisplay == DISPLAY_FOLDER
|| mDisplay == DISPLAY_TASKBAR;
int flags = useTheme ? FLAG_THEMED : 0;
if (mHideBadge) {
flags |= FLAG_NO_BADGE;
}
// 改变图标的样式 begin
BitmapInfo bitmapInfo =
convertBitmapInfoSpecial(info.getTargetComponent().getPackageName());
if (bitmapInfo != null){
info.bitmap = bitmapInfo;
}
// 改变图标的样式 end
FastBitmapDrawable iconDrawable = info.newIcon(getContext(), flags);
mDotParams.appColor = iconDrawable.getIconColor();
mDotParams.dotColor = getContext().getResources()
.getColor(android.R.color.system_accent3_200, getContext().getTheme());
setIcon(iconDrawable);
applyLabel(info);
}
/**
* 根据包名替换app图标
*/
public BitmapInfo convertBitmapInfoSpecial(String pckName){
if (pckName == null || "".equals(pckName)) return null;
if (customIconMap.containsKey(pckName)){
LauncherIcons li = LauncherIcons.obtain(mContext);
Drawable d = ContextCompat.getDrawable(mContext, customIconMap.get(pckName));
Bitmap bitmap = drawableToBitmap(d);
return li.createIconBitmap(bitmap);
}
return null;
}
// 静态代码块中初始话一个哈希表。最好写个公共类,创建一个静态的customIconMap 哈希表,这样各个想用的类都可以调用
static{
// 构造一个包名:资源名的哈希表
customIconMap = new HashMap<>();
customIconMap.put("com.android.settings", com.android.launcher3.icons.R.drawable.ic_launcher_setting); // 比如我们要替换设置应用的图标,在这里put进去其对应的图标资源
}
经过这样修改后,就能替换掉launcher中的图标
2.1 图标被放大
上面的简单替换可能导致图标被放大了,这里再介绍如何解决这个问题。
核心就是convertBitmapInfoSpecial()
方法中,LauncherIcons
类是继承BaseIconFactory
,因此li.createIconBitmap(bitmap);
其实用的是BaseIconFactory类中的createIconBitmap()
来看BaseIconFactory#createIconBitmap(),代码如下
// BaseIconFactory.java
public BitmapInfo createIconBitmap(Bitmap icon) {
if (this.mIconBitmapSize != icon.getWidth() || this.mIconBitmapSize != icon.getHeight()) {
icon = this.createIconBitmap(new BitmapDrawable(this.mContext.getResources(), icon), 1.0F); // 这里的1.0f太大了导致的
}
return BitmapInfo.of(icon, this.mColorExtractor.findDominantColorByHue(icon));
}
因此我们在LauncherIcons中重写该方法:
修改的类:
\packages\apps\Launcher3\src\com\android\launcher3\icons\LauncherIcons.java
// LauncherIcons.java
@Override
public BitmapInfo createIconBitmap(Bitmap icon) {
if (this.mIconBitmapSize != icon.getWidth() || this.mIconBitmapSize != icon.getHeight()) {
icon = this.createIconBitmap(new BitmapDrawable(this.mContext.getResources(), icon), 0.8F); // 缩小点,否则换了图标之后会被放大
}
return BitmapInfo.of(icon, this.mColorExtractor.findDominantColorByHue(icon));
}
经过重写LauncherIcons#createIconBitmap()后,就能解决替换后的图标被放大问题。当然这个不是唯一的方案,可能还有其他的方法。
2.2 图标拖拽的时候被打回原型
关于拖拽的简单介绍,大家可以看下这篇文章:Launcher3拖拽分析(一)——Android13版本
其中拖拽的时候,会创建一个DragView。
DragView:BubbTextView的平替(他们携带的信息是一样的),因为BubbTextView的父布局是ShortcutAndWidgetContainer,如果拖拽到另一个ShortcutAndWidgetContainer是不允许的(跨布局了)。所以创造了一个DragView来代替BubbTextView
因此,在上面我们只是单纯的改了BubbleTextView中的图标内容,还要改下DragView的图标。
具体看DragView#setItemInfo()
方法:
public void setItemInfo(final ItemInfo info) {
if (info.itemType != LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
&& info.itemType != LauncherSettings.Favorites.ITEM_TYPE_SEARCH_ACTION
&& info.itemType != LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
&& info.itemType != LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
return;
}
// Load the adaptive icon on a background thread and add the view in ui thread.
MODEL_EXECUTOR.getHandler().postAtFrontOfQueue(() -> {
Object[] outObj = new Object[1];
int w = mWidth;
int h = mHeight;
Drawable dr = Utilities.getFullDrawable(mActivity, info, w, h,
true /* shouldThemeIcon */, outObj); // 关键就是这个
......
关键就是看 Drawable dr = Utilities.getFullDrawable()这个方法调用了啥,根据调用链依次追踪,最后发现这个
Utilities#loadFullDrawableWithoutTheme()
修改的类:
\packages\apps\Launcher3\src\com\android\launcher3\Utilities.java
private static Drawable loadFullDrawableWithoutTheme(Context context, ItemInfo info,
int width, int height, Object[] outObj) {
ActivityContext activity = ActivityContext.lookupContext(context);
LauncherAppState appState = LauncherAppState.getInstance(context);
// 如果是应用图标。走这里if代码块
if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
LauncherActivityInfo activityInfo = context.getSystemService(LauncherApps.class)
.resolveActivity(info.getIntent(), info.user);
outObj[0] = activityInfo;
return activityInfo == null ? null : LauncherAppState.getInstance(context)
.getIconProvider().getIcon(
activityInfo, activity.getDeviceProfile().inv.fillResIconDpi);
}
......
}
因此,我们还要根据特定的包名,重新返回一个新的Drawable对象,上修改后的代码
private static Drawable loadFullDrawableWithoutTheme(Context context, ItemInfo info,
int width, int height, Object[] outObj) {
ActivityContext activity = ActivityContext.lookupContext(context);
LauncherAppState appState = LauncherAppState.getInstance(context);
if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
LauncherActivityInfo activityInfo = context.getSystemService(LauncherApps.class)
.resolveActivity(info.getIntent(), info.user);
outObj[0] = activityInfo;
Drawable drawableSpecial = getDrawableSpecial(info.getTargetComponent().getPackageName());
if (drawableSpecial == null){
return activityInfo == null ? null : LauncherAppState.getInstance(context)
.getIconProvider().getIcon(
activityInfo, activity.getDeviceProfile().inv.fillResIconDpi);
}else{
return drawableSpecial;
}
}
......
}
public Drawable getDrawableSpecial(String pckName){
if (pckName == null || "".equals(pckName)) return null;
if (customIconMap.containsKey(pckName)){ // customIconMap 最好弄成个静态的放在公共的类中,供大家调用
return ContextCompat.getDrawable(mContext, customIconMap.get(pckName));
}
return null;
}
经过上面的修改后,就能解决拖拽的时候,被替换的图标又打回原型的问题。
三、去掉”非自适应“图标默认的白色背景
自适应图标介绍与制作,参看这篇文章:Android应用制作自适应图标
如果第三方app开发者,并未适配自适应icon图标,则安装后系统会默认添加一个白色背景。
例如:
解决方案一:直接去掉白色背景
修改的类:
\frameworks\libs\systemui\iconloaderlib\src\com\android\launcher3\icons\BaseIconFactory.java
注意这个类是Framework中的源码。
主要就是normalizeAndWrapToAdaptiveIcon方法中,有对是否自适应图标的判断。如果不是自适应图标就要添加背景,同时修改scale;否则直接取出图标其scale来用。 因此直接屏蔽if、else判断,直接取图标本身的scale即可
protected Drawable normalizeAndWrapToAdaptiveIcon(@Nullable Drawable icon,
final boolean shrinkNonAdaptiveIcons, @Nullable final RectF outIconBounds,
@NonNull final float[] outScale) {
if (icon == null) {
return null;
}
float scale = 1f;
// 1、原本是if.else.结构 把其注释掉
/* begin 注释 */
if (shrinkNonAdaptiveIcons && !(icon instanceof AdaptiveIconDrawable)) {
if (mWrapperIcon == null) {
mWrapperIcon = mContext.getDrawable(R.drawable.adaptive_icon_drawable_wrapper)
.mutate();
}
AdaptiveIconDrawable dr = (AdaptiveIconDrawable) mWrapperIcon;
dr.setBounds(0, 0, 1, 1);
boolean[] outShape = new boolean[1];
scale = getNormalizer().getScale(icon, outIconBounds, dr.getIconMask(), outShape);
if (!outShape[0]) {
FixedScaleDrawable fsd = ((FixedScaleDrawable) dr.getForeground());
fsd.setDrawable(icon);
fsd.setScale(scale);
icon = dr;
scale = getNormalizer().getScale(icon, outIconBounds, null, null);
((ColorDrawable) dr.getBackground()).setColor(mWrapperBackgroundColor);
}
} else {
scale = getNormalizer().getScale(icon, outIconBounds, null, null);
}
/* end 注释 */
// 2、打开这个注释,就能去掉图像的白边
// scale = getNormalizer().getScale(icon, outIconBounds, null, null);
outScale[0] = scale;
return icon;
}
此方案会误伤不规则图标:即有些icon本身不是方形的,而是不规则图形,不规则图标本来就要有个白色背景
。
方案二:放大前景图标,达到正好盖住白色背景
既然对于不规则图标,白色背景有存在的必要,且规则图标又不想要白色背景,只有放大前景图标的大小了。
效果如下:
修改的类:
\frameworks\libs\systemui\iconloaderlib\src\com\android\launcher3\icons\FixedScaleDrawable.java
public class FixedScaleDrawable extends DrawableWrapper {
// TODO b/33553066 use the constant defined in MaskableIconDrawable
// private static final float LEGACY_ICON_SCALE = .7f * .6667f;
private static final float LEGACY_ICON_SCALE = 0.86f; // 这个静态变量就是控制前景图标缩放比例的,我这里设置成0.86正好能让前景图标盖住白色背景
private float mScaleX, mScaleY;
public FixedScaleDrawable() {
super(new ColorDrawable());
mScaleX = LEGACY_ICON_SCALE;
mScaleY = LEGACY_ICON_SCALE;
}
......
}
四、图标增加遥控选中状态
修改的类:
\packages\apps\Launcher3\src\com\android\launcher3\BubbleTextView.java
BubbleTextView就是图标的载体(包含一个icon图片、对应的名字)。增加选中背景就是新建一个背景资源文件,同时在BubbleTextView的构造方法中setBackgroundResource()中设置这个资源文件即可
public BubbleTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
......
setBackgroundResource(R.drawable.select_background); // 增加这一句
}
新建资源文件 \packages\apps\Launcher3\res\drawable\select_background.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:state_pressed="true">
<shape>
<solid android:color="#F44336"/>
</shape>
</item>
<item android:state_hovered="true">
<shape>
<solid android:color="#F44336"/>
</shape>
</item>
<item android:state_focused="true">
<shape>
<solid android:color="#F44336"/>
</shape>
</item>
<item android:state_selected="true">
<shape>
<solid android:color="#F44336"/>
</shape>
</item>
<item android:state_selected="false">
<shape>
<solid android:color="#00000000"/>
</shape>
</item>
<item android:state_pressed="false">
<shape>
<solid android:color="#00000000"/>
</shape>
</item>
<item android:state_focused="false">
<shape>
<solid android:color="#00000000"/>
</shape>
</item>
</selector>