思路分析
AppCompatActivity在setContentView的时候, 会解析xml文件,用xml文件的每个节点信息创建与之对应的View对象。
跟一下setContentView的代码
会走到AppCompatDelegate的setContentView方法。这个方法是个抽象方法, 其实现为AppCompatDelegateImpl的setContentView
public void setContentView(int resId) {
this.ensureSubDecor();
ViewGroup contentParent = (ViewGroup)this.mSubDecor.findViewById(16908290);
contentParent.removeAllViews();
LayoutInflater.from(this.mContext).inflate(resId, contentParent);
this.mOriginalWindowCallback.onContentChanged();
}
这个方法内部使用了LayoutInflater的inflate方法,并且把布局资源id和父布局的View传递过去了。经过反复横跳,最后调用了如下方法
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;
try {
...
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
...
return result;
}
}
这个方法里面调用了createViewFromTag方法, 用xml的节点信息创建View,下面看一下这个方法的详细信息
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
View view;
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
}
重点就是 if (mFactory2 != null) 这行代码。 如果mFactory2不为空,就走mFactory2的onCreateView方法。这个代码就是我们实现动态换肤的关键点。我们只要自己写一个Factory2,并且赋值进来。那么View的创建就会走我们的代码,我们就可以拿到View的各种属性,
从而在动态换肤的时候,修改这些属性。
关于Factory2接口
重大发现
public class Activity extends ContextThemeWrapper
implements LayoutInflater.Factory2,
Window.Callback, KeyEvent.Callback,
OnCreateContextMenuListener, ComponentCallbacks2,
Window.OnWindowDismissedCallback, WindowControllerCallback,
AutofillManager.AutofillClient {
Activity 是实现了Factory2接口的, 只要在Activity 中设置LayoutInflater的Factory2属性,就可以将View的创建流程截获到。
那么创建一个SkinActivity,重写Factory2的方法onCreateView。
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
// 如果开启了换肤,则使用自定义的ViewInflater,否则使用父类的
if (openChangeSkin()) {
if (viewInflater == null) {
viewInflater = new CustomAppCompatViewInflater(context);
}
viewInflater.setName(name);
viewInflater.setAttrs(attrs);
return viewInflater.autoMatch();
}
return super.onCreateView(parent, name, context, attrs);
}
这样,我们就把创建View的过程截获了。 然后我们自定义ViewInflater,自己写创建View的逻辑, 根据不同的标签, 返回自定义的支持换肤的控件View。如下所示
/**
* @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;
}
而这些自定义的控件,例如TextView,如下所示:
/**
* 继承TextView兼容包,9.0源码中也是如此
* 参考:AppCompatViewInflater.java
* 86行 + 138行 + 206行
*/
public class SkinnableTextView extends AppCompatTextView implements ViewsMatch {
private AttrsBean attrsBean;
public SkinnableTextView(Context context) {
this(context, null);
}
public SkinnableTextView(Context context, AttributeSet attrs) {
this(context, attrs, android.R.attr.textViewStyle);
}
public SkinnableTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
attrsBean = new AttrsBean();
// 根据自定义属性,匹配控件属性的类型集合,如:background + textColor
TypedArray typedArray = context.obtainStyledAttributes(attrs,
R.styleable.SkinnableTextView,
defStyleAttr, 0);
// 存储到临时JavaBean对象
attrsBean.saveViewResource(typedArray, R.styleable.SkinnableTextView);
// 这一句回收非常重要!obtainStyledAttributes()有语法提示!!
typedArray.recycle();
}
@Override
public void skinnableView() {
// 根据自定义属性,获取styleable中的background属性
int key = R.styleable.SkinnableTextView[R.styleable.SkinnableTextView_android_background];
// 根据styleable获取控件某属性的resourceId
int backgroundResourceId = attrsBean.getViewResource(key);
if (backgroundResourceId > 0) {
// 是否默认皮肤
if (SkinManager.getInstance().isDefaultSkin()) {
// 兼容包转换
Drawable drawable = ContextCompat.getDrawable(getContext(), backgroundResourceId);
// 控件自带api,这里不用setBackgroundColor()因为在9.0测试不通过
// setBackgroundDrawable本来过时了,但是兼容包重写了方法
setBackgroundDrawable(drawable);
} else {
// 获取皮肤包资源
Object skinResourceId = SkinManager.getInstance().getBackgroundOrSrc(backgroundResourceId);
// 兼容包转换
if (skinResourceId instanceof Integer) {
int color = (int) skinResourceId;
setBackgroundColor(color);
// setBackgroundResource(color); // 未做兼容测试
} else {
Drawable drawable = (Drawable) skinResourceId;
setBackgroundDrawable(drawable);
}
}
}
// 根据自定义属性,获取styleable中的textColor属性
key = R.styleable.SkinnableTextView[R.styleable.SkinnableTextView_android_textColor];
int textColorResourceId = attrsBean.getViewResource(key);
if (textColorResourceId > 0) {
if (SkinManager.getInstance().isDefaultSkin()) {
ColorStateList color = ContextCompat.getColorStateList(getContext(), textColorResourceId);
setTextColor(color);
} else {
ColorStateList color = SkinManager.getInstance().getColorStateList(textColorResourceId);
setTextColor(color);
}
}
// 根据自定义属性,获取styleable中的字体 custom_typeface 属性
key = R.styleable.SkinnableTextView[R.styleable.SkinnableTextView_custom_typeface];
int textTypefaceResourceId = attrsBean.getViewResource(key);
if (textTypefaceResourceId > 0) {
if (SkinManager.getInstance().isDefaultSkin()) {
setTypeface(Typeface.DEFAULT);
} else {
setTypeface(SkinManager.getInstance().getTypeface(textTypefaceResourceId));
}
}
}
}
那么当需要换肤时,触发换肤的事件,
/**
* 控件回调监听,匹配上则给控件执行换肤方法
*/
protected void applyViews(View view) {
if (view instanceof ViewsMatch) {
ViewsMatch viewsMatch = (ViewsMatch) view;
viewsMatch.skinnableView();
}
if (view instanceof ViewGroup) {
ViewGroup parent = (ViewGroup) view;
int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
applyViews(parent.getChildAt(i));
}
}
}
就可以调起各个自定义控件中重写ViewsMatch 接口的skinnableView方法,从而做换肤操作。
总结
能这么做的原因是
- 系统的源码中判断了mFactory2是否为空,不为空则使用mFactory2的onCreateView方法创建View
- Activity重写了Factory2接口,可以在Activity中拦截View的创建过程。
- 不必想常规的换肤思路,保存所有的换肤View,在换肤的时候在遍历,直接使用系统的DectorView递归找View, 节省了存- 的空间。而且不必在换肤的时候2层嵌套循环。