Android O Launcher3新特性(图标形状)

之前我简单介绍过AdaptiveIcon的适配方式和实现原理,今天主要介绍下在Launcher中是如何实现切换图标形状的。

Launcher设置图标形状

先看下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) {
        ///1.判断系统SDK 版本是否>=26
        if (!Utilities.isAtLeastO()) {
            return false;
        }
        // Only supported when developer settings is enabled
        ///2.是否打开了开发者选项。如果开发者选项没打开,就看不到这个菜单。
        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,如果获取不到,说明当前系统存在问题
                return false;
            }
        } catch (Exception e) {
            // Ignore, not supported
            return false;
        }
        ///4. 获取系统中config_icon_mask的resource id
        return getConfigResId() != 0;
    }

注意点就是android 8.0设备要打开开发者选项一般就会有此功能,说明支持AdaptiveIcon.

菜单出现后,我们选择其中一种形状来设置。

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

打开可以看到一个形状对应的value 就是一个矢量图的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());
        }
    }

设置的时候执行上面代码,主要将设置的保存到本地,清除图标缓存,然后重启launcher。

如何改变Launcher上的图标的

我们再看下上面设置的图标形状的value到底是怎么使用的,如何使图标变化的

我们找到LauncherProvider 的onCreate方法里面使用的地方。

IconShapeOverride.apply(getContext());

看看这个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;
    }

这个方法是反射系统Resources中mSystem变量。

上面大概的意思就是Launcher中将Resources 的mSystem设置成了ResourcesOverride对象,
也就是说Resources的getSystem方法获取的是我们重写的ResourcesOverride,当调用getString方法的时候,走的也是重写的方法。getString方法里面判断了如果string id 是config_icon_mask这个的时候,返回我们传入的mOverrideValue,这个mOverrideValue就是用户选择的图标形状值。

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

现在回头看下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获取应用图标的时候时候,如果该应用是支持AdaptiveIcon的话,返回的图标就是根据形状裁剪出来的AdaptiveIconDrawable,Launcher从系统拿到的图标已经是想要的形状图标了。

看下我们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;
    }

经过试验,系统返回的drawable,就已经是我们想要的设置的形状图标了。

Demo验证

下面我自己参考上述的代码,写个独立的demo,看看获取的图标。我们可以传任意形状的图形,看看返回的图显示情况。

我们将上面的写在一个helper类中。代码如下:

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

    /**
     * 设置应用的新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;
    }

    /**
     * 此方法可以获取应用图标的原始图
     * @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;
    }

}

然后再写个Activity,通过标准api来获取应用图标,看看显示什么。

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);
        ///直接用标准接口获取图标
        Drawable drawable = IconShapeOverrideHelper.getAppIcon(getPackageManager(), "com.leong.testandroido");
        imageView.setImageDrawable(drawable);
        ///图标原始
        Bitmap bitmap = IconShapeOverrideHelper.getAppIcon2(getPackageManager(), "com.leong.testandroido");
        Log.d(TAG, "origin bitmap w = "+bitmap.getWidth()+", h = "+bitmap.getHeight());
        imageView1.setImageBitmap(bitmap);
    }

}

显示效果如下:


效果图

上面的图就是我们返回的图标,下面的图是一个应用的原图。

Demo 源码路径:https://github.com/LeongAndroid/OLauncherNewFeature

总结

上面的方式我们可以设想下,如果Launcher3 将设置的图标形状这个参数公开出去,那所有其他的应用都可以根据这个mMask来获取跟Launcher3相同形状的图标。当然,这个就需要修改下Launcher3的代码了,将设置的参数公开给外部应用。

由于作者本人能力有限,如果文中有错误的地方,欢迎指正,十分感谢啊!!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,529评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,015评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,409评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,385评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,387评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,466评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,880评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,528评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,727评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,528评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,602评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,302评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,873评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,890评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,132评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,777评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,310评论 2 342

推荐阅读更多精彩内容