Android换肤的实现以及相应的源码分析

不处理关于AMS相关的信息,那么在这后面的流程:
ActivityThread-》performLaunchActivity()方法-》方法内部构建了Context上下文,并且初始化了一个window对象,这个window对象在activity.attach之前只是一个Activity的window对象,并没有任何的值,只有在activity.attach之后这里会构建称PhoneWindow对象,并且这里初始化了UIThread.

布局创建的流程:
PhoneWindow-》DecorView-》RootView-》LayoutInflater来进行布局的加载
,这是整个布局创建的过程
资源加载的流程:
App打包的apk中有一个resources.arsc的二进制文件,这个文件里存储了所有资源相关的ID,名称以及路径,所以如果要进行换肤实际就是将这个查找的路径替换为插件包里的路径,皮肤插件包其实也是一个apk,不过只包含资源而已,为了避免被处理,一般会修改后缀名称,例如.skin,但是这个不影响资源的加载

在Android中资源类是Resource类,它的内部持有一个ResourcesImpl对象,而ResourcesImpl里内部持有一个AssetManager,这个是资源的实际管理类,虽然随着版本的提升AssetManager通过ApkAssets来进行资源的设置了,但是它依然有addAssetPath方法,所以我们可以通过构建一个AssetManager来实现一个Resources,这个Resources就是插件资源管理对象,其中资源插件包的id必须是与宿主的id一致

   /**
     * 1.通过原始app中的resId(R.color.XX)获取到自己的 名字
     * 2.根据名字和类型获取皮肤包中的ID
     */
    public int getIdentifier(int resId) {
        if (isDefaultSkin) {
            return resId;
        }
        //在主app中通过资源id获取name和类型,然后通过这个类型获取在资源包里的resid
        String entryName = mAppResources.getResourceEntryName(resId);
        String entryTypeName = mAppResources.getResourceTypeName(resId);
        int skinId = mSkinResources.getIdentifier(entryName, entryTypeName, mSkinPackageName);
        return skinId;
    }

基于上面id获取方法实现的资源获取

/**
     * 通过R.color.xx 来获取到皮肤包中的color
     * 获取color
     * @param resId
     * @return
     */
    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(skinId);
    }

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

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

    public Object getBackground(int resId) {
        String resourceTypeName = mAppResources.getResourceTypeName(resId);
        if ("color".equals(resourceTypeName)) {
            return getColor(resId);
        } else {
            return getDrawable(resId);
        }
    }

现在资源替换有了,那么如何进行替换?
这里要先了解View的构建过程,也就是Android是如何把xml的文件解析成View展示到界面的,针对setContentView(R.layout.XXX)进行分析,可以看出最总就是在Activity创建出的RootView进行View解析插入,而且通过源码的分析可以看

 public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {

因为Android的view是树形结构的,所以一个view必然只能有一个直接父布局,在view的布局测量过程中是受这个父布局约束的。

    /**
     * Creates a view from a tag name using the supplied attribute set.
     * <p>
     * <strong>Note:</strong> Default visibility so the BridgeInflater can
     * override it.
     *
     * @param parent the parent view, used to inflate layout params
     * @param name the name of the XML tag used to define the view
     * @param context the inflation context for the view, typically the
     *                {@code parent} or base layout inflater context
     * @param attrs the attribute set for the XML tag used to define the view
     * @param ignoreThemeAttr {@code true} to ignore the {@code android:theme}
     *                        attribute (if set) for the view being inflated,
     *                        {@code false} otherwise
     */
    View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
        if (name.equals("view")) {
            name = attrs.getAttributeValue(null, "class");
        }

        // Apply a theme wrapper, if allowed and one is specified.
        if (!ignoreThemeAttr) {
            final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
            final int themeResId = ta.getResourceId(0, 0);
            if (themeResId != 0) {
                context = new ContextThemeWrapper(context, themeResId);
            }
            ta.recycle();
        }

        if (name.equals(TAG_1995)) {
            // Let's party like it's 1995!
            return new BlinkLayout(context, attrs);
        }

        try {
            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);
            }

            if (view == null) {
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = context;
                try {
                    if (-1 == name.indexOf('.')) {
                        view = onCreateView(parent, name, attrs);
                    } else {
                        view = createView(name, null, attrs);
                    }
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
            }

            return view;
        } catch (InflateException e) {
            throw e;

        } catch (ClassNotFoundException e) {
            final InflateException ie = new InflateException(attrs.getPositionDescription()
                    + ": Error inflating class " + name, e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;

        } catch (Exception e) {
            final InflateException ie = new InflateException(attrs.getPositionDescription()
                    + ": Error inflating class " + name, e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;
        }
    }

这是LayoutInflater中的view创建过程,其中上面会有mFactory2,mFactory,mPrivateFactory,三个提供的拦截器,如果这三个中有一个实现的了View的创建过程,那么就直接返回对应的View了,这就给到了我们去获取和修改View的可能性,而view的创建方法就是通过解析到的view节点名称然后去通过类名反射创建(两个参数)

所以换肤这块有两种思路,一种构建新的LayoutInflater来接管系统的创建,二是设置mFactory2来进行拦截创建。

所以现在通过继承mFactory2来实现:

public class SkinLayoutInflaterFactory 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>> mConstructorMap =
            new HashMap<String, Constructor<? extends View>>();

    // 当选择新皮肤后需要替换View与之对应的属性
    // 页面属性管理器
    private SkinAttribute skinAttribute;
    // 用于获取窗口的状态框的信息
    private Activity activity;

    public SkinLayoutInflaterFactory(Activity activity) {
        this.activity = activity;
        skinAttribute = new SkinAttribute();
    }

    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        //换肤就是在需要时候替换 View的属性(src、background等)
        //所以这里创建 View,从而修改View属性
        View view = createSDKView(name, context, attrs);
        if (null == view) {
            view = createView(name, context, attrs);
        }
        //这就是我们加入的逻辑
        if (null != view) {
            //加载属性
            skinAttribute.look(view, attrs);
        }
        return view;
    }

    private View createSDKView(String name, Context context, AttributeSet
            attrs) {
        //如果包含 . 则不是SDK中的view 可能是自定义view包括support库中的View
        if (-1 != name.indexOf('.')) {
            return null;
        }
        //不包含就要在解析的 节点 name前,拼上: android.widget. 等尝试去反射
        for (int i = 0; i < mClassPrefixList.length; i++) {
            View view = createView(mClassPrefixList[i] + name, context, attrs);
            if(view!=null){
                return view;
            }
        }
        return null;
    }

    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) {
        }
        return null;
    }

    private Constructor<? extends View> findConstructor(Context context, String name) {
        Constructor<? extends View> constructor = mConstructorMap.get(name);
        if (constructor == null) {
            try {
                Class<? extends View> clazz = context.getClassLoader().loadClass
                        (name).asSubclass(View.class);
                constructor = clazz.getConstructor(mConstructorSignature);
                mConstructorMap.put(name, constructor);
            } catch (Exception e) {
            }
        }
        return constructor;
    }




    @Override
    public View onCreateView(String name, Context context, AttributeSet attrs) {
        return null;
    }

    //如果有人发送通知,这里就会执行
    @Override
    public void update(Observable o, Object arg) {
        SkinThemeUtils.updateStatusBarColor(activity);
        skinAttribute.applySkin();
    }
}

属性的收集,因为我们能够替换的只有资源,例如文字的颜色,背景,图片的src,背景等等。

package com.tojoy.skin.lib;

import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.core.view.ViewCompat;


import com.tojoy.skin.lib.utils.SkinResources;
import com.tojoy.skin.lib.utils.SkinThemeUtils;

import java.util.ArrayList;
import java.util.List;

/**
 * <pre>
 *    author : heyueyang
 *    time   : 2021/10/28
 *    desc   :  所有需要换肤的view的所对应的属性
 *    version: 1.0
 */
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");
    }

    //记录换肤需要操作的View与属性信息
    private List<SkinView> mSkinViews = new ArrayList<>();

    /**
     * 记录一个view上需要换肤的属性
     * @param view
     * @param attrs
     */
    public void look(View view, AttributeSet attrs) {
        List<SkinPair> mSkinPairs = new ArrayList<>();
        for (int i = 0; i < attrs.getAttributeCount(); i++) {
            String attrName = attrs.getAttributeName(i);
            if (mAttributes.contains(attrName)) {
                String attrValue = attrs.getAttributeValue(i);
                /**
                 * # 写死的
                 * ? 系统的 例如?actionBarHeight
                 * @ 自定义的,正常使用的
                 */
                if (attrValue.startsWith("#")) {
                    continue;
                }
                int resId;
                if (attrValue.startsWith("?")) {
                    int attrId = Integer.parseInt(attrValue.substring(1));
                    resId = SkinThemeUtils.getResId(view.getContext(), new int[]{attrId})[0];
                } else {
                    resId = Integer.parseInt(attrValue.substring(1));
                }
                SkinPair skinPair = new SkinPair(attrName, resId);
                mSkinPairs.add(skinPair);
            }
        }
        if (!mSkinPairs.isEmpty() || view instanceof SkinViewSupport) {
            SkinView skinView = new SkinView(view, mSkinPairs);
            skinView.applySkin();
            mSkinViews.add(skinView);
        }
    }

    /*
   对所有的view中的所有的属性进行皮肤修改
 */
    public void applySkin() {
        for (SkinView mSkinView : mSkinViews) {
            mSkinView.applySkin();
        }
    }


    static class SkinView {
        //替换的view
        View mView;
        //替换的属性
        List<SkinPair> mPairs;

        public SkinView(View view, List<SkinPair> pairs) {
            mView = view;
            mPairs = pairs;
        }

        /**
         * 进行相应的属性替换
         */
        public void applySkin() {
            applySkinSupport();
            for (SkinPair pair : mPairs) {
                Drawable left = null, top = null, right = null, bottom = null;
                switch (pair.attributeName) {
                    case "background":
                        Object background = SkinResources.getInstance().getBackground(pair.resId);
                        if (background instanceof Integer) {
                            mView.setBackgroundColor((int) background);
                        } else {
                            ViewCompat.setBackground(mView, (Drawable) background);
                        }
                        break;
                    case "src":
                        background = SkinResources.getInstance().getBackground(pair.resId);
                        if (background instanceof Integer) {
                            ((ImageView) mView).setImageDrawable(new ColorDrawable((Integer) background));
                        } else {
                            ((ImageView) mView).setImageDrawable((Drawable) background);
                        }
                        break;
                    case "textColor":
                        ((TextView) mView).setTextColor(SkinResources.getInstance().getColorStateList(pair.resId));
                        break;
                    case "drawableLeft":
                        left = SkinResources.getInstance().getDrawable(pair.resId);
                        break;
                    case "drawableTop":
                        top = SkinResources.getInstance().getDrawable(pair.resId);
                        break;
                    case "drawableRight":
                        right = SkinResources.getInstance().getDrawable(pair.resId);
                        break;
                    case "drawableBottom":
                        bottom = SkinResources.getInstance().getDrawable(pair.resId);
                        break;
                    default:
                        break;
                }
                if (left != null || top != null || right != null || bottom != null) {
                    ((TextView) mView).setCompoundDrawablesRelativeWithIntrinsicBounds(left, top,
                            right, bottom);
                }
            }
        }

        private void applySkinSupport() {
            if (mView instanceof SkinViewSupport) {
                ((SkinViewSupport) mView).applySkin();
            }
        }

    }


    static class SkinPair {
        //属性名
        String attributeName;
        //属性id
        int resId;

        public SkinPair(String attributeName, int resId) {
            this.attributeName = attributeName;
            this.resId = resId;
        }
    }
}

所以整个需要替换属性的View的结构,替换的view和需要替换的属性,而属性又分为,属性名和id。

下个问题Factory设置的实际,必须在创建View之前,如果是只有就收集不到相关的信息

package com.tojoy.skin.lib;

import android.app.Activity;
import android.app.Application;
import android.os.Bundle;
import android.util.ArrayMap;
import android.view.LayoutInflater;


import com.tojoy.skin.lib.utils.SkinThemeUtils;

import java.lang.reflect.Field;
import java.util.Observable;

/**
 * <pre>
 *    author : heyueyang
 *    time   : 2021/11/01
 *    desc   : 在这个进行activity中Layoutinflater的fatory2的设置替换,并且设置Observer的绑定设置
 *    version: 1.0
 */
public class ApplicationActivityLifecycle implements Application.ActivityLifecycleCallbacks {

    private Observable mObservable;
    private ArrayMap<Activity, SkinLayoutInflaterFactory> mMap = new ArrayMap<>();

    public ApplicationActivityLifecycle(Observable observable) {
        mObservable = observable;
    }

    @Override
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
        SkinThemeUtils.updateStatusBarColor(activity);
        LayoutInflater layoutInflater = activity.getLayoutInflater();

        try {
            Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
            field.setAccessible(true);
            field.setBoolean(layoutInflater, false);
        } catch (Exception e) {
            e.printStackTrace();
        }
        SkinLayoutInflaterFactory skinLayoutInflaterFactory =
                new SkinLayoutInflaterFactory(activity);
        layoutInflater.setFactory2(skinLayoutInflaterFactory);
        mMap.put(activity, skinLayoutInflaterFactory);
        mObservable.addObserver(skinLayoutInflaterFactory);
    }

    @Override
    public void onActivityStarted(Activity activity) {

    }

    @Override
    public void onActivityResumed(Activity activity) {

    }

    @Override
    public void onActivityPaused(Activity activity) {

    }

    @Override
    public void onActivityStopped(Activity activity) {

    }

    @Override
    public void onActivitySaveInstanceState(Activity activity, Bundle outState) {

    }

    @Override
    public void onActivityDestroyed(Activity activity) {
        SkinLayoutInflaterFactory skinLayoutInflaterFactory = mMap.remove(activity);
        SkinManager.getInstance().deleteObserver(skinLayoutInflaterFactory);
    }
}

工厂和Activity进行绑定并通过Observable来进行通知触发换肤改变。


package com.tojoy.skin.lib;

import android.app.Application;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.text.TextUtils;

import com.tojoy.skin.lib.utils.SkinResources;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Observable;

/**
 * <pre>
 *    author : heyueyang
 *    time   : 2021/10/29
 *    desc   :
 *    version: 1.0
 */
public class SkinManager extends Observable {

    private volatile static SkinManager instance;

    private Application mContext;

    private ApplicationActivityLifecycle mLifecycle;

    public static void init(Application application) {
        if (instance == null) {
            synchronized (SkinManager.class) {
                if (instance == null) {
                    instance = new SkinManager(application);
                }
            }
        }
    }

    private SkinManager(Application context) {
        mContext = context;
        SkinPreference.init(context);
        SkinResources.init(context);
        mLifecycle = new ApplicationActivityLifecycle(this);
        context.registerActivityLifecycleCallbacks(mLifecycle);
        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 skinResource = new Resources(assetManager,
                        appResources.getDisplayMetrics(), appResources.getConfiguration());

                PackageManager manager = mContext.getPackageManager();
                PackageInfo skinInfo = manager.getPackageArchiveInfo(skinPath,
                        PackageManager.GET_ACTIVITIES);
                String skinPackageName = skinInfo.packageName;
                SkinResources.getInstance().applySkin(skinResource, skinPackageName);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
        setChanged();
        notifyObservers(null);
    }
}

Assets的路径文件不能直接访问,所以需要先将文件拷贝到目录下再进行访问。


一个单独要注意的问题,因为LayoutInflater的限定是只能有一个Factory,所以这里一个背景库给到的解决方案是获取已经设置过的Factory来进行view的创建
GitHub - JavaNoober/BackgroundLibrary: A framework for directly generating shape through Tags, no need to write shape.xml again(通过标签直接生成shape,无需再写shape.xml)