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

前言:

  上一篇文章我们储备了一些基础知识,现在要开始着手筛选出需要换肤的 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容