完整脑图:https://note.youdao.com/s/V2csJmYS
Demo源码:点击下载
技术分析
我们在换肤的第一篇介绍了换肤的核心思想。就是在setContentView()
之前调用setFactory2()
。
第一篇的Demo利用的是AOP切面方法registerActivityLifecycleCallbacks(xxx)
回调在setContentView()
之前,从而在registerActivityLifecycleCallbacks的onActivityCreated()
方法中设置Factory。如此就能拦截到控件的属性,根据拦截到的控件的属性,重新赋值控件的textColor、background等属性,从而实现换肤的。
本Demo的实现,主要基于以下两个狙击点。
1、super.onCreate(savedInstanceState)方法
2、Activity实现了Factory接口
前面说过,只要在setContentView()之前setFactory2()就行。super.onCreate(savedInstanceState)
方法就是在setContentView()方法之前执行的。
一直跟踪super.onCreate(savedInstanceState)方法,最终会发现setFactory的逻辑,如下:
AppCompatDelegateImpl.java(1008)
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(this.mContext);
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");
}
}
它这里传了this
,可以预见AppCompatDelegateImpl
是实现了Factory接口的,最后会通过AppCompatDelegateImpl自身的onCreateView()方法创建的View。
onCreateView()中如何创建的View的,下面再看源码,先知道是通过AppCompatViewInflater
来做控件的具体初始化的。
第一个狙击点可以抽出下图内容:
细心地同学肯定注意到了AppCompatDelegateImpl的installViewFactory()方法中,只有当layoutInflater.getFactory() == null
的时候,才会去setFactory。
也就是说我在super.onCreate(savedInstanceState)之前,先给它setFactory就能走自己Factory的onCreateView()回调。
换肤第一篇中我们是自己去实现Factory2接口,在本例中,就用到了我们第二个狙击点。
Activity实现了Factory接口!!!
也就是说,只要我们在super.onCreate(savedInstanceState)之前,setFactory的时候,传this,就能走Activity
的onCreateView()回调,来对控件属性做操作。
用归纳法,见下图:
最后,也就剩下Activity的onCreateView()中的回调怎么实现了。
直接模拟super.onCreate(savedInstanceState)中AppCompatViewInflater类中的实现就好了。
参考代码:
/**
* 自定义控件加载器(可以考虑该类不被继承)
*/
public final class CustomAppCompatViewInflater extends AppCompatViewInflater {
private String name; // 控件名
private Context context; // 上下文
private AttributeSet attrs; // 某控件对应所有属性
public CustomAppCompatViewInflater(@NonNull Context context) {
this.context = context;
}
public void setName(String name) {
this.name = name;
}
public void setAttrs(AttributeSet attrs) {
this.attrs = attrs;
}
/**
* @return 自动匹配控件名,并初始化控件对象
*/
public View autoMatch() {
View view = null;
switch (name) {
case "LinearLayout":
// view = super.createTextView(context, attrs); // 源码写法
view = new SkinnableLinearLayout(context, attrs);
this.verifyNotNull(view, name);
break;
case "RelativeLayout":
view = new SkinnableRelativeLayout(context, attrs);
this.verifyNotNull(view, name);
break;
case "TextView":
view = new SkinnableTextView(context, attrs);
this.verifyNotNull(view, name);
break;
case "ImageView":
view = new SkinnableImageView(context, attrs);
this.verifyNotNull(view, name);
break;
case "Button":
view = new SkinnableButton(context, 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");
}
}
}
详细实现就参考Demo吧,思路其实很简单,只是会有对setFactory这块逻辑的流程不了解的。建议跟踪着点几遍源码。
About