Launcher3 一鍵改變Icon Shape 原理淺析

# Launcher3 一鍵改變Icon Shape 原理淺析

在Android O Launcher3 Google 團隊增加了一個新特性,可以在設(shè)置里面更改 桌面Icon 形狀恬总,分別可以改為系統(tǒng)默認讹俊、方形羽资、方圓形、圓形压汪、淚珠形牵囤。
在Android P Launcher3 Google團隊繼續(xù)保持這一神奇特性,那么欣舵,看上去好高大上神奇的特性是怎樣實現(xiàn)的呢擎鸠?帶著這個疑問,follow me》》》》》

1.png
下面我們基于Android P Launcher3 分析Launcher3 實現(xiàn)基本原理缘圈。

一.先看桌面設(shè)置中的菜單實現(xiàn):

源碼位置 Launcher90\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方法是是否支持設(shè)置圖標形狀的判斷條件劣光。

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;
}

由源碼 可以看出 滿足幾個條件才能看到設(shè)置選項

1.判斷系統(tǒng)SDK 版本是否>=26

2.是否打開了開發(fā)者選項。如果開發(fā)者選項沒打開糟把,就看不到這個菜單绢涡。(至于為神馬開發(fā)者模式才可以看到待追蹤!G卜琛雄可!可能讓廠商在適配此特性吧)

3.大概意思就是獲取不到mSystem,如果獲取不到,說明當前系統(tǒng)存在問題滞项。

二.菜單出現(xiàn)后狭归,我們選擇其中一種形狀來設(shè)置:

<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>

發(fā)現(xiàn)每個Item對應(yīng)一個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());
    }
}

設(shè)置的時候執(zhí)行上面代碼文判,主要將設(shè)置的保存到本地过椎,清除圖標緩存,然后kill Launcher process 重啟launcher戏仓。

三.怎樣通過矢量圖工作的:

源碼位置 :Launcher90\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);
    }
}

在根據(jù)源碼看下getSystemResField方法:

private static Field getSystemResField() throws Exception {
Field staticField = Resources.class.getDeclaredField("mSystem");
staticField.setAccessible(true);
return staticField;
}

這個方法是反射系統(tǒng)Resources中mSystem變量赏殃。

小結(jié):

從Launcher 源代碼可以看出大概的意思就是Launcher中將Resources 的mSystem設(shè)置成了ResourcesOverride對象敷待,
也就是說Resources的getSystem方法獲取的是我們重寫的ResourcesOverride,當調(diào)用getString方法的時候仁热,走的也是重寫的方法榜揖。getString方法里面判斷了如果string id 是config_icon_mask這個的時候,返回我們傳入的mOverrideValue抗蠢,這個mOverrideValue就是用戶選擇的圖標形狀值举哟。

2.pmg.jpg

追蹤下 AdaptiveIconDrawable的構(gòu)造方法:

/**
* 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是如何獲取應(yīng)用圖標的:

public Drawable getFullResIcon(LauncherActivityInfo info) {

return mIconProvider.getIcon(info, mIconDpi);
}

 public Drawable getIcon(LauncherActivityInfo info, int iconDpi) {
    return info.getIcon(iconDpi);
}

最終調(diào)用到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獲取應(yīng)用圖標的時候時候,如果該應(yīng)用是支持AdaptiveIcon的話销斟,返回的圖標就是根據(jù)形狀裁剪出來的AdaptiveIconDrawable庐椒,Launcher從系統(tǒng)拿到的圖標已經(jīng)是想要的形狀圖標了。
這就是在把launcher進程kill掉蚂踊,重啟 launcher 重新獲取加載就是被裁減過的Icon形狀了扼睬。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市悴势,隨后出現(xiàn)的幾起案子窗宇,更是在濱河造成了極大的恐慌,老刑警劉巖特纤,帶你破解...
    沈念sama閱讀 217,907評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件军俊,死亡現(xiàn)場離奇詭異,居然都是意外死亡捧存,警方通過查閱死者的電腦和手機粪躬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評論 3 395
  • 文/潘曉璐 我一進店門担败,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人镰官,你說我怎么就攤上這事提前。” “怎么了泳唠?”我有些...
    開封第一講書人閱讀 164,298評論 0 354
  • 文/不壞的土叔 我叫張陵狈网,是天一觀的道長。 經(jīng)常有香客問我笨腥,道長拓哺,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,586評論 1 293
  • 正文 為了忘掉前任脖母,我火速辦了婚禮士鸥,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘谆级。我一直安慰自己烤礁,他們只是感情好,可當我...
    茶點故事閱讀 67,633評論 6 392
  • 文/花漫 我一把揭開白布肥照。 她就那樣靜靜地躺著脚仔,像睡著了一般。 火紅的嫁衣襯著肌膚如雪建峭。 梳的紋絲不亂的頭發(fā)上玻侥,一...
    開封第一講書人閱讀 51,488評論 1 302
  • 那天决摧,我揣著相機與錄音亿蒸,去河邊找鬼。 笑死掌桩,一個胖子當著我的面吹牛边锁,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播波岛,決...
    沈念sama閱讀 40,275評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼茅坛,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了则拷?” 一聲冷哼從身側(cè)響起贡蓖,我...
    開封第一講書人閱讀 39,176評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎煌茬,沒想到半個月后斥铺,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,619評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡坛善,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,819評論 3 336
  • 正文 我和宋清朗相戀三年晾蜘,在試婚紗的時候發(fā)現(xiàn)自己被綠了邻眷。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,932評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡剔交,死狀恐怖肆饶,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情岖常,我是刑警寧澤驯镊,帶...
    沈念sama閱讀 35,655評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站腥椒,受9級特大地震影響阿宅,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜笼蛛,卻給世界環(huán)境...
    茶點故事閱讀 41,265評論 3 329
  • 文/蒙蒙 一洒放、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧滨砍,春花似錦往湿、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,871評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至响逢,卻和暖如春绒窑,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背舔亭。 一陣腳步聲響...
    開封第一講書人閱讀 32,994評論 1 269
  • 我被黑心中介騙來泰國打工些膨, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人钦铺。 一個月前我還...
    沈念sama閱讀 48,095評論 3 370
  • 正文 我出身青樓订雾,卻偏偏與公主長得像,于是被迫代替她去往敵國和親矛洞。 傳聞我的和親對象是個殘疾皇子洼哎,可洞房花燭夜當晚...
    茶點故事閱讀 44,884評論 2 354

推薦閱讀更多精彩內(nèi)容