换肤功能说得直白点就是改变View的样式,例如textColor,backgroundColor等。我们知道View的加载都是通过LayoutInflater类来实现的,如果我们能在View被创建前拿到View的相关信息(名称,布局属性等),那么我们就能自己去改变View被创建的逻辑,更改View相关的属性值来实现换肤功能。
Activity的onCreate方法有两个一定会用到的方法:
super.onCreate(savedInstanceState);
setContentView(getContentView());
View的创建也一定会在这两个方法中完成,所以这是一个很明显的切入点,下面我们来着重分析这两个方法
注:源码分析基于android 9.0,其它版本大同小异
在Activity的onCreate方法中我们可以看到调用了deletgate.installViewFactory();
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
delegate.installViewFactory();
delegate.onCreate(savedInstanceState);
super.onCreate(savedInstanceState);
}
其中installViewFactory()是一个很重要的方法,该方法存在于AppCompatDelegateImpl类中,系统通过调用LayoutInflaterCompat.setFactory2()为LayoutInflater中的mFactory2属性赋值,下面我们来看一下具体实现。
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
//这里的第二个参数就是Factory2,之所以传递参数为this是因为AppCompatDelegateImpl类本身实现了Factory2接口
LayoutInflaterCompat.setFactory2(layoutInflater, this);
} else {
if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
+ " so we can not install AppCompat's");
}
}
}
那么Factory2又是什么呢?
Factory2是LayoutInflater中的一个内部接口,用于创建view
public interface Factory2 extends Factory {
/**
* Version of {@link #onCreateView(String, Context, AttributeSet)}
* that also supplies the parent that the view created view will be
* placed in.
*
* @param parent 被创建View的父布局
* @param View的名称,例如TextView,Buttong等
* @param context
* @param View的布局属性
*
* @return 返回被创建的view,如果返回值为null则走其它创建View的逻辑
*/
public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
}
这里有一点很重要,那就是通过Factory2创建的View有可能为null,这时候回去走其它创建View的逻辑,因为该接口是public修饰的,开发人员可以进行自由实现,返回值不可控。
setContentView的具体实现依然在AppCompatDelegateImpl类,来看一下具体的代码:
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mAppCompatWindowCallback.getWrapped().onContentChanged();
}
我们需要关注的就是inflate方法,这一看就是加载布局的关键实现,点开inflate方法并一步一步跟踪,具体流程如下
重点关注第4个方法,View的创建逻辑也在这里得到了实现。(以上4个方法全部在LayoutInflater中)
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
........................ 省略部分代码
try {
View view;
if (mFactory2 != null) {
//由于Activity在调用onCreate的时候就为mFactory2赋值了,所以肯定会走到这里
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 (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
} catch (InflateException e) {
throw e;
} catch (ClassNotFoundException e) {
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
}
}
由于系统在onCreate之前就为mFactory赋值了,所以一定会调用mFactory2.onCreateView(parent, name, context, attrs);
那么mFactory2.onCreateView的具体实现在哪里呢?其实上面的代码已经给出了答案:
在AppCompatDelegateImpl -> installViewFactory()方法中有这样一句代码
LayoutInflaterCompat.setFactory2(layoutInflater, this);
第二个参数类型是Factory2,而this指代了AppCompatDelegateImpl,所以mFactory2.onCreateView的具体实现一定在AppCompatDelegateImpl。下面来看一下具体的代码:
/**
* From {@link LayoutInflater.Factory2}.
*/
@Override
public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
return createView(parent, name, context, attrs);
}
public View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
//创建一个AppCompatViewInflater对象
if (mAppCompatViewInflater == null) {
TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
String viewInflaterClassName =
a.getString(R.styleable.AppCompatTheme_viewInflaterClass);
if ((viewInflaterClassName == null)
|| AppCompatViewInflater.class.getName().equals(viewInflaterClassName)) {
mAppCompatViewInflater = new AppCompatViewInflater();
} else {
Class> viewInflaterClass = Class.forName(viewInflaterClassName);
mAppCompatViewInflater =
(AppCompatViewInflater) viewInflaterClass.getDeclaredConstructor()
.newInstance();
}
}
............... 省略部分代码
//通过调用AppCompatViewInflater的createView来创建View
return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
IS_PRE_LOLLIPOP, /* Only read android:theme pre-L (L+ handles this anyway) */
true, /* Read read app:theme as a fallback at all times for legacy reasons */
VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
);
}
好家伙,追踪了半天代码,这createView的重担由交给了AppCompatViewInflater,那么再继续追下去吧:
AppCompatViewInflater -> createView()
final View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs, boolean inheritContext,
boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
final Context originalContext = context;
// We can emulate Lollipop's android:theme attribute propagating down the view hierarchy
// by using the parent's context
if (inheritContext && parent != null) {
context = parent.getContext();
}
if (readAndroidTheme || readAppTheme) {
// We then apply the theme on the context, if specified
context = themifyContext(context, attrs, readAndroidTheme, readAppTheme);
}
if (wrapContext) {
context = TintContextWrapper.wrap(context);
}
View view = null;
// We need to 'inject' our tint aware Views in place of the standard framework versions
switch (name) {
case "TextView":
view = createTextView(context, attrs);
verifyNotNull(view, name);
break;
case "ImageView":
view = createImageView(context, attrs);
verifyNotNull(view, name);
break;
case "Button":
view = createButton(context, attrs);
verifyNotNull(view, name);
break;
case "EditText":
view = createEditText(context, attrs);
verifyNotNull(view, name);
break;
case "Spinner":
view = createSpinner(context, attrs);
verifyNotNull(view, name);
break;
case "ImageButton":
view = createImageButton(context, attrs);
verifyNotNull(view, name);
break;
case "CheckBox":
view = createCheckBox(context, attrs);
verifyNotNull(view, name);
break;
case "RadioButton":
view = createRadioButton(context, attrs);
verifyNotNull(view, name);
break;
case "CheckedTextView":
view = createCheckedTextView(context, attrs);
verifyNotNull(view, name);
break;
case "AutoCompleteTextView":
view = createAutoCompleteTextView(context, attrs);
verifyNotNull(view, name);
break;
case "MultiAutoCompleteTextView":
view = createMultiAutoCompleteTextView(context, attrs);
verifyNotNull(view, name);
break;
case "RatingBar":
view = createRatingBar(context, attrs);
verifyNotNull(view, name);
break;
case "SeekBar":
view = createSeekBar(context, attrs);
verifyNotNull(view, name);
break;
case "ToggleButton":
view = createToggleButton(context, attrs);
verifyNotNull(view, name);
break;
default:
// The fallback that allows extending class to take over view inflation
// for other tags. Note that we don't check that the result is not-null.
// That allows the custom inflater path to fall back on the default one
// later in this method.
view = createView(context, name, attrs);
}
if (view == null && originalContext != context) {
// If the original context does not equal our themed context, then we need to manually
// inflate it using the name so that android:theme takes effect.
view = createViewFromTag(context, name, attrs);
}
if (view != null) {
// If we have created a view, check its android:onClick
checkOnClickListener(view, attrs);
}
return view;
}
这次真的没跑了,View的创建最具体的实现就是在这个方法,我们也不需要再追下去了。
通过前面两步我们知道了系统如何通过Factory2来实现View的创建,那么Factory2如何才能为我所用,让创建View的逻辑都由我接管,达到View创建由我不由系统的目的?
3.1 因为Factory2的赋值操作是由系统完成的,并最终由AppCompatDelegateImpl来完成Factory2的具体实现,具体的代码再贴一次
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
//这里的第二个参数就是Factory2,之所以传递参数为this是因为AppCompatDelegateImpl类本身实现了Factory2接口
LayoutInflaterCompat.setFactory2(layoutInflater, this);
} else {
if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
+ " so we can not install AppCompat's");
}
}
}
给mFactory2赋值的时候有一个很重要的先决条件,那就是if (layoutInflater.getFactory() == null),如果我们提前给mFactory2赋值,并且在Activity中实现Factory2接口,那么创建View的逻辑自然而然地被我们接管了
仿佛android系统知道我们要这么干,所以一早就为我们实现了Factory2接口
3.2 在onCreate(@Nullable Bundle savedInstanceState)方法调用super.onCreate(savedInstanceState);之前我们先给mFactory2赋值
@Override
protected void onCreate(Bundle savedInstanceState) {
LayoutInflater mInflater = LayoutInflater.from(this);
// Activity也实现了LayoutInflater.Factory2接口
LayoutInflaterCompat.setFactory2(mInflater, this);
super.onCreate(savedInstanceState);
setContentView(getContentView());
}
3.3 在Activity里面重写Factory2的onCreateView(View parent, String name, Context context, AttributeSet attrs)方法,实现自己创建View的逻辑,下面给出伪代码
@Nullable
@Override
public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
if (需要接管View的创建过程) {
//TODO 实现自己的创建逻辑,返回值可以为null,这样就会走非Factory2的View创建逻辑
View view = null;
switch (name) {
case "TextView":
view = 自定义TextView;
break;
case "Button":
view = 自定义Button;
break;
....
}
return view;
}
return super.onCreateView(parent, name, context, attrs);
}
由于具体的实现逻辑比较简单,这里就不展示代码了。大家可以根据自己项目的需求进行扩展!
至此内置换肤的逻辑就分析完成了,欢迎大家指出不足!