Android O Launcher3新特性(圖標(biāo)形狀)

之前我簡(jiǎn)單介紹過(guò)AdaptiveIcon的適配方式和實(shí)現(xiàn)原理避乏,今天主要介紹下在Launcher中是如何實(shí)現(xiàn)切換圖標(biāo)形狀的赵抢。

Launcher設(shè)置圖標(biāo)形狀

先看下SettingsActivity.java中的菜單實(shí)現(xiàn)

Preference iconShapeOverride = findPreference(IconShapeOverride.KEY_PREFERENCE);
            if (iconShapeOverride != null) {
                if (IconShapeOverride.isSupported(getActivity())) {
                    IconShapeOverride.handlePreferenceUi((ListPreference) iconShapeOverride);
                } else {
                    getPreferenceScreen().removePreference(iconShapeOverride);
                }
            }

由此可以看到isSupported方法是是否支持設(shè)置圖標(biāo)形狀的判斷條件枣抱。

public static boolean isSupported(Context context) {
        ///1.判斷系統(tǒng)SDK 版本是否>=26
        if (!Utilities.isAtLeastO()) {
            return false;
        }
        // Only supported when developer settings is enabled
        ///2.是否打開了開發(fā)者選項(xiàng)解愤。如果開發(fā)者選項(xiàng)沒打開骤素,就看不到這個(gè)菜單家淤。
        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.
                /// 3.大概意思就是獲取不到mSystem异剥,如果獲取不到,說(shuō)明當(dāng)前系統(tǒng)存在問(wèn)題
                return false;
            }
        } catch (Exception e) {
            // Ignore, not supported
            return false;
        }
        ///4. 獲取系統(tǒng)中config_icon_mask的resource id
        return getConfigResId() != 0;
    }

注意點(diǎn)就是android 8.0設(shè)備要打開開發(fā)者選項(xiàng)一般就會(huì)有此功能絮重,說(shuō)明支持AdaptiveIcon.

菜單出現(xiàn)后冤寿,我們選擇其中一種形狀來(lái)設(shè)置。

<!-- Values for icon shape overrides. These should correspond to entries defined
     in icon_shape_override_paths_names -->
    <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>

打開可以看到一個(gè)形狀對(duì)應(yīng)的value 就是一個(gè)矢量圖的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è)置的時(shí)候執(zhí)行上面代碼督怜,主要將設(shè)置的保存到本地,清除圖標(biāo)緩存狠角,然后重啟launcher号杠。

如何改變Launcher上的圖標(biāo)的

我們?cè)倏聪律厦嬖O(shè)置的圖標(biāo)形狀的value到底是怎么使用的,如何使圖標(biāo)變化的

我們找到LauncherProvider 的onCreate方法里面使用的地方丰歌。

IconShapeOverride.apply(getContext());

看看這個(gè)apply方法:

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

這個(gè)方法是反射系統(tǒng)Resources中mSystem變量辣吃。

上面大概的意思就是Launcher中將Resources 的mSystem設(shè)置成了ResourcesOverride對(duì)象,
也就是說(shuō)Resources的getSystem方法獲取的是我們重寫的ResourcesOverride芬探,當(dāng)調(diào)用getString方法的時(shí)候,走的也是重寫的方法厘惦。getString方法里面判斷了如果string id 是config_icon_mask這個(gè)的時(shí)候偷仿,返回我們傳入的mOverrideValue,這個(gè)mOverrideValue就是用戶選擇的圖標(biāo)形狀值宵蕉。

/**
     * Return a global shared Resources object that provides access to only
     * system resources (no application resources), and is not configured for
     * the current screen (can not use dimension units, does not change based
     * on orientation, etc).
     */
    public static Resources getSystem() {
        synchronized (sSync) {
            Resources ret = mSystem;
            if (ret == null) {
                ret = new Resources();
                mSystem = ret;
            }
            return ret;
        }
    }

現(xiàn)在回頭看下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)酝静,通過(guò)getString方法,如果id是config_icon_mask羡玛,則返回的是mOverrideValue别智,mOverrideValue就是上面5種里面的一種。
因此稼稿,Launcher獲取應(yīng)用圖標(biāo)的時(shí)候時(shí)候薄榛,如果該應(yīng)用是支持AdaptiveIcon的話,返回的圖標(biāo)就是根據(jù)形狀裁剪出來(lái)的AdaptiveIconDrawable让歼,Launcher從系統(tǒng)拿到的圖標(biāo)已經(jīng)是想要的形狀圖標(biāo)了敞恋。

看下我們Launcher是如何獲取應(yīng)用圖標(biāo)的

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

經(jīng)過(guò)試驗(yàn),系統(tǒng)返回的drawable谋右,就已經(jīng)是我們想要的設(shè)置的形狀圖標(biāo)了硬猫。

Demo驗(yàn)證

下面我自己參考上述的代碼,寫個(gè)獨(dú)立的demo改执,看看獲取的圖標(biāo)啸蜜。我們可以傳任意形狀的圖形,看看返回的圖顯示情況辈挂。

我們將上面的寫在一個(gè)helper類中衬横。代碼如下:

/**
 * Created by LeongAndroid on 2017/11/9.
 */
@TargetApi(Build.VERSION_CODES.O)
public class IconShapeOverrideHelper {

    /**
     * 設(shè)置應(yīng)用的新Resource
     * @param path
     */
    public static void apply(String path) {
        try {
            Resources override =
                    new ResourcesOverride(Resources.getSystem(), getConfigResId(), path);
            getSystemResField().set(null, override);
        } catch (Exception e) {
            // revert value.
            Log.d("IconShapeHelper", "apply exception "+e);
        }
    }

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

    private static int getConfigResId() {
        return Resources.getSystem().getIdentifier("config_icon_mask", "string", "android");
    }

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

    public static Drawable getAppIcon(PackageManager pm, String packname){
        try {
            ApplicationInfo info = pm.getApplicationInfo(packname, 0);
            return info.loadIcon(pm);
        } catch (PackageManager.NameNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();

        }
        return null;
    }

    /**
     * 此方法可以獲取應(yīng)用圖標(biāo)的原始圖
     * @param mPackageManager
     * @param packageName
     * @return
     */
    public static Bitmap getAppIcon2(PackageManager mPackageManager, String packageName) {
        try {
            Drawable drawable = mPackageManager.getApplicationIcon(packageName);

            if (drawable instanceof BitmapDrawable) {
                return ((BitmapDrawable) drawable).getBitmap();
            } else if (drawable instanceof AdaptiveIconDrawable) {
                Drawable backgroundDr = ((AdaptiveIconDrawable) drawable).getBackground();
                Drawable foregroundDr = ((AdaptiveIconDrawable) drawable).getForeground();

                Drawable[] drr = new Drawable[2];
                drr[0] = backgroundDr;
                drr[1] = foregroundDr;

                LayerDrawable layerDrawable = new LayerDrawable(drr);

                int width = layerDrawable.getIntrinsicWidth();
                int height = layerDrawable.getIntrinsicHeight();

                Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);

                Canvas canvas = new Canvas(bitmap);

                layerDrawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
                layerDrawable.draw(canvas);

                return bitmap;
            }
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }

        return null;
    }

}

然后再寫個(gè)Activity,通過(guò)標(biāo)準(zhǔn)api來(lái)獲取應(yīng)用圖標(biāo)呢岗,看看顯示什么冕香。

public class AdaptiveIconActivity extends AppCompatActivity {
    private static final String TAG = "AdaptiveIcon";
    private ImageView imageView = null;
    private ImageView imageView1 = null;
    String patch = "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";
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.adaptive_icon_layout);
        IconShapeOverrideHelper.apply(patch);
        imageView = (ImageView)this.findViewById(R.id.image);
        imageView1 = (ImageView)this.findViewById(R.id.image1);
        ///直接用標(biāo)準(zhǔn)接口獲取圖標(biāo)
        Drawable drawable = IconShapeOverrideHelper.getAppIcon(getPackageManager(), "com.leong.testandroido");
        imageView.setImageDrawable(drawable);
        ///圖標(biāo)原始
        Bitmap bitmap = IconShapeOverrideHelper.getAppIcon2(getPackageManager(), "com.leong.testandroido");
        Log.d(TAG, "origin bitmap w = "+bitmap.getWidth()+", h = "+bitmap.getHeight());
        imageView1.setImageBitmap(bitmap);
    }

}

顯示效果如下:


效果圖

上面的圖就是我們返回的圖標(biāo),下面的圖是一個(gè)應(yīng)用的原圖后豫。

Demo 源碼路徑:https://github.com/LeongAndroid/OLauncherNewFeature

總結(jié)

上面的方式我們可以設(shè)想下悉尾,如果Launcher3 將設(shè)置的圖標(biāo)形狀這個(gè)參數(shù)公開出去,那所有其他的應(yīng)用都可以根據(jù)這個(gè)mMask來(lái)獲取跟Launcher3相同形狀的圖標(biāo)挫酿。當(dāng)然构眯,這個(gè)就需要修改下Launcher3的代碼了,將設(shè)置的參數(shù)公開給外部應(yīng)用早龟。

由于作者本人能力有限惫霸,如果文中有錯(cuò)誤的地方猫缭,歡迎指正,十分感謝耙嫉辍2碌ぁ!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末硅卢,一起剝皮案震驚了整個(gè)濱河市射窒,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌将塑,老刑警劉巖脉顿,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異点寥,居然都是意外死亡艾疟,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門敢辩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)蔽莱,“玉大人,你說(shuō)我怎么就攤上這事责鳍∧牍樱” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵历葛,是天一觀的道長(zhǎng)正塌。 經(jīng)常有香客問(wèn)我,道長(zhǎng)恤溶,這世上最難降的妖魔是什么乓诽? 我笑而不...
    開封第一講書人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮咒程,結(jié)果婚禮上鸠天,老公的妹妹穿的比我還像新娘。我一直安慰自己帐姻,他們只是感情好稠集,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著饥瓷,像睡著了一般剥纷。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上呢铆,一...
    開封第一講書人閱讀 49,144評(píng)論 1 285
  • 那天晦鞋,我揣著相機(jī)與錄音,去河邊找鬼。 笑死悠垛,一個(gè)胖子當(dāng)著我的面吹牛线定,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播确买,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼斤讥,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了拇惋?” 一聲冷哼從身側(cè)響起周偎,我...
    開封第一講書人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎撑帖,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體澳眷,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡胡嘿,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了钳踊。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片衷敌。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖拓瞪,靈堂內(nèi)的尸體忽然破棺而出缴罗,到底是詐尸還是另有隱情,我是刑警寧澤祭埂,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布面氓,位于F島的核電站,受9級(jí)特大地震影響蛆橡,放射性物質(zhì)發(fā)生泄漏舌界。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一泰演、第九天 我趴在偏房一處隱蔽的房頂上張望呻拌。 院中可真熱鬧,春花似錦睦焕、人聲如沸藐握。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)猾普。三九已至,卻和暖如春缔御,著一層夾襖步出監(jiān)牢的瞬間抬闷,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留笤成,地道東北人评架。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像炕泳,于是被迫代替她去往敵國(guó)和親纵诞。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

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