Android 动态换肤原理及实现

Android 内置的换肤

Android 中可以动态切换主题,但是只能切换主题的颜色,并且主题的切换会对view进行重新加载并不连贯.
Android 中内置的换肤需要在apk中存在需要换肤的图片等资源,导致apk包的增大,并且不能进行更多皮肤的扩展,需要重新打包上线.

如何实现动态换肤

类似网易云音乐的动态换肤,是通过下载皮肤包,加载皮肤包来实现动态换肤的功能,那么皮肤包是啥呢? 其实就是apk包,读取apk中的资源文件进行替换就可以实现动态的换肤功能.

实现代码地址:https://github.com/JakePrim/PrimSkinCore

那么动态换肤的实现原理是什么? 我们知道在Activity和Fragment通过 setContentView 来加载布局文件,setContentView
中如何加载布局呢? 内部通过 LayoutInflater 布局加载器来加载布局. LayoutInflater
如何加载布局呢? 我们看如下系统的源代码:

LayoutInflater.from(mContext).inflate(resId, contentParent);

//inflate 做了什么呢? 点击看下inflate 方法做了什么

 public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
     .....
     //这里是获取返回的view    
     final View temp = createViewFromTag(root, name, inflaterContext, attrs);
     ........
 }
//在createViewFromTag返回了一个view
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {    
    .....
     View view;
            if (mFactory2 != null) {//如果mFactory2 != null 则通过mFactory2加载
                view = mFactory2.onCreateView(parent, name, context, attrs);
            } else if (mFactory != null) {//如果mFactory != null 则通过mFactory加载
                view = mFactory.onCreateView(name, context, attrs);
            } else {
                view = null;
            }

            if (view == null && mPrivateFactory != null) {
                view = mPrivateFactory.onCreateView(parent, name, context, attrs);
            }
            //默认通过一下方式返回view          
            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;
    ......
}

上述代码中首先判断mFactory2是否为空,如果不为空就交给了mFactory2去处理返回view,很明显google给了我们支持自定义的方法.setFactory2方法还是public的公有的方法,我们只需要实现Factory2,对view进行响应的处理然后返回就可以实现动态换肤. 在上述代码中还有一个 mFactory 其实是和mFactory2 一样的道理,实现mFactory也是一样的只不过 mFactory2处于第一个,防止有其他的地方实现了mFactory2,所以我们就需要去实现mFactory2,确保mFactory2被正确运行.

   /**
     * Like {@link #setFactory}, but allows you to set a {@link Factory2}
     * interface.
     */
    public void setFactory2(Factory2 factory) {
        if (mFactorySet) {
            throw new IllegalStateException("A factory has already been set on this LayoutInflater");
        }
        if (factory == null) {
            throw new NullPointerException("Given factory can not be null");
        }
        mFactorySet = true;
        if (mFactory == null) {
            mFactory = mFactory2 = factory;
        } else {
            mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
        }
    }

我们看了上述的系统源代码,知道了系统是支持自定义mFactory2,来自己实现对view对加载,返回view.加载view只需要参考系统的代码即可,核心的代码就是筛选属性,然后修改属性的值,来实现颜色和图片的改变.

那么系统是如何加载view的呢? 我们从系统的源代码中来查找,在 createViewFromTag 方法中name.indexOf(‘.’)如果布局中的view带. 这说明是自定义view,否则是基本的view,我们先来看一下基本的view,来看一下 onCreateView 方法

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

onCreateView其实是有AsyncLayoutInlater调用的onCreateView

private static class BasicInflater extends LayoutInflater {
        private static final String[] sClassPrefixList = {
            "android.widget.",
            "android.webkit.",
            "android.app."
        };

        BasicInflater(Context context) {
            super(context);
        }

        @Override
        public LayoutInflater cloneInContext(Context newContext) {
            return new BasicInflater(newContext);
        }

        @Override
        protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
            for (String prefix : sClassPrefixList) {
                try {
                    View view = createView(name, prefix, attrs);
                    if (view != null) {
                        return view;
                    }
                } catch (ClassNotFoundException e) {
                    // In this case we want to let the base class take a crack
                    // at it.
                }
            }

            return super.onCreateView(name, attrs);
        }
    }

最核心的代码如下: 本质上是通过反射来实现返回View对象即可.是不是很简单,系统的源码就是这样加载布局的.

if (constructor == null) {
                // Class not found in the cache, see if it's real, and try to add it
                clazz = mContext.getClassLoader().loadClass(
                        prefix != null ? (prefix + name) : name).asSubclass(View.class);

                if (mFilter != null && clazz != null) {
                    boolean allowed = mFilter.onLoadClass(clazz);
                    if (!allowed) {
                        failNotAllowed(name, prefix, attrs);
                    }
                }
                constructor = clazz.getConstructor(mConstructorSignature);
                constructor.setAccessible(true);
                sConstructorMap.put(name, constructor);
            }

我们在得到view对象之后,获取view的属性将view的属性资源修改为皮肤包的资源.需要采集需要换肤的需要替换的属性.
采集如下基本属性,替换为皮肤包中的资源ID

static {
        mAttributes.add("background");
        mAttributes.add("src");

        mAttributes.add("textColor");
        mAttributes.add("drawableLeft");
        mAttributes.add("drawableTop");
        mAttributes.add("drawableRight");
        mAttributes.add("drawableBottom");
    }

动态换肤的实现

在上述中我们知道通过布局加载器LayoutInflater来加载布局,所以我们需要每进入一个Activity拿到 LayoutInflater 设置自定义布局加载工厂,如果建立一个BaseActivity对代码对入侵性很强,Application中提供监听App的所有Activity的生命周期回调.

代码如下

public class SkinActivityLifecycle implements Application.ActivityLifecycleCallbacks {

    private HashMap<Activity, SkinLayoutFactory> mLayoutFactoryMap = new HashMap<>();

    @Override
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
        //拿到每个activity的布局加载器
        LayoutInflater layoutInflater = LayoutInflater.from(activity);
        //mFactorySet 如果为true 抛出异常,有可能会出现这种情况,通过反射将mFactorySet设置为false
        try {
            Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
            field.setAccessible(true);
            field.setBoolean(layoutInflater, false);
        } catch (Exception e) {
            e.printStackTrace();
        }
        //自定义布局处理工厂
        SkinLayoutFactory factory = new SkinLayoutFactory();
        //设置工厂
        LayoutInflaterCompat.setFactory2(layoutInflater, factory);
    }

    @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) {

    }
}

接下来就是自定义 LayoutInflater.Factory2 ,Factory2的实现其实就是参考系统的实现,代码原理也非常简单,代码如下

public class SkinLayoutFactory implements LayoutInflater.Factory2, Observer {

    private static final String[] sClassPrefixList = {
            "android.widget.",
            "android.webkit.",
            "android.app."
    };

    private static final HashMap<String, Constructor<? extends View>> sConstructorMap =
            new HashMap<String, Constructor<? extends View>>();

    private static final Class<?>[] mConstructorSignature = new Class[]{
            Context.class, AttributeSet.class};

    private SkinAttribute skinAttribute;

    public SkinLayoutFactory() {
        skinAttribute = new SkinAttribute();
    }

    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        //参考系统的实现
        View view = createViewFromTag(context, name, attrs);
        if (view == null) {
            view = createView(name, context, attrs);
        }
        //采集view的属性
        skinAttribute.load(view,attrs);
        return view;
    }

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

    private View createViewFromTag(Context context, String name, AttributeSet attrs) {
        if (name.contains(".")) {//如果布局xml中的view 名包含.表示为自定义view 此处先不做处理
            return null;
        }
        View view = null;
        for (String prefix : sClassPrefixList) {//遍历view属于哪个包中 然后反射创建view对象
            try {
                view = createView(prefix + name, context, attrs);
                if (view != null) {
                    break;
                }
            } catch (Exception e) {
                // In this case we want to let the base class take a crack
                // at it.
            }
        }
        return view;
    }

    private View createView(String name, Context context, AttributeSet attrs) {
        Constructor<? extends View> constructor = sConstructorMap.get(name);
        try {
            if (constructor == null) {
                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) {

        }

        if (null != constructor) {
            try {
                return constructor.newInstance(context, attrs);
            } catch (Exception e) {
            }
        }

        return null;
    }

    @Override
    public void update(Observable o, Object arg) {
        skinAttribute.applySkin();
    }
}

真正实现换肤的就是,对Attribute的操作, 接下来看一下SkinAttribute的实现很非常简单,其实就是遍历所有view的属性拿到自己需要的属性,然后存储在集合中如下:

//采集的属性集合
static {
        mAttributes.add("background");
        mAttributes.add("src");

        mAttributes.add("textColor");
        mAttributes.add("drawableLeft");
        mAttributes.add("drawableTop");
        mAttributes.add("drawableRight");
        mAttributes.add("drawableBottom");
    }

public void load(View view, AttributeSet attrs) {
        List<SkinPair> 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);
                if (attributeValue.startsWith("#")) {//写死的先不去管 #000000
                    continue;
                }
                int resId;
                if (attributeValue.startsWith("?")) {
                    //attrId
                    int attrId = Integer.parseInt(attributeValue.substring(1));
                    resId = utils.getResId(view.getContext(), new int[]{attrId})[0];
                } else {
                    //@1232311
                    resId = Integer.parseInt(attributeValue.substring(1));
                }
                if (resId != 0) {
                    //将匹配的都存储下来
                    SkinPair skinPair = new SkinPair(attributeName, resId);
                    skinPairs.add(skinPair);
                }
            }
        }
        //将布局中每个view与之对应的属性集合放入到对应的view集合中
        if (!skinPairs.isEmpty()) {
            SkinView skinView = new SkinView(view, skinPairs);
            //每次新加载activity就要进行换肤
            applySkin();
            skinViews.add(skinView);
        }
    }

至于SkinView类存储了属性名和资源ID和当前的view

   /**
     * 保存每个view要修改的属性值列表
     */
static class SkinView {
        View view;
        List<SkinPair> skinPairs;

        public SkinView(View view, List<SkinPair> skinPairs) {
            this.view = view;
            this.skinPairs = skinPairs;
        }
}

static class SkinPair {
        String attrName;
        int resId;

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

上述代码就是核心的实现,最后我们需要实现初始化和资源的切换,代码如下 通过皮肤包的路径,Application获取到资源管理器AssetManager获取皮肤包的资源,然后通过SkinResources来实现资源的加载,然后使SkinLayoutFactory做为观察者,当更换皮肤包时更新SkinLayoutFactory. 然后通过SkinAttribute将属性的资源更改为皮肤包的资源

   /**
     * 加载皮肤包
     *
     * @param path
     */
    public void loadSkin(String path) {
        if (path == null || path.isEmpty()) {
            SkinPreference.getInstance().setSkin("");
            SkinResources.getInstance().reset();
        } else {
            try {
                //获取资源管理器 加载皮肤包中的资源
                AssetManager assetManager = AssetManager.class.newInstance();
                Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
                addAssetPath.setAccessible(true);
                addAssetPath.invoke(assetManager, path);

                //加载皮肤包里的资源

                //获取当前应用的资源
                Resources resources = application.getResources();

                //皮肤包的资源
                Resources skinResources = new Resources(assetManager,
                        resources.getDisplayMetrics(), resources.getConfiguration());

                //加载皮肤包资源
                //皮肤包的包名
                PackageManager packageManager = application.getPackageManager();
                //获取一个apk的包名
                PackageInfo info = packageManager.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES);
                String packageName = info.packageName;
                Log.e(TAG, "loadSkin: " + packageName);
                //更换为皮肤包资源
                SkinResources.getInstance().applySkin(skinResources, packageName);
                //记录使用的皮肤包
                SkinPreference.getInstance().setSkin(path);
            } catch (Exception e) {
                e.printStackTrace();
            }

        }

        //通知观察者 //应用皮肤包
        setChanged();
        notifyObservers();
    }

SkinResources的实现,其实就是根据app的资源的资源名和资源类型,通过皮肤包的资源返回皮肤包的资源ID,如果没有就返回当前app的资源ID.

public class SkinResources {
    //皮肤的资源
    private Resources mSkinResources;
    //当前应用的资源
    private Resources mAppResources;
    //皮肤包名
    private String mSkinName;
    //是否使用默认皮肤
    private boolean isDefalueSkin = true;

    public static SkinResources instance;

    public static void init(Context context) {
        if (null == instance) {
            synchronized (SkinResources.class) {
                if (null == instance) {
                    instance = new SkinResources(context);
                }
            }
        }
    }

    public static SkinResources getInstance() {
        return instance;
    }

    public SkinResources(Context context) {
        mAppResources = context.getResources();
    }

    public void reset() {
        mSkinResources = null;
        mSkinName = "";
        isDefalueSkin = true;
    }

    public void applySkin(Resources resources, String name) {
        mSkinResources = resources;
        mSkinName = name;
        isDefalueSkin = false;
    }

    /**
     * 通过资源ID 获取皮肤包中对应的资源ID 然后进行设置
     *
     * @param resId
     * @return
     */
    public int getIdentfier(int resId) {
        if (isDefalueSkin) {
            return resId;
        }

        //通过当前应用资源获取到 资源名 和 资源类型,然后通过资源名  资源类型 资源ID 获取到皮肤包中的资源ID
        String resourceName = mAppResources.getResourceEntryName(resId);
        String resourceTypeName = mAppResources.getResourceTypeName(resId);
        //getIdentifier ?? 获取到皮肤包对应到资源ID
        int skinId = mSkinResources.getIdentifier(resourceName, resourceTypeName, mSkinName);
        return skinId;
    }

    private static final String TAG = "SkinResources";

    public int getColor(int resId) {
        if (isDefalueSkin) {
            return mAppResources.getColor(resId);
        }
        int skinId = getIdentfier(resId);
        Log.e(TAG, "getColor: " + skinId);
        if (skinId == 0) {
            return mAppResources.getColor(resId);
        }
        return mSkinResources.getColor(skinId);
    }

    public ColorStateList getColorStateList(int resId) {
        if (isDefalueSkin) {
            return mAppResources.getColorStateList(resId);
        }
        int skinId = getIdentfier(resId);
        if (skinId == 0) {
            return mAppResources.getColorStateList(resId);
        }
        return mSkinResources.getColorStateList(skinId);
    }

    public Drawable getDrawable(int resId) {
        //如果有皮肤  isDefaultSkin false 没有就是true
        if (isDefalueSkin) {
            return mAppResources.getDrawable(resId);
        }
        int skinId = getIdentfier(resId);
        if (skinId == 0) {
            return mAppResources.getDrawable(resId);
        }
        return mSkinResources.getDrawable(skinId);
    }


    /**
     * 可能是Color 也可能是drawable
     *
     * @return
     */
    public Object getBackground(int resId) {
        String resourceTypeName = mAppResources.getResourceTypeName(resId);

        if (resourceTypeName.equals("color")) {
            return getColor(resId);
        } else {
            // drawable
            return getDrawable(resId);
        }
    }

    public String getString(int resId) {
        try {
            if (isDefalueSkin) {
                return mAppResources.getString(resId);
            }
            int skinId = getIdentfier(resId);
            if (skinId == 0) {
                return mAppResources.getString(skinId);
            }
            return mSkinResources.getString(skinId);
        } catch (Resources.NotFoundException e) {

        }
        return null;
    }

    public Typeface getTypeface(int resId) {
        String skinTypefacePath = getString(resId);
        if (TextUtils.isEmpty(skinTypefacePath)) {
            return Typeface.DEFAULT;
        }
        try {
            Typeface typeface;
            if (isDefalueSkin) {
                typeface = Typeface.createFromAsset(mAppResources.getAssets(), skinTypefacePath);
                return typeface;

            }
            typeface = Typeface.createFromAsset(mSkinResources.getAssets(), skinTypefacePath);
            return typeface;
        } catch (RuntimeException e) {
        }
        return Typeface.DEFAULT;
    }


}

当切换资源通知观察者SkinLayoutFactory的SkinAttribute 来采集属性和更换资源ID.代码的实现都非常简单

/**
     * 更换皮肤
     */
    public void applySkin() {
        //遍历保存的表然后换皮肤
        for (SkinView skinView : skinViews) {
            skinView.applySkin();
        }
    }

 /**
     * 保存每个view要修改的属性值列表
     */
    static class SkinView {
        View view;
        List<SkinPair> skinPairs;

        public SkinView(View view, List<SkinPair> skinPairs) {
            this.view = view;
            this.skinPairs = skinPairs;
        }

        public void applySkin() {
            for (SkinPair skinPair : skinPairs) {
                Drawable left = null, right = null, top = null, bottom = null;
                switch (skinPair.attrName) {
                    case "background":
                        Object background = SkinResources.getInstance().getBackground(skinPair.resId);
                        //可能是一个图片或者颜色
                        if (background instanceof Integer) {
                            //color 选择器
                            view.setBackgroundColor((Integer) background);
                        } else {
                            ViewCompat.setBackground(view, (Drawable) background);
                        }
                        break;
                    case "src":
                        if (view instanceof ImageView) {
                            ImageView imageView = (ImageView) view;
                            Object background1 = SkinResources.getInstance().getBackground(skinPair.resId);
                            if (background1 instanceof Integer) {
                                imageView.setImageDrawable(new ColorDrawable((Integer) background1));
                            } else {
                                imageView.setImageDrawable((Drawable) background1);
                            }
                        }
                        break;
                    case "textColor":
                        int color = SkinResources.getInstance().getColor(skinPair.resId);
                        if (view instanceof TextView) {
                            TextView textView = (TextView) view;
                            textView.setTextColor(color);
                        }
                        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 (null != left || null != right || null != top || null != bottom) {
                    ((TextView) view).setCompoundDrawablesWithIntrinsicBounds(left, top, right,
                            bottom);
                }
            }
        }
    }

OK,现在我们实现了基本的动态换肤功能,目前我们实现的换肤功能还没有实现自定义view和状态栏的换肤功能.下一节来讲解如何实现自定义view的换肤和状态栏的换肤,其实代码写到这里大家都知道了实现的原理可以先试着自己实现.