美文网首页
Android O Launcher3新特性(图标形状)

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

作者: LeongAndroid | 来源:发表于2017-11-09 15:28 被阅读0次

    之前我简单介绍过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的代码了,将设置的参数公开给外部应用。

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

    相关文章

      网友评论

          本文标题:Android O Launcher3新特性(图标形状)

          本文链接:https://www.haomeiwen.com/subject/xytmmxtx.html