美文网首页
动态换肤二(筛选需要换肤的 View)

动态换肤二(筛选需要换肤的 View)

作者: radish520like | 来源:发表于2018-05-14 17:12 被阅读0次

    前言:

      上一篇文章我们储备了一些基础知识,现在要开始着手筛选出需要换肤的 View。在上篇文章中说过,需要分两步,先获取所有的 View,再进行筛选。

    上一篇文章地址:https://www.jianshu.com/p/ec0704524528

    获取所有 View

      先创建上篇文章中提到的自定义工厂类。


    SkinLayoutFactory2
    
    /**
     * 自定义 Factory2
     */
    
    public class SkinLayoutFactory implements LayoutInflater.Factory2 {
    
        /**
         * 一般 Android 系统的 View 都存储在这几个包下面
         */
        private static final String[] mClassPrefixList = {
                "android.widget.",
                "android.view.",
                "android.webkit."
        };
    
        /**
         * 系统调用的是两个参数的构造方法,我们也调用这个构造方法
         */
        private static final Class<?>[] mConstructorSignature = new Class[]{
                Context.class, AttributeSet.class};
    
        private static final String SPOT = ".";
    
        /**
         * 创建 View 的过程会回调到该 onCreateView 的方法中,并且每有一个 View 就调用一次
         */
        @Override
        public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
            if (TextUtils.isEmpty(name)) {
                return null;
            }
            /*
                我们模仿源码那样来创建 View
             */
            View view = createViewFromTag(name, context, attrs);
            /*
                这里如果 View 返回的是 null 的话,就是自定义控件,
                自定义控件不需要我们进行拼接,可以直接拿到全类名
             */
            if (view == null) {
                view = createView(name,context,attrs);
            }
    
            return view;
        }
    
        /**
         * 真正创建 View 的方法
         */
        private View createView(String name, Context context, AttributeSet attributeSet) {
            try {
                //通过反射来获取 View 实例对象
                Class<? extends View> aClass = context.getClassLoader().loadClass(name).asSubclass(View.class);
                Constructor<? extends View> constructor = aClass.getConstructor(mConstructorSignature);
                if (constructor != null) {
                    return constructor.newInstance(aClass, attributeSet);
                } else {
                    throw new Exception("该 View 没有指定的构造函数");
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    
        @Override
        public View onCreateView(String name, Context context, AttributeSet attrs) {
            return null;
        }
    
        /**
         * 创建系统自带的 View
         *
         * @param name         View 的名字,比如 ImageView,Button,EditText
         * @param context      上下文
         * @param attributeSet 属性
         * @return             View
         */
        private View createViewFromTag(String name, Context context, AttributeSet attributeSet) {
            //如果 name 中包含了".",暂时不做处理,返回 null
            if (SPOT.equals(name)) {
                return null;
            }
    
            View view = null;
            //拼接 name
            for (int i = 0; i < mClassPrefixList.length; i++) {
                view = createView(mClassPrefixList[i]+name,context,attributeSet);
                if(view != null){
                    break;
                }
            }
    
            return view;
        }
    }
    

      获取所有的 View 已经完成,但是还是有些问题,看 createViewFromTag() 方法,加入,我们的布局文件中有两个 ImageView,两个 Button,三个 TextView,那是不是意味着我们这几个 View 需要反射创建对象 7 次呢?按照我们的代码来说,答案是肯定的,这样不好,我们使用缓存,来提高一下效率。

    import android.content.Context;
    import android.text.TextUtils;
    import android.util.AttributeSet;
    import android.view.LayoutInflater;
    import android.view.View;
    
    import java.lang.reflect.Constructor;
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * 自定义 Factory2
     */
    
    public class SkinLayoutFactory implements LayoutInflater.Factory2 {
    
        /**
         * 用于缓存
         */
        private static final Map<String,Constructor<? extends View>> sConstructorMap = new HashMap<>();
    
        @Override
        public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
            //...
        }
    
        /**
         * 真正创建 View 的方法
         */
        private View createView(String name, Context context, AttributeSet attributeSet) {
            Constructor<? extends View> constructor = sConstructorMap.get(name);
            //通过反射来获取 View 实例对象
            if(constructor == null){
                try {
                    Class<? extends View> aClass = context.getClassLoader().loadClass(name).asSubclass(View.class);
                    constructor = aClass.getConstructor(mConstructorSignature);
                    sConstructorMap.put(name,constructor);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
    
            if(constructor != null){
                try {
                    return constructor.newInstance(context,attributeSet);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            return null;
        }
        //...
    }
    

      至此,所有的 View 已经获取完毕,接下来,我们需要对 View 进行筛选。

    筛选 View

      需要换肤的 View 肯定是设置了一些可换肤属性,我们需要根据获取的 View ,拿到它在 xml 中设置的属性,然后根据属性来判断是否要进行换肤。
      新建 SkinAttribute 类,来进行筛选。

    import android.content.res.ColorStateList;
    import android.graphics.drawable.ColorDrawable;
    import android.graphics.drawable.Drawable;
    import android.support.v4.view.ViewCompat;
    import android.util.AttributeSet;
    import android.view.View;
    import android.widget.ImageView;
    import android.widget.TextView;
    
    import com.radish.android.skin_core.util.SkinResources;
    import com.radish.android.skin_core.util.SkinThemeUtils;
    
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * 筛选需要进行换肤的 View
     */
    
    public class SkinAttribute {
    
        private static final List<String> mAttributes = new ArrayList<>();
    
        private List<SkinView> skinViews = new ArrayList<>();
    
        /**
         * 如果 View 设置了如下的属性,
         * 我们还需要进行下一步的判断,才能知道该 View 是否需要换肤
         */
        static {
            mAttributes.add("background");
            mAttributes.add("src");
            mAttributes.add("textColor");
            mAttributes.add("drawableLeft");
            mAttributes.add("drawableTop");
            mAttributes.add("drawableRight");
            mAttributes.add("drawableBottom");
        }
    
    
        /**
         * 对 View 进行筛选
         *
         * @param view         被筛选的 View
         * @param attributeSet 被筛选的 View 对应的 attributeSet
         */
        public void filtrate(View view, AttributeSet attributeSet) {
            List<SkinPair> skinPairs = new ArrayList<>();
            for (int i = 0; i < attributeSet.getAttributeCount(); i++) {
                String attributeName = attributeSet.getAttributeName(i);
                if (mAttributes.contains(attributeName)) {
                    //该 View 属性中包含了需要换肤的属性
                    String attributeValue = attributeSet.getAttributeValue(i);
                    /*
                        这里,假如获取的值是这样的 android:textColor="#ffffff"
                        写死了,那么我们不管
                     */
                    if (attributeValue.startsWith("#")) {
                        continue;
                    }
    
                    int resId;
                    /*
                        android:background="?attr/colorAccent"
                        那么我们需要去 style 中再次获取 resId
                     */
                    if (attributeValue.startsWith("?")) {
                        int attrId = Integer.valueOf(attributeValue.substring(1));
                        resId = SkinThemeUtils.getResId(view.getContext(), new int[]{attrId})[0];
                    } else {
                        /*
                            可以直接获取 resId
                            输出 attributeValue 的值是这样的:@开头的资源 attributeValue = @2130837604
                         */
                        resId = Integer.valueOf(attributeValue.substring(1));
                    }
    
                    if (resId != 0) {
                        /*
                            这里保存的是我们需要换肤的属性--资源 id 这个映射关系,然后将这些映射关系保存到 List 里面
                         */
                        SkinPair skinPair = new SkinPair(attributeName, resId);
                        skinPairs.add(skinPair);
                    }
                }
            }
    
            /*
                这里保存以后,我们的对应关系是 view -- 属性表,其中属性表对应的关系是 属性名 -- 资源 id
             */
            if (!skinPairs.isEmpty()) {
                SkinView skinView = new SkinView(view, skinPairs);
                skinView.applySkin();
                skinViews.add(skinView);
            }
        }
    
        /**
         * 提供外部调用换肤的方法
         */
        public void applySkin() {
            for(int i = 0 ;i < skinViews.size();i++){
                SkinView skinView = skinViews.get(i);
                skinView.applySkin();
            }
        }
    
        private 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,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 "textColor":
                            ColorStateList colorStateList = SkinResources.getInstance().getColorStateList(skinPair.resId);
                            ((TextView) view).setTextColor(colorStateList);
                            break;
                        case "src":
                            Object bg = SkinResources.getInstance().getBackground(skinPair.resId);
                            if(bg instanceof Integer){
                                ((ImageView) view).setImageDrawable(new ColorDrawable((Integer) bg));
                            }else{
                                ((ImageView) view).setImageDrawable((Drawable) bg);
                            }
                            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(left != null || top != null || right != null || bottom != null){
                        ((TextView) view).setCompoundDrawablesWithIntrinsicBounds(left,top,right,bottom);
                    }
                }
            }
        }
    
        private static class SkinPair {
            String attributeName;
            int resId;
    
            SkinPair(String attributeName, int resId) {
                this.attributeName = attributeName;
                this.resId = resId;
            }
        }
    }
    
    import android.content.Context;
    import android.content.res.TypedArray;
    
    /**
     * 工具类
     */
    
    public class SkinThemeUtils {
    
        /**
         * 无法直接从 AttributeSet 中获取到资源 id 的情况下,需要通过转换的方式来进行获取
         * 比如说,android:background="?attr/colorAccent"
         * 这里 ? 后面拿到值后,还需要去 style.xml 文件中继续获取
         * 对应资源 id,在 style.xml 文件中拿到的才是资源 id
         * @param context    上下文
         * @param attrs      需要获取的资源 id
         * @return           资源 id
         */
        public static int[] getResId(Context context, int[] attrs) {
            int[] resId = new int[attrs.length];
            TypedArray typedArray = context.obtainStyledAttributes(attrs);
            for (int i = 0; i < typedArray.length(); i++) {
                resId[i] = typedArray.getResourceId(i,0);
            }
            typedArray.recycle();
            return resId;
        }
    }
    

      然后千万不要忘了调用 skinAttribute 的 filtrate() 方法。

    /**
     * 自定义 Factory2
     */
    
    public class SkinLayoutFactory implements LayoutInflater.Factory2 {
    
        private SkinAttribute skinAttribute;
    
        public SkinLayoutFactory(SkinAttribute skinAttribute){
            this.skinAttribute = skinAttribute;
        }
    
        //...
    
        /**
         * 创建 View 的过程会回调到该 onCreateView 的方法中,并且每有一个 View 就调用一次
         */
        @Override
        public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
           //...
            if(skinAttribute != null){
                skinAttribute.filtrate(view,attrs);
            }
    
            return view;
        }
    
        //...
    }
    

      写了这么多代码,先测试一下吧。改一下 MainActivity 的布局文件:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:gravity="center"
        android:background="?attr/colorAccent">
    
        <TextView
            android:id="@+id/tv_click"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="换肤"
            android:padding="10dp"
            android:background="#ffffff"
            android:layout_marginBottom="30dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    
        <TextView
            android:id="@+id/tv_other"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:padding="10dp"
            android:textColor="@color/black"
            android:text="还原"
            android:background="@color/black"/>
    
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="20dp"
            android:text="button"/>
    
    </LinearLayout>
    

      然后在 MainActivity 中加上这句话

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            LayoutInflater layoutInflater = getLayoutInflater();
            try {
                Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
                field.setAccessible(true);
                field.setBoolean(layoutInflater,false);
            } catch (Exception e) {
                e.printStackTrace();
            }
    
            layoutInflater.setFactory2(new SkinLayoutFactory(new SkinAttribute()));
            setContentView(R.layout.activity_main);
        }
    

      具体这句话的作用后面会说,然后我们在将前面保存 View 的集合输出:

        private void testSkinPairs(List<SkinPair> skinPairs) {
            for (int i = 0; i < skinPairs.size(); i ++) {
                SkinPair skinPair = skinPairs.get(i);
                Log.i(TAG, "abc : skinPair.View = " + skinPair.view.getClass().getSimpleName()");
            }
        }
    

      看 MainActivity 的布局文件,LinearLayout 有 background 属性,并且值是 ?attr/colorAccent,需要换肤,进行保存;第一个 TextView 虽然有 background,但值是写死的,不保存;第二个 TextView 不但有 background,还有 textColor,而且值都是可以换肤的,保存;而 Button 没有任何要换肤的属性,不保存。那么结果就是一个 LinearLayout 和 一个 TextView

    abc : skinPair.View = LinearLayout
    abc : skinPair.View = TextView
    

    没问题。

    下一篇文章地址:https://www.jianshu.com/p/1139df041cb6

    相关文章

      网友评论

          本文标题:动态换肤二(筛选需要换肤的 View)

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