Launcher3 一键改变Icon Shape 原理浅析
在Android O Launcher3 Google 团队增加了一个新特性,可以在设置里面更改 桌面Icon 形状,分别可以改为系统默认、方形、方圆形、圆形、泪珠形。
在Android P Launcher3 Google团队继续保持这一神奇特性,那么,看上去好高大上神奇的特性是怎样实现的呢?带着这个疑问,follow me》》》》》
下面我们基于Android P Launcher3 分析Launcher3 实现基本原理。
一.先看桌面设置中的菜单实现:
源码位置 Launcher3\src\com\android\launcher3\SettingsActivity.java Preference iconShapeOverride = findPreference(IconShapeOverride.KEY_PREFERENCE); if (iconShapeOverride != null) { if (IconShapeOverride.isSupported(getActivity())) { IconShapeOverride.handlePreferenceUi((ListPreference) iconShapeOverride); } else { getPreferenceScreen().removePreference(iconShapeOverride); } }
可以看到isSupported方法是是否支持设置图标形状的判断条件。
public static boolean isSupported(Context context) { if (!Utilities.ATLEAST_OREO) { return false; } // Only supported when developer settings is enabled if (Settings.Global.getInt(context.getContentResolver(), Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 1) { return false; } try { if (getSystemResField().get(null) != Resources.getSystem()) { // Our assumption that mSystem is the system resource is not true. return false; } } catch (Exception e) { // Ignore, not supported return false; } return getConfigResId() != 0; }
由源码 可以看出 满足几个条件才能看到设置选项
1.判断系统SDK 版本是否>=26
2.是否打开了开发者选项。如果开发者选项没打开,就看不到这个菜单。(至于为神马开发者模式才可以看到待追踪!!!可能让厂商在适配此特性吧)
3.大概意思就是获取不到mSystem,如果获取不到,说明当前系统存在问题。
二.菜单出现后,我们选择其中一种形状来设置:
<string-array translatable="false" name="icon_shape_override_paths_values"> <item></item> <item>M50,0L100,0 100,100 0,100 0,0z</item> <item>M50,0 C10,0 0,10 0,50 0,90 10,100 50,100 90,100 100,90 100,50 100,10 90,0 50,0 Z</item> <item>M50 0A50 50,0,1,1,50 100A50 50,0,1,1,50 0</item> <item>M50,0A50,50,0,0 1 100,50 L100,85 A15,15,0,0 1 85,100 L50,100 A50,50,0,0 1 50,0z</item> </string-array> <string-array translatable="false" name="icon_shape_override_paths_names"> <!-- Option to not change the icon shape on home screen. [CHAR LIMIT=50] --> <item>@string/icon_shape_system_default</item> <item>Square</item> <item>Squircle</item> <item>Circle</item> <item>Teardrop</item> </string-array>
发现每个Item对应一个path 矢量图的string值。
private static class PreferenceChangeHandler implements OnPreferenceChangeListener { private final Context mContext; private PreferenceChangeHandler(Context context) { mContext = context; } @Override public boolean onPreferenceChange(Preference preference, Object o) { String newValue = (String) o; if (!getAppliedValue(mContext).equals(newValue)) { // Value has changed ProgressDialog.show(mContext, null /* title */, mContext.getString(R.string.icon_shape_override_progress), true /* indeterminate */, false /* cancelable */); new LooperExecuter(LauncherModel.getWorkerLooper()).execute( new OverrideApplyHandler(mContext, newValue)); } return false; } }
private static class OverrideApplyHandler implements Runnable { private final Context mContext; private final String mValue; private OverrideApplyHandler(Context context, String value) { mContext = context; mValue = value; } @Override public void run() { // Synchronously write the preference. prefs(mContext).edit().putString(KEY_PREFERENCE, mValue).commit(); // Clear the icon cache. LauncherAppState.getInstance(mContext).getIconCache().clear(); // Wait for it try { Thread.sleep(PROCESS_KILL_DELAY_MS); } catch (Exception e) { Log.e(TAG, "Error waiting", e); } // Schedule an alarm before we kill ourself. Intent homeIntent = new Intent(Intent.ACTION_MAIN) .addCategory(Intent.CATEGORY_HOME) .setPackage(mContext.getPackageName()) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); PendingIntent pi = PendingIntent.getActivity(mContext, RESTART_REQUEST_CODE, homeIntent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT); mContext.getSystemService(AlarmManager.class).setExact( AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 50, pi); // Kill process android.os.Process.killProcess(android.os.Process.myPid()); } }
设置的时候执行上面代码,主要将设置的保存到本地,清除图标缓存,然后kill Launcher process 重启launcher。
三.怎样通过矢量图工作的:
源码位置 :Launcher3\src\com\android\launcher3\graphics\IconShapeOverride.java
IconShapeOverride.apply(getContext()); private static int getConfigResId() { return Resources.getSystem().getIdentifier("config_icon_mask", "string", "android"); }
public static void apply(Context context) { if (!Utilities.isAtLeastO()) { return; } String path = getAppliedValue(context); if (TextUtils.isEmpty(path)) { return; } if (!isSupported(context)) { return; } // magic try { Resources override = new ResourcesOverride(Resources.getSystem(), getConfigResId(), path); getSystemResField().set(null, override); } catch (Exception e) { Log.e(TAG, "Unable to override icon shape", e); // revert value. prefs(context).edit().remove(KEY_PREFERENCE).apply(); } }
其中ResourcesOverride是继承了Resources,并且重写了getString方法。
private static class ResourcesOverride extends Resources { private final int mOverrideId; private final String mOverrideValue; @SuppressWarnings("deprecated") public ResourcesOverride(Resources parent, int overrideId, String overrideValue) { super(parent.getAssets(), parent.getDisplayMetrics(), parent.getConfiguration()); mOverrideId = overrideId; mOverrideValue = overrideValue; } @NonNull @Override public String getString(int id) throws NotFoundException { if (id == mOverrideId) { return mOverrideValue; } return super.getString(id); } }
在根据源码看下getSystemResField方法:
private static Field getSystemResField() throws Exception { Field staticField = Resources.class.getDeclaredField("mSystem"); staticField.setAccessible(true); return staticField; }
这个方法是反射系统Resources中mSystem变量。
小结:
从Launcher 源代码可以看出大概的意思就是Launcher中将Resources 的mSystem设置成了ResourcesOverride对象,
也就是说Resources的getSystem方法获取的是我们重写的ResourcesOverride,当调用getString方法的时候,走的也是重写的方法。getString方法里面判断了如果string id 是config_icon_mask这个的时候,返回我们传入的mOverrideValue,这个mOverrideValue就是用户选择的图标形状值。
2.pmg.jpg
追踪下 AdaptiveIconDrawable的构造方法:
/**
* The one constructor to rule them all. This is called by all public
* constructors to set the state and initialize local properties.
*/
AdaptiveIconDrawable(@Nullable LayerState state, @Nullable Resources res) { mLayerState = createConstantState(state, res); if (sMask == null) { sMask = PathParser.createPathFromPathData( Resources.getSystem().getString(R.string.config_icon_mask)); } mMask = PathParser.createPathFromPathData( Resources.getSystem().getString(R.string.config_icon_mask)); mMaskMatrix = new Matrix(); mCanvas = new Canvas(); mTransparentRegion = new Region(); }
此方法的Resources.getSystem().getString(R.string.config_icon_mask),通过getString方法,如果id是config_icon_mask,则返回的是mOverrideValue,mOverrideValue就是上面5种里面的一种。
四.Launcher是如何获取应用图标的:
public Drawable getFullResIcon(LauncherActivityInfo info) { return mIconProvider.getIcon(info, mIconDpi); } public Drawable getIcon(LauncherActivityInfo info, int iconDpi) { return info.getIcon(iconDpi); }
最终调用到LauncherActivityInfo的getIcon方法
/**
* Returns the icon for this activity, without any badging for the profile
* @param density The preferred density of the icon, zero for default density. Use * density DPI values from {@link DisplayMetrics}. * @see #getBadgedIcon(int) * @see DisplayMetrics * @return The drawable associated with the activity. */ public Drawable getIcon(int density) { // TODO: Go through LauncherAppsService final int iconRes = mActivityInfo.getIconResource(); Drawable icon = null; // Get the preferred density icon from the app's resources if (density != 0 && iconRes != 0) { try { final Resources resources = mPm.getResourcesForApplication(mActivityInfo.applicationInfo); icon = resources.getDrawableForDensity(iconRes, density); } catch (NameNotFoundException | Resources.NotFoundException exc) { } } // Get the default density icon if (icon == null) { icon = mActivityInfo.loadIcon(mPm); } return icon; }
通过以上步骤可以看出,Launcher获取应用图标的时候时候,如果该应用是支持AdaptiveIcon的话,返回的图标就是根据形状裁剪出来的AdaptiveIconDrawable,Launcher从系统拿到的图标已经是想要的形状图标了。
这就是在把launcher进程kill掉,重启 launcher 重新获取加载就是被裁减过的Icon形状了