美文网首页程序员
Android 动态换肤原理与实现

Android 动态换肤原理与实现

作者: 昊空_6f4f | 来源:发表于2020-07-10 11:46 被阅读0次

    概述

    本文主要分享类似于酷狗音乐动态换肤效果的实现。

    动态换肤的思路:

    • 收集换肤控件以及对应的换肤属性
    • 加载插件皮肤包
    • 替换资源实现换肤效果
    • 制作插件皮肤包

    收集换肤控件以及对应的换肤属性

    换肤实际上进行资源替换,如替换字体、颜色、背景、图片等,对应控件属性有src、textColor、background、drawableLeft等。需要先收集页面控件是否包含换肤属性,那如何收集页面的控件呢?
    跟踪LayoutInflater中的createViewFromTag与tryCreateView方法:

    View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
                boolean ignoreThemeAttr) {
            ...
            try {
                View view = tryCreateView(parent, name, context, attrs);
    
                if (view == null) {
                    final Object lastContext = mConstructorArgs[0];
                    mConstructorArgs[0] = context;
                    try {
                        if (-1 == name.indexOf('.')) {
                            view = onCreateView(context, parent, name, attrs);
                        } else {
                            view = createView(context, name, null, attrs);
                        }
                    } finally {
                        mConstructorArgs[0] = lastContext;
                    }
                }
    
                return view;
            } catch (InflateException e) {
                    ...
            } catch (ClassNotFoundException e) {
                    ...
            } catch (Exception e) {
                    ...
            }
        }
    
    public final View tryCreateView(@Nullable View parent, @NonNull String name,
            @NonNull Context context,
            @NonNull AttributeSet attrs) {
            if (name.equals(TAG_1995)) {
                // Let's party like it's 1995!
                return new BlinkLayout(context, attrs);
            }
    
            View view;
            if (mFactory2 != null) {
                view = mFactory2.onCreateView(parent, name, context, attrs);
            } else if (mFactory != null) {
                view = mFactory.onCreateView(name, context, attrs);
            } else {
                view = null;
            }
    
            if (view == null && mPrivateFactory != null) {
                view = mPrivateFactory.onCreateView(parent, name, context, attrs);
            }
    
            return view;
        }
    

    通过源码可知创建控件会先调用Factory2的onCreateView方法,如果返回的View为空才会调用LayoutInflater中的onCreateView与createView,那我们自定一个Factory2就可以用于创建控件并判断是否包含换肤属性了。核心代码如下:

    public class SkinLayoutInflateFactory implements LayoutInflater.Factory2, Observer {
    
        static final String mPrefix[] = {
                "android.view.",
                "android.widget.",
                "android.webkit.",
                "android.app."
        };
    
        //xml中控件的初始化都是调用带Context和AttributeSet这个构造方法进行反射创建的
        static final Class<?>[] mConstructorSignature = new Class[]{
                Context.class, AttributeSet.class};
    
        //减少相同控件反射的次数
        private static final HashMap<String, Constructor<? extends View>> sConstructorMap =
                new HashMap<>();
        
        //记录每一个页面需要换肤的控件
        private SkinAttribute mSkinAttribute;
    
        /*
         * 关系:Activity对应一个LayoutInflate、
         *     LayoutInflate对一个SkinLayoutInflateFactory
         *     SkinLayoutInflateFactory对应一个SkinAttribute
         */
        private Activity mActivity;
    
        public SkinLayoutInflateFactory(Activity activity) {
            this.mActivity = activity;
            this.mSkinAttribute = new SkinAttribute();
        }
    
        @Nullable
        @Override
        public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
            View view;
            if (-1 == name.indexOf('.')) {//ImageView、TextView等
                view = createSdkView(context, name, attrs);
            } else {//自定义View、support、AndroidX、第三方控件等
                view = createView(context, name, attrs);
            }
    
            //关键代码:采集需要换肤的控件
            if (view != null) {
                mSkinAttribute.look(view, attrs);
            }
            return view;
        }
    
        //以下代码为控件初始化
        private View createSdkView(Context context, String name, AttributeSet attrs) {
            for (String prefix : mPrefix) {
                View view = createView(context, prefix + name, attrs);
                if (view != null) {
                    return view;
                }
            }
            return null;
        }
    
        private View createView(Context context, String name, AttributeSet attrs) {
            Constructor<? extends View> constructor = findConstructor(context, name);
            try {
                return constructor.newInstance(context, attrs);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    
        private Constructor<? extends View> findConstructor(Context context, String name) {
            Constructor<? extends View> constructor = sConstructorMap.get(name);
            if (constructor == null) {
                try {
                    Class<? extends View> clazz = Class.forName(name, false,
                            context.getClassLoader()).asSubclass(View.class);
                    constructor = clazz.getConstructor(mConstructorSignature);
                    constructor.setAccessible(true);
                    sConstructorMap.put(name, constructor);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            return constructor;
        }
    
        @Nullable
        @Override
        public View onCreateView(@NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
            return null;
        }
    
        @Override
        public void update(Observable o, Object arg) {
             //此处进行换肤
            mSkinAttribute.applySkin();
        }
    }
    

    SkinLayoutInflateFactory的主要工作是:

    • 创建xml中的控件
    • 收集需要换肤的控件

    创建控件主要是参考系统源码实现的,重点在于收集换肤控件,通过SkinAttribute记录每一个页面需要换肤的控件,核心代码如下:

    public class SkinAttribute {
    
        //需要换肤的属性集合,如背景、颜色、字体等
        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");
        }
    
        //记录每一个页面需要换肤的控件集合
        private List<SkinView> mSkinViewList = new ArrayList<>();
    
        //查找需要换肤的控件以及对应的换肤属性
        public void look(View view, AttributeSet attrs) {
            List<SkinPair> skinPairList = new ArrayList<>();
            for (int i = 0; i < attrs.getAttributeCount(); i++) {
                String attributeName = attrs.getAttributeName(i);
                if (mAttributes.contains(attributeName)) {
                    String attributeValue = attrs.getAttributeValue(i);
                    //如果是写死颜色,则不可换肤
                    if (attributeValue.startsWith("#")) {
                        continue;
                    }
                    int resId;
                    //判断是否使用系统资源
                    if (attributeValue.startsWith("?")) {// ? 系统资源
                        int attrId = Integer.parseInt(attributeValue.substring(1));
                        //获取获得Theme中属性中定义的资源id
                        resId = SkinThemeUtils.getThemeResId(view.getContext(), new int[]{attrId})[0];
                    } else {//@ 开发者自定义资源
                        resId = Integer.parseInt(attributeValue.substring(1));
                    }
    
                    SkinPair skinPair = new SkinPair(attributeName, resId);
                    skinPairList.add(skinPair);
                }
            }
            //如果skinPairList长度不为0,即有换肤属性,此时记录换肤控件
            if (!skinPairList.isEmpty() || view instanceof SkinViewSupport) {
                SkinView skinView = new SkinView(view, skinPairList);
                //如果已经加载过换肤了,此时需要主动调用一次换肤方法
                skinView.applySkin();
                mSkinViewList.add(skinView);
            }
        }
    
        //提供页面换肤功能
        public void applySkin() {
            for (SkinView skinView : mSkinViewList) {
                skinView.applySkin();
            }
        }
    
        //对应每一个换肤控件
        static class SkinView {
            View view;//换肤控件
            List<SkinPair> skinPairList;//换肤属性集合
    
            SkinView(View view, List<SkinPair> skinPairList) {
                this.view = view;
                this.skinPairList = skinPairList;
            }
    
            //关键方法:换肤方法(提供给Sdk自带控件)
            public void applySkin() {
                applySkinSupport();
                /*
                 * 关键思路:1.获取原始App中resId对应的类型、名称
                 *     2.根据类型、名称、插件皮肤包名获取插件皮肤包中对应的resId
                 *     3.获取插件插件皮肤包中resId对应的资源(如:颜色、背景、图片)再设置给原始App中的控件实现换肤功能
                 */
                for (SkinPair skinPair : skinPairList) {
                    Drawable left = null, top = null, right = null, bottom = null;
                    switch (skinPair.attributeName) {
                        //后续的换肤属性可在此处添加
                        case "background":
                            Object background = SkinResources.getInstance().getBackground(skinPair
                                    .resId);
                            //背景可能是 @color 也可能是 @drawable
                            if (background instanceof Integer) {
                                view.setBackgroundColor((int) 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;
                        default:
                            break;
                    }
                    if (null != left || null != right || null != top || null != bottom) {
                        ((TextView) view).setCompoundDrawablesWithIntrinsicBounds(left, top, right,
                                bottom);
                    }
                }
            }
    
            //提供给自定义控件进行换肤
            public void applySkinSupport() {
                if (view instanceof SkinViewSupport) {
                    ((SkinViewSupport) view).applySkin();
                }
            }
        }
    
        //对应每一个换肤属性
        static class SkinPair {
            //换肤属性
            String attributeName;
            //资源Id
            int resId;
    
            SkinPair(String attributeName, int resId) {
                this.attributeName = attributeName;
                this.resId = resId;
            }
        }
    }
    

    这里要注意如果是自定义View需要实现SkinViewSupport接口,自己实现换肤功能,代码如下:

    public interface SkinViewSupport {
        void applySkin();
    }
    
    /**
     * 注意:如果自定义View需要自己实现换肤,先通过属性获取ResourceId,再通过代码方式实现换肤
     */
    public class MyTabLayout extends TabLayout implements SkinViewSupport {
    
        int mTabIndicatorColorResId;
    
        public MyTabLayout(@NonNull Context context) {
            this(context, null);
        }
    
        public MyTabLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public MyTabLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabLayout);
            mTabIndicatorColorResId = a.getResourceId(R.styleable.TabLayout_tabIndicatorColor, 0);
            a.recycle();
        }
    
        @Override
        public void applySkin() {
            if (mTabIndicatorColorResId != 0) {
                int tabIndicatorColor = SkinResources.getInstance().getColor(mTabIndicatorColorResId);
                setSelectedTabIndicatorColor(tabIndicatorColor);
            }
        }
    }
    
    
    

    由源码可知SkinLayoutInflateFactory必须在setContentView之前设置才能生效,这里有两种实现方式:

    • 封装BaseActivity中,但侵入性比较强
    • 在ActivityLifecycleCallbacks的onActivityCreated方法中添加,AOP思想(推荐)

    核心代码如下:

    public class ApplicationActivityLifecycle implements Application.ActivityLifecycleCallbacks {
    
        private Observable mObservable;
        private ArrayMap<Activity, SkinLayoutInflateFactory> mSkinLayoutInflateFactory = new ArrayMap<>();
    
        public ApplicationActivityLifecycle(Observable observable) {
            this.mObservable = observable;
        }
    
        @Override
        public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
    
            //Activity -->LayoutInflate -->SkinLayoutInflateFactory
            //为每一个Activity对应的LayoutInflate添加SkinLayoutInflateFactory
    
            LayoutInflater layoutInflater = activity.getLayoutInflater();
    
            try {
                //注意:LayoutInflate的setFactory2方法中将mFactorySet设置成true了,第二次调用会报错,所以此处使用反射手动修改成false
                Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
                field.setAccessible(true);
                field.setBoolean(layoutInflater, false);
            } catch (Exception e) {
                e.printStackTrace();
            }
    
            SkinLayoutInflateFactory factory = new SkinLayoutInflateFactory(activity);
            LayoutInflaterCompat.setFactory2(layoutInflater, factory);
    
            //添加换肤观察者
            mObservable.addObserver(factory);
            mSkinLayoutInflateFactory.put(activity, factory);
        }
        
        @Override
        public void onActivityDestroyed(@NonNull Activity activity) {
            SkinLayoutInflateFactory factory = mSkinLayoutInflateFactory.get(activity);
            mObservable.deleteObserver(factory);
        }
    }
    

    加载插件皮肤包

    通过创建AssetManager加载插件皮肤包,核心代码如下:

         AssetManager assetManager = AssetManager.class.newInstance();
         Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
         addAssetPath.invoke(assetManager,skinPath);
    

    替换资源实现换肤效果

    替换资源的流程:通过原始App的resId获取对应的名称、类型,再根据名称、类型、插件包名去皮肤包中查找出对应的resId,获取插件resId对应的资源再设置给原始App的控件,从而实现换肤。

    资源替换工具类:

    public class SkinResources {
    
        //插件App包名
        private String mSkinPgk;
    
        //是否使用默认皮肤包
        private boolean mDefaultSkin = true;
    
        //原始App的资源
        private Resources mAppResources;
        //插件App的资源
        private Resources mSkinResources;
    
        private SkinResources(Context context) {
            mAppResources = context.getResources();
        }
    
        private volatile static SkinResources instance;
    
        public static void init(Context context) {
            if (instance == null) {
                synchronized (SkinResources.class) {
                    if (instance == null) {
                        instance = new SkinResources(context);
                    }
                }
            }
        }
    
        public static SkinResources getInstance() {
            return instance;
        }
    
        //设置皮肤包资源
        public void applySkin(Resources skinResources, String skinPgk) {
            mSkinResources = skinResources;
            mSkinPgk = skinPgk;
            mDefaultSkin = skinResources == null || TextUtils.isEmpty(skinPgk);
        }
    
        //恢复默认皮肤包
        public void reset() {
            mSkinResources = null;
            mDefaultSkin = true;
            mSkinPgk = "";
        }
    
        /**
         * 1.通过原始app中的resId(R.color.XX)获取到自己的名字和类型
         * 2.根据名字和类型获取皮肤包中的resId
         */
        public int getIdentifier(int resId) {
            if (mDefaultSkin) return resId;
            String name = mAppResources.getResourceEntryName(resId);
            String type = mAppResources.getResourceTypeName(resId);
            return mSkinResources.getIdentifier(name, type, mSkinPgk);
        }
    
        public int getColor(int resId) {
            if (mDefaultSkin) return mAppResources.getColor(resId);
    
            int skinId = getIdentifier(resId);
            if (skinId == 0) return mAppResources.getColor(resId);
    
            return mSkinResources.getColor(skinId);
        }
    
        public ColorStateList getColorStateList(int resId) {
            if (mDefaultSkin) return mAppResources.getColorStateList(resId);
    
            int skinId = getIdentifier(resId);
            if (skinId == 0) return mAppResources.getColorStateList(resId);
            return mSkinResources.getColorStateList(skinId);
        }
    
        public Drawable getDrawable(int resId) {
            if (mDefaultSkin) return mAppResources.getDrawable(resId);
    
            //通过 app的resource 获取id 对应的 资源名 与 资源类型
            //找到 皮肤包 匹配 的 资源名资源类型 的 皮肤包的 资源 ID
            int skinId = getIdentifier(resId);
            if (skinId == 0) return mAppResources.getDrawable(resId);
    
            return mSkinResources.getDrawable(skinId);
        }
    
        /**
         * 背景可能是Color 也可能是drawable
         */
        public Object getBackground(int resId) {
            String resourceTypeName = mAppResources.getResourceTypeName(resId);
            if ("color".equals(resourceTypeName)) {
                return getColor(resId);
            } else {
                return getDrawable(resId);
            }
        }
    }
    

    换肤管理类,负责App换肤功能:

    public class SkinManager extends Observable {
    
        private Application mContext;
    
        private volatile static SkinManager instance;
    
        public static void init(Application application) {
            if (instance == null) {
                synchronized (SkinManager.class) {
                    if (instance == null) {
                        instance = new SkinManager(application);
                    }
                }
            }
        }
    
        private SkinManager(Application application) {
            mContext = application;
            application.registerActivityLifecycleCallbacks(new ApplicationActivityLifecycle(this));
            SkinResources.init(application);
            SkinPreference.init(application);
            //加载上次使用保存的皮肤
            loadSkin(SkinPreference.getInstance().getSkin());
        }
    
        public static SkinManager getInstance() {
            return instance;
        }
    
        //加载换肤插件
        public void loadSkin(String skinPath) {
            if (TextUtils.isEmpty(skinPath)) {
                SkinPreference.getInstance().reset();
                SkinResources.getInstance().reset();
            } else {
                try {
                    Resources appResources = mContext.getResources();
    
                    //创建AssetManager对象用于加载换肤插件
                    AssetManager assetManager = AssetManager.class.newInstance();
                    Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
                    addAssetPath.invoke(assetManager,skinPath);
    
                    //创建Resources用于加载换肤插件的资源
                    Resources skinResources = new Resources(assetManager, appResources.getDisplayMetrics(), appResources.getConfiguration());
    
                    //根据皮肤插件路径获取加载换肤插件的包名
                    PackageManager packageManager = mContext.getPackageManager();
                    PackageInfo packageArchiveInfo = packageManager.getPackageArchiveInfo(skinPath, PackageManager.GET_ACTIVITIES);
                    String packageName = packageArchiveInfo.packageName;
    
                    //设置皮肤
                    SkinResources.getInstance().applySkin(skinResources, packageName);
    
                    //记录当前皮肤
                    SkinPreference.getInstance().setSkin(skinPath);
    
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
    
            /**
             * 关键要点:
             *      上面设置完皮肤后,要通知页面进行换肤,此处采用观察者模式进行通知,通知的对象为SkinLayoutInflateFactory,
             *      SkinLayoutInflateFactor在调用SkinAttribute的applySkin方法进行换肤
             */
            setChanged();
            notifyObservers();
        }
    }
    

    这里采用了观察者模式通知多页面换肤,SkinManager对应Observable,SkinLayoutInflateFactory对应Observer,当SkinManager调用loadSkin进行换肤后,会通知SkinLayoutInflateFactory回调update方法,而SkinLayoutInflateFactory包含了SkinAttribute,在update方法中调用SkinAttribute的applySkin方法便可以通知到页面控件进行资源替换,从而实现换肤效果。

    制作插件皮肤包

    皮肤包只需要包含资源文件并且资源的名称要与原始App保持一致,制作完成后上传到服务的,客户端按需下载皮肤包,进行加载以及换肤操作

    完整代码实现

    百度链接
    密码:wmay

    相关文章

      网友评论

        本文标题:Android 动态换肤原理与实现

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