架构师学习--内置换肤

一、换肤原理

首先需要明白以下几点:

  1. setContentView()原理
  2. 布局加载工程Factory2
  3. onCreateView()方法
  4. super.onCreate()原理
  5. 如何拦截Factory2

下面就以上几点进行源码分析

1、 setContentView()

(1)都知道通过setContentView()能够加载布局,那么布局是xml文件,它是如何加载到内存中的呢?首先跟踪源码,最终会跟到AppCompatDelegateImpl类中的setContentView()方法,源码如下:

public void setContentView(int resId) {
		//初始化父容器
        this.ensureSubDecor();
        
        //拿到id为R.id.content的父布局,我们自己的布局view将会添加在它之上
        ViewGroup contentParent = (ViewGroup)this.mSubDecor.findViewById(16908290);
        
        contentParent.removeAllViews();
        
		//解析我们自己的布局
        LayoutInflater.from(this.mContext).inflate(resId, contentParent);
        
		//通知界面显示
        this.mOriginalWindowCallback.onContentChanged();
    }

(2)重点分析解析布局过程,继续跟踪inflate()方法,最终跟到LayoutInflater类的inflate()方法,主要代码如下:

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        if (TAG_MERGE.equals(name)) {
                    ....
        } else {
                //-------------1、解析根布局,创建根view
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                ViewGroup.LayoutParams params = null;

				...
					
				//-------------2、根据根view解析子view,创建子view, 最总创建子view还是调用createViewFromTag()方法
                rInflateChildren(parser, temp, attrs, true);

				...
                //-------------3 赋值返回view
                if (root == null || !attachToRoot) {
                    result = temp;
                }
        }
			...

        return result;
		
    }

这里主要进行解析xml,一开始先调用createViewFromTag()并且返回view。然后将view作为参数传递给rInflateChildren()方法,处理完成后最后将view返回。看一下rInflateChildren()方法做了什么处理,该方法最终调用rInflate()方法源码如下:

void rInflate(XmlPullParser parser, View parent, Context context,
            AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {

       .........................

		//-----------------------------------1、while循环进行xmlPullParser解析
        while (((type = parser.next()) != XmlPullParser.END_TAG ||
                parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {

           ...........................//标签判断

		//--------------------------------2、最总调用createViewFromTag()返回view
           final View view = createViewFromTag(parent, name, context, attrs);
           final ViewGroup viewGroup = (ViewGroup) parent;
            final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
          rInflateChildren(parser, view, attrs, true);
            viewGroup.addView(view, params);
        }
        .......................
    }

可以发现还是会调用createViewFromTag()方法。

(2)看一下createViewFromTag()方法,在LayoutInflater类中,重点来了,前方高能,源码如下:

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
       ...

        View view;
		
		//---------------1、如果mFactory2不为空,会调用onCreateView()方法创建view,参数中name为控件名称,attrs为控件属性
        if (mFactory2 != null) {
            view = mFactory2.onCreateView(parent, name, context, attrs);
        } else if (mFactory != null) {
                view = mFactory.onCreateView(name, context, attrs);
        } else {
                view = null;
        }

		//-------------2、上面没有创建view、这这里初始化view
        if (view == null) {
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = context;
                try {
				
				//-------------3、判断是不是自定义控件
                    if (-1 == name.indexOf('.')) {
                        view = onCreateView(parent, name, attrs);
                    } else {
					//-------------4、非自定义控件
                        view = createView(name, null, attrs);
                    }
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
        }

        return view;
    }

注释1告诉我们如果mFactory2不为空,就会调用它的onCreateView()方法创建view,并且将控件的名称和属性作为参数传递进去。看到这里难免会有疑问mFactory2是什么?其实它就是一个接口,并且实现了Factory接口,两个接口的区别就是回调方法参数不一样。注释4处通过createView()方法返回系统控件。源码如下:

 public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
			
		//--------------1、拿到当前view的构造方法
        Constructor<? extends View> constructor = sConstructorMap.get(name);
        
		......
		//------------参数之类的初始化
		......

		//-------------2、通过反射创建view			
        final View view = constructor.newInstance(args);
		
        ....
		
		//---------3、返回
        return view;

        
    }

没错,你没有看错,系统通过反射创建了这个view,并返回。回到上面的createViewFromTag()方法,既然mFactory2会调用onCreateView()方法创建一个view,那么我们可不可以在这个方法上面做点文章呢?比如返回我们自己的控件view!!!先放这里,去看一下Factory2.

2、Factory2

它是一个接口,实现了Factory,在LayoutInflater类中,源码如下:

	public interface Factory {
      
        public View onCreateView(String name, Context context, AttributeSet attrs);
    }

    public interface Factory2 extends Factory {
       
        public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
    }

既然是接口,那么它的实现类是哪些呢?那就不得不看看 super.onCreate()方法了,这里只需要知道Factory2 接口提供了一个onCreateView()方法,并且方法中参数包含控件名称和属性等。

3、onCreateView()

(1)在上面代码分析中会调用mFactory2.onCreateView()方法,看一下是如何创建view的,跟踪源码会进入AppCompatDelegateImpl类的onCreateView()方法,源码如下:

public View createView(View parent, String name, @NonNull Context context, @NonNull AttributeSet attrs) {
    if (this.mAppCompatViewInflater == null) {
	...........
         //--------------------------------1、创建mAppCompatViewInflater 
         this.mAppCompatViewInflater = new AppCompatViewInflater();
          ..........
         }
	//--------------------------------2、调用createView()方法
       return this.mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext, IS_PRE_LOLLIPOP, true, VectorEnabledTintResources.shouldBeUsed());
}

(2)注释2处createView(),AppCompatViewInflater类的createView()方法,代码如下:

final View createView(View parent, String name, @NonNull Context context, @NonNull AttributeSet attrs, boolean inheritContext, boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
        ...
		
        View view = null;
        byte var12 = -1;
		
		//--------------1、匹配基础控件,比如TextView,ImageView,这里没有匹配RelativeLayout这样的父容器控件
        switch(name.hashCode()) {
        ...
        case -938935918:
            if (name.equals("TextView")) {
                var12 = 0;
            }
            break;
       ...
        case 1125864064:
            if (name.equals("ImageView")) {
                var12 = 1;
            }
            break;
        ...
        }


		//`------------2、创建兼容的view
        switch(var12) {
        case 0:
            view = this.createTextView(context, attrs);
            this.verifyNotNull((View)view, name);
            break;
        case 1:
            view = this.createImageView(context, attrs);
            this.verifyNotNull((View)view, name);
            break;
       ...
	   }

        return (View)view;
    }

这里为了兼容V7包做了控件的兼容处理,通过调用createTextView()、createImageView()等方法,截图如下:
在这里插入图片描述

4、super.onCreate()

(1)跟踪源码会进入父类AppCompatActivity的onCreate()方法,源码如下:

protected void onCreate(@Nullable Bundle savedInstanceState) {
       ......
       
        delegate.installViewFactory();
       .....
       
       //-------------------主要为了调用其父类Activity的onCreate方法来实现对界面的图画绘制工作
        super.onCreate(savedInstanceState);
    }

(2)绘制我们不做分析,重点看installViewFactory()方法,最后进入AppCompatDelegateImpl类的installViewFactory()方法,源码如下:

public void installViewFactory() {
        LayoutInflater layoutInflater = LayoutInflater.from(this.mContext);
		//----------这句话的意思是如过Factory不为空了,就不能再次进行创建,一开始界面初始化肯定为空的,就会进行赋值操作
        if (layoutInflater.getFactory() == null) {
            LayoutInflaterCompat.setFactory2(layoutInflater, this);
        } else if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
            Log.i("AppCompatDelegate", "The Activity's LayoutInflater already has a Factory installed so we can not install AppCompat's");
        }

    }

以上会判断factory是否为空,系统为了避免重复设置Factory

public void setFactory2(Factory2 factory) {

	//--------------------1、这个属性为true就会抛出异常
        if (mFactorySet) {
            throw new IllegalStateException("A factory has already been set on this LayoutInflater");
        }
       ....
        mFactorySet = true;
        if (mFactory == null) {
            mFactory = mFactory2 = factory;
        } else {
            mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
        }
    }

对mFactory 和mFactory2 进行赋值,并且进行工厂合并。注意注释1处,mFactorySet为true将会抛出异常,这个属性将会在后面介绍到

总结一下:
1、setContentView()进行了xml的解析,并且将view填充到父view之上
2、创建view的过程中,通过布局加载工程Factory2交给实现类创建view并返回
3、创建view的是通过Factory2的onCreateView()方法,最终实现在AppCompatViewInflater类的createView()方法中
4、super.onCreate()初始化布局加载工厂Factory2
通过以上几点分析,发现我们可以通过拦截Factory2的onCreateView()方法可以进行控件信息的拦截,可以将这些控件收集起来,当需要换肤的时候可以拿到当前控件,并将它的控件属性值(比如background,textColor等)进行修改即可。
5、如何拦截Factory2

拦截的方法很简单,只需要将系统的mFactory2替换成自己的即可,在哪里替换呢?我们先看一下我们平常写的Acctivity中是否有onCreatView()方法。我去!!!还真有。这是为什么呢?其实我们activity的父类Activity类是实现了Factory2接口的,截图如下:
在这里插入图片描述
之前分析在super.onCreate()方法种说到,当factory不为空的时候就会抛出异常,说明我们初始化factory要在super.onCreate()方法之前进行,这样mFactory2就会被初始化了,之后就可以通过当前activity的onCreateView()方法收集控件信息了。

二、代码实现

1、创建依赖module,命名为skin

在这里插入图片描述

2、在module中,创建父类SkinActivity.java
/**
 * 供子类继承
 */
public class SkinActivity extends AppCompatActivity {
    private SkinAppCompatViewInflater mAppCompatViewInflater;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {

        //拦截新系统factory2
        LayoutInflater layoutInflater = LayoutInflater.from(this);
        LayoutInflaterCompat.setFactory2(layoutInflater, this);

        super.onCreate(savedInstanceState);
    }

    @Override
    public View onCreateView(String name, Context context, AttributeSet attrs) {
        //判断是否打开换肤
        if (openChangeSkin()) {
            if (this.mAppCompatViewInflater == null) {
                mAppCompatViewInflater = new SkinAppCompatViewInflater(context);
            }

            //控件名称和属性传递过去
            mAppCompatViewInflater.setName(name);
            mAppCompatViewInflater.setAttrs(attrs);

            //返回自定义的view 匹配不到自定义view,返回null,交给系统反射创建
            return mAppCompatViewInflater.matchView();
        }

        /*
            1、没有打开换肤,调用系统方法
            2、打开换肤、但是没有匹配到view就会通过系统方法反射创建view
         */

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

    /**
     * 提供给子类activity,是否需要开启换肤
     *
     * @return 默认为false
     */
    protected boolean openChangeSkin() {
        return false;
    }


    /**
     * 开始换肤
     *
     * @param nightMode 当前白天还是黑夜模式
     */
    protected void applySkin(@AppCompatDelegate.NightMode int nightMode) {

        //判断版本号,适配5.0以上,5.0以下不适合
        final boolean isPost21 = Build.VERSION.SDK_INT >= 21;

        getDelegate().setLocalNightMode(nightMode);

        //修改状态栏、标题栏和导航栏颜色
        if (isPost21) {
            // 换状态栏
            StatusBarUtils.forStatusBar(this);
            // 换标题栏
            ActionBarUtils.forActionBar(this);
            // 换底部导航栏
            NavigationUtils.forNavigation(this);
        }

        //拿到顶层布局
        View decorView = getWindow().getDecorView();
        startChangeSkin(decorView);
    }

    /**
     * 执行开始换肤
     */
    protected void startChangeSkin(View view) {

        //判断当前view是否实现了该接口,如果是的话就需要到类中更新
        if (view instanceof ViewChange) {

            //子view进行换肤
            ((ViewChange) view).viewChange();
        }

        //遍历子控件,递归调用
        if (view instanceof ViewGroup) {
            ViewGroup vg = (ViewGroup) view;
            for (int i = 0; i < vg.getChildCount(); i++) {
                View childView = vg.getChildAt(i);
                startChangeSkin(childView);
            }
        }

    }
}

在super.oncreate()方法之前调用了设置当前activity的factory2。在onCreateView()方法中调用mAppCompatViewInflater.matchView();并将控件的name和attrs传递过去。看一下mAppCompatViewInflater的代码:

3、在module中,创建父类SkinAppCompatViewInflater.java
/**
 * 可以不继承 ,当matchView()返回空的时候,系统在setContentView()中会判断view是否为空,为空的话会通过反射创建出系统的view
 */
public class SkinAppCompatViewInflater /*extends AppCompatViewInflater*/ {

    private Context mContext;

    public SkinAppCompatViewInflater(Context context) {
        this.mContext = context;
    }

    private String name;
    private AttributeSet attrs;


    public void setName(String name) {
        this.name = name;
    }

    public void setAttrs(AttributeSet attrs) {
        this.attrs = attrs;
    }

    public View matchView(){

        View view = null;
        switch (name){
            case "LinearLayout":
                view = new SkinLinearLayout(mContext,attrs);
                this.verifyNotNull(view, name);
                break;

            case "RelativeLayout":
                view = new SkinRelativeLayout(mContext,attrs);
                this.verifyNotNull(view, name);
                break;

            case "TextView":
                view = new SkinTextView(mContext,attrs);
                this.verifyNotNull(view, name);
                break;

            case "Button":
                view = new SkinButton(mContext,attrs);
                this.verifyNotNull(view, name);
                break;
        }

        return view;

    }

    /**
     * 校验控件不为空(源码方法,由于private修饰,只能复制过来了。为了代码健壮,可有可无)
     *
     * @param view 被校验控件,如:AppCompatTextView extends TextView(v7兼容包,兼容是重点!!!)
     * @param name 控件名,如:"ImageView"
     */
    private void verifyNotNull(View view, String name) {
        if (view == null) {
            throw new IllegalStateException(this.getClass().getName() + " asked to inflate view for <" + name + ">, but returned null");
        }
    }
}

显而易见,它是一个匹配view的过程,匹配到以后就会创建我们自定义的view并返回。

回到上面SkinActivity代码中,StatusBarUtils,ActionBarUtils和NavigationUtils工具类处理了状态栏、标题栏和导航栏颜色变化,后面源码会提供。在startChangeSkin()方法中调用了((ViewChange) view).viewChange()进行换肤。看一下ViewChange代码

4、在module中,创建ViewChange.java
public interface ViewChange {
    void viewChange();
}

它是一个接口,供我们的自定义view实现,这样调用上面的方法就可以在viewChange()方法中各自执行换肤代码。这里仅仅参考自定义的TextView控件,命名为SkinTextView

5、在module中,创建SkinTextView.java
public class SkinTextView extends AppCompatTextView implements ViewChange {
    private SkinBean skinBean;

    public SkinTextView(Context context) {
        this(context, null);
    }
    public SkinTextView(Context context, AttributeSet attrs) {
        this(context, attrs, android.R.attr.textViewStyle);
    }

    public SkinTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        skinBean = new SkinBean();
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.SkinTextView,
                defStyleAttr, 0);
                
		//临时存储相关属性	
        skinBean.setResource(typedArray, R.styleable.SkinTextView);

        typedArray.recycle();
    }

    @Override
    public void viewChange() {
        //改变控件背景颜色
        int bgKey = R.styleable.SkinTextView[R.styleable.SkinTextView_android_background];
        int bgColor = skinBean.getResource(bgKey);

        //动态改变背景颜色
        if (bgColor > 0) {
            // 兼容包转换
            Drawable drawable = ContextCompat.getDrawable(getContext(), bgColor);
            // 控件自带api,这里不用setBackgroundColor()因为在9.0测试不通过
            // setBackgroundDrawable本来过时了,但是兼容包重写了方法
            setBackgroundDrawable(drawable);
        }

        int textColorKey = R.styleable.SkinTextView[R.styleable.SkinTextView_android_textColor];
        int textColor = skinBean.getResource(textColorKey);

        if (textColor > 0) {
            ColorStateList color = ContextCompat.getColorStateList(getContext(), textColor);
            this.setTextColor(color);
        }
    }
}

在自定义控件中会临时存储当前控件的属性值,保存在skinBean中,注意对于TextView来说,换肤只需要更改字体颜色和背景颜色,所以这里仅仅存储这两个属性,大大的节省了资源,属性配置在attrs.xml中,截图如下:
在这里插入图片描述

6、在module中,创建SkinBean.java
/**
 * 存放控件的相关信息
 */
public class SkinBean {
    private SparseIntArray sourceArray;

    public SkinBean() {
        sourceArray = new SparseIntArray();
    }


    public void setResource(TypedArray typedArray, int[] skinLinearLayout) {

        for (int i = 0; i < typedArray.length(); i++) {
            int key = skinLinearLayout[i];
            int value = typedArray.getResourceId(i,-1);
            sourceArray.put(key,value);
        }
    }

    public int getResource(int key){
        return sourceArray.get(key);
    }
}

以上就完成了SkinModule的配置,现在在需要换肤的Activity继承SkinActivity即可,比如MainActivity

7、在app中,创建MainActivity.java
public class MainActivity extends SkinActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    @Override
    protected boolean openChangeSkin() {
        return true;
    }

    /**
     * 换肤切换
     */
    public void changeColor(View view) {
        int uiMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;

        switch (uiMode) {
            case Configuration.UI_MODE_NIGHT_NO:
                applySkin(AppCompatDelegate.MODE_NIGHT_YES);
                PreferencesUtils.putBoolean(this, "isNight", true);
                break;
            case Configuration.UI_MODE_NIGHT_YES:
                applySkin(AppCompatDelegate.MODE_NIGHT_NO);
                PreferencesUtils.putBoolean(this, "isNight", false);
                break;
            default:
                break;
        }
    }
}

有以下几点需要注意:

  1. 内置换肤需要在app的res目录下配置values和values-night两个文件夹,并且对于color属性来说,白天和黑夜的颜色值名称要一致。
    在这里插入图片描述在这里插入图片描述在这里插入图片描述
  2. 如果换肤过程中出现闪烁或者导航栏不能换肤的情况,请在AndroidManifest.xml中加如下配置:
    在这里插入图片描述
    最后上一波效果图
    在这里插入图片描述

三、性能对比

为什么要拓展一下,针对目前市面上的换肤,也是通过拦截Factory2的onCreateView()方法实现。但是他们是在super.onCreate()方法之后,setContentView()之前处理。大致步骤如下:

1、注册ActivityLifecycleCallbacks,因为这个可以看成是系统的拦截所有Activity生命周期的方式(AOP的实现),在重载方法onActivityCreated()中通过反射修改属性mFactorySet值为false,对的这个属性之前有提到过,再看一次

public void setFactory2(Factory2 factory) {

   //--------------------1、这个属性为true就会抛出异常
       if (mFactorySet) {
           throw new IllegalStateException("A factory has already been set on this LayoutInflater");
       }
      ....
       mFactorySet = true;
       if (mFactory == null) {
           mFactory = mFactory2 = factory;
       } else {
           mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
       }
   }

为什么设置成false?看源码就明白了,当系统在调用super.onCreate()方法的时候就已经把mFactorySet 设置为true了,只有设置为false,才能将Factory2重新赋值,进行拦截。

大致浏览下onActivityCreated()方法的代码

@Override
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
        Log.d(TAG, "onActivityCreated");

        LayoutInflater layoutInflater = LayoutInflater.from(activity);

        // 利用反射去修改mFactorySet的值为false,防止抛出 A factory has already been set on this...
        // 反正就是为了提高健壮性
        try {
            // 尽量使用LayoutInflater.class,不要使用layoutInflater.getClass()
            Field mFactorySet = LayoutInflater.class.getDeclaredField("mFactorySet");
            // 源码312行
            mFactorySet.setAccessible(true);
            mFactorySet.set(layoutInflater, false);
        } catch (Exception e) {
            e.printStackTrace();
            Log.e(TAG, "reflect failed e: " + e.toString());
        }

        skinFactory = new SkinFactory(activity);
        // mFactorySet = true是无法设置成功的(源码312行)
        LayoutInflaterCompat.setFactory2(layoutInflater, skinFactory);
    }

2、注册观察者(监听用户操作,点击了换肤,通知观察者更新)

3、写个类如上面的SkinFactory继承Factory2,并实现了观察者接口

4、在SkinFactory的onCreateView()方法中收集所有的控件的所有属性。

5、用户点击换肤了,通知观察者更新,比如先找到Textview,再找到Textview的textColor和backgroundColor等属性,进行改变。这样就会导致两层的for循环,第一层遍历所有的控件,第二处遍历所有控件的所有属性名称

这样做的缺点:

1、临时集合收集所有控件,换肤时需要双层遍历。如果布局里面有几十个控件,那么会导致性能消耗明显增加。
2、置换setContentView()方法,容易不兼容。。
3、对自定义控件兼容很难。

四、实现过程的疑惑点

不拦截系统的onCreatView()方法,即没有设置Factory2,发现在activity的onCreateView()方法同样可以收集控件,但是收集到的只是viewGroup控件和一些系统默认添加的节点控件,而没有我们布局中的TextView,ImageView等这些子控件。这是为什么?
答:debug源码你会发现在FragmentActivity中的onCreateView()方法,截图如下:
在这里插入图片描述
这里的View变量v就会去creatView()方法中找有没有这个控件(之前知道creatView()方法只会返回兼容的控件比如TextView,ImageView)。而LinerLayout,RelativeLayout是不在里面的。所以返回v = null,这样就会返回。反之找到了就不会返回。

最后,高效的换肤代码地址如下:

代码地址app内置换肤

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值