Launcher3 一鍵改變Icon Shape 原理淺析
在Android O Launcher3 Google 團隊增加了一個新特性,可以在設置里面更改 桌面Icon 形狀,分別可以改為系統默認、方形、方圓形、圓形、淚珠形。
在Android P Launcher3 Google團隊繼續保持這一神奇特性,那么,看上去好高大上神奇的特性是怎樣實現的呢?帶著這個疑問,follow me》》》》》
下面我們基于Android P Launcher3 分析Launcher3 實現基本原理。
一.先看桌面設置中的菜單實現:
源碼位置 Launcher3\src\com\android\launcher3\SettingsActivity.javaPreference 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 enabledif (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 supportedreturn 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;}@Overridepublic boolean onPreferenceChange(Preference preference, Object o) {String newValue = (String) o;if (!getAppliedValue(mContext).equals(newValue)) {// Value has changedProgressDialog.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;}@Overridepublic 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 ittry {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 processandroid.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;}// magictry {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@Overridepublic 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 LauncherAppsServicefinal int iconRes = mActivityInfo.getIconResource();Drawable icon = null;// Get the preferred density icon from the app's resourcesif (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 iconif (icon == null) {icon = mActivityInfo.loadIcon(mPm);}return icon;
}
通過以上步驟可以看出,Launcher獲取應用圖標的時候時候,如果該應用是支持AdaptiveIcon的話,返回的圖標就是根據形狀裁剪出來的AdaptiveIconDrawable,Launcher從系統拿到的圖標已經是想要的形狀圖標了。
這就是在把launcher進程kill掉,重啟 launcher 重新獲取加載就是被裁減過的Icon形狀了