Launcher3 一键改变Icon Shape 原理浅析

简介: Launcher3 一键改变Icon Shape 原理浅析

Launcher3 一键改变Icon Shape 原理浅析


在Android O Launcher3 Google 团队增加了一个新特性,可以在设置里面更改 桌面Icon 形状,分别可以改为系统默认、方形、方圆形、圆形、泪珠形。


在Android P Launcher3 Google团队继续保持这一神奇特性,那么,看上去好高大上神奇的特性是怎样实现的呢?带着这个疑问,follow me》》》》》


下面我们基于Android P Launcher3 分析Launcher3 实现基本原理。


一.先看桌面设置中的菜单实现:

源码位置 Launcher3\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方法是是否支持设置图标形状的判断条件。

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

由源码 可以看出 满足几个条件才能看到设置选项

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

设置的时候执行上面代码,主要将设置的保存到本地,清除图标缓存,然后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;
    }
 
    // 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 源代码可以看出大概的意思就是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 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获取应用图标的时候时候,如果该应用是支持AdaptiveIcon的话,返回的图标就是根据形状裁剪出来的AdaptiveIconDrawable,Launcher从系统拿到的图标已经是想要的形状图标了。


这就是在把launcher进程kill掉,重启 launcher 重新获取加载就是被裁减过的Icon形状了


目录
相关文章
|
XML Android开发 数据格式
Android中利用shape属性自定义设置Button按钮
Android中利用shape属性自定义设置Button按钮
246 0
|
Android开发
Android 使用DataBinding时 将布局页面转换为数据绑定布局(Convert to data binding layout) 不出现提示解决办法
Android 使用DataBinding时 将布局页面转换为数据绑定布局(Convert to data binding layout) 不出现提示解决办法
155 0
|
编解码 Android开发
|
XML 前端开发 Android开发
|
XML Android开发 数据格式
Android drawable layer-list 牛刀小试
Android drawable layer-list 牛刀小试
87 0
Android drawable layer-list 牛刀小试
|
API uml Android开发
Android | 深入理解View.post()获取宽高、Window加载View原理
深入理解View.post()获取宽高、Window加载View原理
428 0
|
编解码 Java Android开发
Android加载drawable中图片后自动缩放的原理
Android加载drawable中图片后自动缩放的原理
|
Android开发
Android 动态修改shape
Android 动态修改shape
282 0
Android 动态修改shape
|
Dart 开发者
【Flutter】Image 组件 ( 内存加载 Placeholder | transparent_image 透明图像插件 )
【Flutter】Image 组件 ( 内存加载 Placeholder | transparent_image 透明图像插件 )
330 0
【Flutter】Image 组件 ( 内存加载 Placeholder | transparent_image 透明图像插件 )