Bootstrap

如何应对Android面试官->布局原理与xml解析,手写插件化换肤框架核心实现(下)

前言

上一章我们讲解了布局原理与xml解析流程,以及资源加载流程、插件化换肤思路,可以查看上一章,本章咱们重点代码实现插件化换肤逻辑;

撸码实现

自定义 View 换肤控制

这里我们采用接口的方式,进行自定义 View 的换肤控制

public interface SkinCustomViewSupport {    
    void applySkin();
}

这里是一个预留接口,预留给哪些自定义的 View 且需要支持换肤的 View;

记录皮肤配置信息工具类

这里使用 MMVK、SP、DataStore、Room 等等都可以只要能用来记录当前配置的皮肤信息即可,并且在下次冷启动的时候能获取到即可,我这里使用了 SP;

public class SkinSharedPreference {    
    private static final String SKIN_SHARED = "skins";    
    private static final String KEY_SKIN_PATH = "skin-path";    
    private SharedPreferences mPref;    
    public void init(Context context) {        
        mPref = context.getSharedPreferences(SKIN_SHARED, Context.MODE_WORLD_WRITEABLE);    
    }    
    private SkinSharedPreference() {}        
    public static final class Holder {        
        private static final SkinSharedPreference instance = new SkinSharedPreference();    
    }    
    public static SkinSharedPreference getInstance() {        
        return Holder.instance;    
    }    
    /**     
      * 设置并记录当前生效的皮肤包信息     
      * @param skinPath 皮肤包路径     
      */    
    public void setSkin(String skinPath) {        
        mPref.edit().putString(KEY_SKIN_PATH, skinPath).apply();    
    }    
    /**     
      * 重置     
      */    
    public void reset() {        
        mPref.edit().remove(KEY_SKIN_PATH).apply();    
    }    
    /**     
      * 获取当前缓存的皮肤包信息     
      * @return str     
      */    
    public String getSkin() {        
        return mPref.getString(KEY_SKIN_PATH, null);    
    }
}

通过name获取皮肤中对应的资源值

换肤的本质就是将皮肤包中的同名资源值进行替换,也就是说 :在setTextColor的时候,通过传入的 R.color.white 这个white 找到皮肤包中对应的white 的值,然后传递给 setTextColor;

 通过上一章的讲解,Resources 中持有着 ResourcesImpl,ResourcesImpl 中持有着 AssetManager,我们只要拿到皮肤包的 Resources 就可以获取皮肤包中的资源值;所以我们需要一个 SkinResource 用来记录当前使用的是主 App 的 Resources 还是皮肤包中的 Resources,并通过getIdentifier getResourceEntryName getResourceTypeName来获取对应的资源值;

public class SkinResources {    
    // 原始 app 使用的 resources    
    private Resources mAppResources;    
    // 插件 apk 中的 resources    
    private Resources mSkinResources;
    // 皮肤包的包名    
    private String mSkinPkgName;
    // 当前加载的是不是默认皮肤,true是    
    private boolean isDefaultSkin = true;    
    private volatile static SkinResources instance;    
    public void init(Context context) {        
         mAppResources = context.getResources();   
    }    
    private SkinResources(Context context) {}    
    public static SkinResources getInstance() {        
        return instance;    
    }
    // 重置信息    
    public void reset() {        
        mSkinResources = null;        
        mSkinPkgName = "";        
        isDefaultSkin = true;    
    }
    // 应用传递进来的皮肤    
    public void applySkin(Resources resources, String pkgName) {        
        mSkinResources = resources;        
        mSkinPkgName = pkgName;        
        isDefaultSkin = TextUtils.isEmpty(pkgName) || resources == null;    
    }    
    /**     
      * 1. 通过主 app 中的 resId(R.color/drawable.XX) 获取到自己的名字     
      * 2. 根据名字和类型获取皮肤包中的 ID     
      *     
      * @param resId resId     
      */    
    private int getIdentifier(int resId) {        
        if (isDefaultSkin) {            
            return resId;        
        }        
        //        
        String resName = mAppResources.getResourceEntryName(resId);        
        String resType = mAppResources.getResourceTypeName(resId);        
        return mSkinResources.getIdentifier(resName, resType, mSkinPkgName);    
    }    
    /**     
      * 获取颜色值     
      *     
      * @param resId resId     
      */    
    public int getColor(int resId) {        
        if (isDefaultSkin) {            
            return mAppResources.getColor(resId);        
        }        
        int skinId = getIdentifier(resId);        
        if (skinId == 0) {            
            return mAppResources.getColor(resId);        
        }        
        return mSkinResources.getColor(resId);    
    }    
    public ColorStateList getColorStateList(int resId) {        
        if (isDefaultSkin) {            
            return mAppResources.getColorStateList(resId);        
        }        
        int skinId = getIdentifier(resId);        
        if (skinId == 0) {            
            return mAppResources.getColorStateList(resId);        
        }        
        return mSkinResources.getColorStateList(resId);    
    }    
    /**     
      * 获取 drawable     
      *     
      * @param resId resId     
      */    
    public Drawable getDrawable(int resId) {        
        if (isDefaultSkin) {            
            return mAppResources.getDrawable(resId);        
        }        
        int skinId = getIdentifier(resId);        
        if (skinId == 0) {            
            return mAppResources.getDrawable(resId);        
        }        
        return mSkinResources.getDrawable(resId);    
    }    
    /**     
      * 可能是颜色值,可能是 drawable     
      *     
      * @param resId resId     
      */    
    public Object getBackground(int resId) {       
        String resourceTypeName = mAppResources.getResourceTypeName(resId);        
        if ("color".equals(resourceTypeName)) {            
            return getColor(resId);        
        } else {            
            return getDrawable(resId);        
        }    
    }
}

状态栏的换肤支持

当我们页面进行换肤的时候,我们的状态栏也是应该要跟着整体颜色进行改变的,也就是说我们需要动态的修改对应页面状态栏的颜色,这里写一个工具类,用来切换状态栏

public class SkinThemeUtils {    
    private static final int[] APPCOMPAT_COLOR_PRIMARY_DARK_ATTRS = {android.R.attr.colorPrimaryDark};    
    private static final int[] STATUS_BAR_COLOR_ATTRS = {android.R.attr.statusBarColor, android.R.attr.navigationBarColor};    
    /**     
      * 获得 Theme 中属性中定义的资源 id     
      *     
      * @param context context     
      * @param attrs attrs     
      * @return int[]     
      * */    
    public static int[] getResId(Context context, int[] attrs) {        
        int[] resIds = new int[attrs.length];        
        TypedArray a = context.obtainStyledAttributes(attrs);        
        for (int i = 0; i < attrs.length; i++) {            
            resIds[i] = a.getResourceId(i, 0);        
        }        
        a.recycle();        
        return resIds;    
    }    
    /**     
     *  更新导航栏,状态栏的颜色值     
     *     
     * @param activity  activity 当前页面     
     *     
     * */    
    public static void updateStatusBarColor(Activity activity) {        
        // 不处理 5.0 以下的        
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {            
            return;        
        }        
        // 获得 statusBarColor 与 navigationBarColor        
        // 当与 colorPrimaryDark 不同时 以 statusBarColor 为准        
        int[] resIds = getResId(activity, STATUS_BAR_COLOR_ATTRS);        
        int statusBarColorResId = resIds[0];        
        int navigationBarColor = resIds[1];        
        // 如果直接在 style 中写入固定颜色值(而不是 @color/XXX ) 获得0,不处理,这里需要和业务侧订好规范        
        if (statusBarColorResId != 0) {            
            int color = SkinResources.getInstance().getColor(statusBarColorResId);            
            activity.getWindow().setStatusBarColor(color);        
        } else {            
            int colorPrimaryDarkResId = getResId(activity, APPCOMPAT_COLOR_PRIMARY_DARK_ATTRS)[0];            
            if (colorPrimaryDarkResId != 0) {                
                int color = SkinResources.getInstance().getColor(colorPrimaryDarkResId);                
                activity.getWindow().setStatusBarColor(color);            
            }        
        }        
        if (navigationBarColor != 0) {            
            int color = SkinResources.getInstance().getColor(navigationBarColor);            
            activity.getWindow().setNavigationBarColor(color);        
        }    
    }
}

记录所有需要换肤的View的所有属性

在创建 View 的时候,我们需要记录下这个 view 的所有可以换肤的属性,例如:

<TextView    
    android:id="@+id/storage"    
    android:layout_width="match_parent"    
    android:layout_height="50dp"    
    app:layout_constraintStart_toStartOf="parent"    
    app:layout_constraintEnd_toEndOf="parent"    
    app:layout_constraintTop_toTopOf="parent"    
    android:text="@string/scope_storage"    
    android:textColor="@color/material_on_background_disabled"    
    android:gravity="center"/>

我们要拿到 TextView  的 textColor 属性,并记录下来,以及拿到对应的 id 值 记录下来

static class SkinNameValuePair {    
    // 属性名    
    String attributeName;    
    // 对应资源 id    
    int resId;    
    SkinNameValuePair(String attributeName, int resId) {        
        this.attributeName = attributeName;        
        this.resId = resId;    
    }
}

有时候,一个View 可能会设置多个支持换肤的属性,那么就需要一个集合来封装,这里声明了一个内部类,View 和对应的 List 集合;

public static class SkinView {    
    public View view;    
    // 这个 View 换肤的属性与它对应的 id 的集合    
    List<SkinNameValuePair> skinNameValuePairs;    
    SkinView(View view, List<SkinNameValuePair> skinPairs) {        
        this.view = view;        
        this.skinPairs = skinNameValuePairs;    
    }    
    void applySkin() {        
        applySkinSupport();        
        for (SkinNameValuePair skinPair : skinPairs) {            
            Drawable left = null, top = null, right = null, bottom = null;            
            switch (skinPair.attributeName) {                
                case "background":                    
                    Object background = SkinResources.getInstance().getBackground(skinPair.resId);                    
                    if (background instanceof Integer) {                        
                        view.setBackgroundColor((Integer) background);                    
                    } else {                        
                        ViewCompat.setBackground(view, (Drawable) background);                    
                    }                    
                    break;                
                case "src":                  
                    background = SkinResources.getInstance().getBackground(skinPair.resId);                    
                    if (background instanceof Integer) {                        
                        ((ImageView) view).setImageDrawable(new ColorDrawable((Integer) background));                    
                    } else {                        
                        ((ImageView) view).setImageDrawable((Drawable) background);                    
                    }                    
                    break;                
                case "textColor":                    
                    ((TextView) view).setTextColor(SkinResources.getInstance().getColorStateList(skinPair.resId));                    
                    break;                
                case "drawableLeft":                    
                    left = SkinResources.getInstance().getDrawable(skinPair.resId);                    
                    break;                
                case "drawableTop":                    
                    top = SkinResources.getInstance().getDrawable(skinPair.resId);                    
                    break;                
                case "drawableRight":                    
                    right = SkinResources.getInstance().getDrawable(skinPair.resId);                    
                    break;                
                case "drawableBottom":                    
                    bottom = SkinResources.getInstance().getDrawable(skinPair.resId);                    
                    break;            
            }            
            if (left != null || null != top || right != null || null != bottom) {                
                ((TextView) view).setCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom);            
            }        
        }    
    }    
    private void applySkinSupport() {        
        if (view instanceof SkinCustomViewSupport) {            
            ((SkinCustomViewSupport) view).applySkin();        
        }    
    }
}

接下来,就需要收集当前 View 支持换肤的属性,并将这些支持换肤的 View 的属性封装到一个集合中,整体就是下面这样的一个层级

接下来就是遍历这个 View 的所有要换肤的属性并记录下来;

class SkinAttributeAttrs {    
    private final List<SkinView> mSkinViews = new ArrayList<>();    
    private static final List<String> mAttributes = new ArrayList<>();    
    static {        
        mAttributes.add("background");        
        mAttributes.add("src");       
        mAttributes.add("textColor");        
        mAttributes.add("drawableLeft");        
        mAttributes.add("drawableTop");        
        mAttributes.add("drawableRight");        
        mAttributes.add("drawableBottom");    
    }    
    /**     
      * 寻找所有可以换肤的 view     
      * 以 ?开头的 和 以 @ 开头的属性值     
      */    
    void searchView(View view, AttributeSet attrs) {        
        List<SkinNameValuePair> skinPairs = new ArrayList<>();        
        for (int i = 0; i < attrs.getAttributeCount(); i++) {            
            // 获取属性名            
            String attributeName = attrs.getAttributeName(i);            
            if (mAttributes.contains(attributeName)) {                
                // 获取属性值                
                String attributeValue = attrs.getAttributeValue(i);                
                // 比如 color,以 # 号写死的颜色
                // 不支持换肤 android:textColor="#FFFFFFFF" 这种不知道插件皮肤对应的值               
                if (attributeValue.startsWith("#")) {                    
                    continue;                
                }                
                int resId;                
                if (attributeValue.startsWith("?")) {                    
                    // 以 ? 开头的属性值                    
                    int attrId = Integer.parseInt(attributeValue.substring(1));                    
                    resId = SkinThemeUtils.getResId(view.getContext(), new int[]{attrId})[0];                
                } else {                    
                    // 正常以 @ 开头的属性值                    
                    resId = Integer.parseInt(attributeValue.substring(1));                
                }                
                SkinNameValuePair skinPair = new SkinNameValuePair(attributeName, resId);                
                skinPairs.add(skinPair);            
            }        
        }        
        if (!skinPairs.isEmpty() || view instanceof SkinCustomViewSupport) {            
            SkinView skinView = new SkinView(view, skinPairs);            
            mSkinViews.add(skinView);        
        }    
    }    
    /**     
      * 对所有的 View 中的所有属性进行皮肤修改     
      */    
    void applySkin() {        
        for (SkinView mSkinView : mSkinViews) {            
            mSkinView.applySkin();        
        }    
    }
}

接下来就是要接管 View 的创建过程了;

接管 View 的创建流程

上一章讲解过,接管 View 的创建过程,只需要替换 Factory2 接口即可,这里我们也实现 Factory2 接口

public class SkinLayoutInflaterFactory2 implements LayoutInflater.Factory2, Observer {    
    private static final String[] mClassPrefixList = {            
        "android.widget.",            
        "android.webkit.",            
        "android.app.",            
        "android.view.",    
    };    
    // 记录对应 view 的构造函数    
    private static final Class<?>[] mConstructorSignature = new Class[] {            
        Context.class, AttributeSet.class    
    };    
    private static final HashMap<String, Constructor<? extends View>> sConstructorMap = new HashMap<>();    // 当选择新皮肤后需要替换 View 与之对应的属性    
    // 页面属性管理器    
    private final SkinAttributeAttrs mSkinAttribute;    
    // 用于获取窗口的状态框的信息    
    private final Activity mActivity;    
    SkinLayoutInflaterFactory2(Activity activity) {        
        this.mActivity = activity;        
        this.mSkinAttribute = new SkinAttributeAttrs();    
    }    
    @Override    
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {        
        // 换肤就是在需要的时候替换 View 的属性(src background 等)        
        // 所以这里创建 view 从而修改 View 属性        
        View view = createSystemView(name, context, attrs);        
        if (view == null) {            
            view = createView(name, context, attrs);        
        }        
        if (null != view) {            
            // 加载属性            
            mSkinAttribute.searchView(view, attrs);        
        }        
        return view;    
    }    
    @Override    
    public View onCreateView(String name, Context context, AttributeSet attrs) {        
        View view = createSystemView(name, context, attrs);        
        if (view == null) {            
            view = createView(name, context, attrs);        
        }        
        if (null != view) { 
            // 处理换肤的逻辑           
            mSkinAttribute.searchView(view, attrs);        
        }        
        return view;
    }    
    /**     
      * @param name View的类名     
      * @param context 上下文     
      * @param attrs 属性     
      * @return View对象     
      */    
    private View createSystemView(String name, Context context, AttributeSet attrs) {        
        // 如果包含 . 则不是 SDK 中的 View 可能是自定义 View 包括 support 库中的 View        
        if (-1 != name.indexOf('.')) {            
            return null;        
        }        
        for (String s : mClassPrefixList) {
            // 找到了对应的 View 之后返回            
            View view = createView(s + name, context, attrs);            
            if (view != null) {                
                return view;            
            }        
        }        
        return null;    
    }    
    /**     
      * 反射创建全类名View对象     
      *     
      * @param name 全类名View     
      * @param context 上下文     
      * @param attrs 属性     
      * @return View对象     
      */    
    private View createView(String name, Context context, AttributeSet attrs) {        
        Constructor<? extends View> constructor = findConstructor(context, name);        
        try {            
            return constructor.newInstance(context, attrs);        
        } catch (Exception e) {            
            e.printStackTrace();        
        }        
        return null;    
    }    
    /**     
      * 根据全类名找到其对应的构造方法对象     
      *     
      * @param context 上下文     
      * @param name 全类名View     
      * @return 构造方法对象     
      */    
    private Constructor<? extends View> findConstructor(Context context, String name) {        
        Constructor<? extends View> constructor = sConstructorMap.get(name);        
        if (constructor == null) {            
            try {                
                Class<? extends View> clazz = context.getClassLoader().loadClass(name).asSubclass(View.class);                
                constructor = clazz.getConstructor(mConstructorSignature);                
                constructor.setAccessible(true);                
                sConstructorMap.put(name, constructor);            
            } catch (Exception e) {                
                e.printStackTrace();            
            }        
        }        
        return constructor;    
    }    
    /**     
      *  Activity(Observable)发出通知,这里就会执行     
      *     
      *  执行换肤操作     
      * */    
    @Override    
    public void update(Observable o, Object arg) {        
        SkinThemeUtils.updateStatusBarColor(mActivity);        
        mSkinAttribute.applySkin();    
    }
}

接管了 View 的创建流程,这里大部分都是直接抄的源码的创建逻辑,在 View 创建的时候,就可以替换成插件包中皮肤;

接下来就需要监听 Activity 的创建和销毁,在创建之后的回调了重置 mFactorySet 这里 API 也提供了相关接口;

public class SkinActivityLifeCycle extends AbsActivityLifeCycle {    
    private final Observable mObservable;    
    private final ArrayMap<Activity, SkinLayoutInflaterFactory2> mLayoutInflaterFactory = new ArrayMap<>();    
    SkinActivityLifeCycle(Observable observable) {        
        this.mObservable = observable;    
    }    
    @Override    
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {        
        /*         
         *  onCreate方法执行之后 回调到这里,此时Activity的onCreate已经执行完毕,也就是setContentView方法已经执行结束,         
         *  mFactorySet已经被设置为true,所以要反射修改为false,否则会抛出 IllegalStateException("A factory has already been set on this LayoutInflater")         
         * */        
        /*         
         * 更新状态栏         
         * */        
        SkinThemeUtils.updateStatusBarColor(activity);        
        /*         
         * 更新布局视图         
         * */        
        LayoutInflater layoutInflater = activity.getLayoutInflater();        
        try {            
            // Android布局加载器 使用 mFactorySet 标记是否设置过 Factory            
            // 如设置过,再次设置会抛出异常            
            // 设置 mFactorySet 为 false            
            Field field = LayoutInflater.class.getDeclaredField("mFactorySet");            
            field.setAccessible(true);            
            field.setBoolean(layoutInflater, false);        
        } catch (Exception e) {            
            e.printStackTrace();        
        }        
        SkinLayoutInflaterFactory2 skinLayoutInflaterFactory = new SkinLayoutInflaterFactory2(activity);        
        LayoutInflaterCompat.setFactory2(layoutInflater, skinLayoutInflaterFactory);        
        mLayoutInflaterFactory.put(activity, skinLayoutInflaterFactory);        
        // 观察者模式       
        mObservable.addObserver(skinLayoutInflaterFactory);    
    }    
    @Override    
    public void onActivityDestroyed(Activity activity) {        
        SkinLayoutInflaterFactory2 skinLayoutInflaterFactory = mLayoutInflaterFactory.remove(activity);        
        SkinManager.getInstance().deleteObserver(skinLayoutInflaterFactory);    
    }
}

加载皮肤资源,初始化配置信息,

public class SkinManager extends Observable {    
    private Application mContext;
    private Resources skinResources;    
    private SkinManager() {}    
    /**     
      * 初始化必须放在 application 中     
      * */    
    public void init(Application application) {        
        mContext = application;        
        SkinSharedPreference.getInstance().init(application);        
        SkinResources.init(application);        
        // 注册 Activity 生命周期,并设置被观察者        
        SkinActivityLifeCycle skinApplicationActivityLifeCycle = new SkinActivityLifeCycle(this);        
        application.registerActivityLifecycleCallbacks(skinApplicationActivityLifeCycle);        
        // 加载上次使用保存的皮肤        
        loadSkin(SkinSharedPreference.getInstance().getSkin());    
    } 
    public Resources getSkinResources() {
        return skinResources;
    }   
    public static SkinManager getInstance() {        
        return Holder.instance;    
    }    
    private static final class Holder {        
        private static final SkinManager instance = new SkinManager();    
    }    
    /**     
      *  加载皮肤并应用     
      *  @param skin skin 皮肤路径     
      *     
      *  对外暴露这个方法,可以添加设置页面,增加换肤按钮,点击事件中 调用此方法,传递皮肤包路径     
      *     
      * */    
    public void loadSkin(String skin) {        
        if (TextUtils.isEmpty(skin)) {            
            // 还原默认皮肤            
            SkinSharedPreference.getInstance().reset();            
            SkinResources.getInstance().reset();        
        } else {            
            // 宿主 app 的 resources            
            Resources resources = mContext.getResources();            
            // 反射创建 AssetManager 与 Resources            
            try {                
                AssetManager assetManager = AssetManager.class.newInstance();                
                // 资源路径设置目录或者压缩包                
                // TODO 同一个 key 的颜色值 就会被替换掉,所以未打开的Activity页面,在初次打开的时候,使用的是接管 View 创建的过程,同时替换成新的皮肤资源                
                // TODO Resources进行getColor(R.color.xxx) 因为是同名的,这里已经替换成皮肤插件包中的Resources了,所以未打开过的Activity,执行onCreate方法中的setContentView                
                // TODO 中的inflate的时候 读取到的颜色值 就是插件包中的                
                Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);                
                addAssetPath.setAccessible(true);                
                addAssetPath.invoke(assetManager, skin);                
                // 根据当前的设备显示器信息与配置(横竖屏,语言等) 创建 Resources,                
                skinResources = new Resources(assetManager, resources.getDisplayMetrics(), resources.getConfiguration());                
                // 获取外部 apk(皮肤包) 包名                
                PackageManager mPm = mContext.getPackageManager();                
                PackageInfo info = mPm.getPackageArchiveInfo(skin, PackageManager.GET_ACTIVITIES);                
                if(info == null) {
                    return;
                }
                String packageName = info.packageName;                
                // 这里就会进行一个赋值,如果皮肤插件包有值,就会给SkinResources进行赋值                
                SkinResources.getInstance().applySkin(skinResources, packageName);                
                // 记录                
                SkinSharedPreference.getInstance().setSkin(skin);            
            } catch (Exception e) {                
                //            
            }        
        }        
        // 通知采集的 view 更新皮肤        
        // 被观察者改变,通知所有的观察者        
        setChanged();        
        notifyObservers(null);    
    }
}

Application 中初始化插件化换肤框架

public class MyApplication extends Application {    
    public static Application sApplication;    
    @Override    
    public void onCreate() {        
        super.onCreate();        
        sApplication = this;        
        SkinManager.getInstance().init(this);    
    }    
    public static Application getApplication() {        
        return sApplication;    
    }    
    @Override    
    public Resources getResources() {        
        Resources skinResources = SkinManager.getInstance().getSkinResources();        
        if (skinResources != null) {            
            if (skinResources.getConfiguration() != getSuperResources().getConfiguration()                     
                || skinResources.getDisplayMetrics() != getSuperResources().getDisplayMetrics()) {                
                skinResources.updateConfiguration(getSuperResources().getConfiguration(), 
                    getSuperResources().getDisplayMetrics());            
            }            
            return skinResources;        
        }        
        return getSuperResources();    
    }    
    @Override    
    public void onConfigurationChanged(@NonNull Configuration newConfig) {        
        super.onConfigurationChanged(newConfig);        
        // 适配 Android 12 屏幕切换 resources 不更新的问题        
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) {            
            getSuperResources().updateConfiguration(newConfig, Resources.getSystem().getDisplayMetrics());        
        }    
    }    
    public Resources getSuperResources() {        
        return super.getResources();    
    }
}

换肤按钮切换通知

// 这里也可以是网络下载的地址

SkinManager.getInstance().loadSkin("/data/data/com.example.llc/skin/skin-debug.apk");

最终效果

PS:TODO 版本适配,以及编译版本和目标版本

mFactorySet 做了版本限制,当 targetSdkVersion > 27 的时候会抛出异常,因为涉及到了插件化Hook,demo中做了 compileSdkVersion 和 targetSdkVersion 都是 27 的限制,主要是为了讲解 xml 解析和布局原理,后面我会补上适配的内容;

根据注释提示,可以参考 AsyncLayoutInflater 的实现自己的 LayoutInflater 接管 View 的创建;

简历润色

简历上可写:深度理解布局原理和XML解析,可手写插件化换肤核心实现;

下一章预告

嵌套滑动原理,手写淘宝首页二级联动

欢迎三连

来都来了,点个关注点个赞吧,你的支持是我最大的动力;

;