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

前言:

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

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

获取所有 View

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


动态换肤二(筛选需要换肤的 View)_第1张图片
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 aClass = context.getClassLoader().loadClass(name).asSubclass(View.class);
            Constructor 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> 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 constructor = sConstructorMap.get(name);
        //通过反射来获取 View 实例对象
        if(constructor == null){
            try {
                Class 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 mAttributes = new ArrayList<>();

    private List 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 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 skinPairs;

        public SkinView(View view, List 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 的布局文件:




    

    

    

  然后在 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 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))