前言
AppCompatActivity中定义的控件可以很容易的支持各种新的特性,但是在源代码中使用的依然是普通的View对象,这里其实是AppCompatActivity在生成控件的过程中做了替换操作将普通的View替换成了CompatView类型。这种技术在很多换肤功能实现中经常会使用到,这里就通过代码阅读来了解具体的实现过程。
代码分析
AppCompatActivity继承自FragmentActivity类,FragmentActivity中间又继承了很多其他的类,最终继承自Activity类,而Activity类其实实现了LayoutInflater.Factory2接口。Activity实现了LayoutInflater.Factory2说明只要Activity或它的某个子类也可以作为LayoutInflater的工厂对象。
public class AppCompatActivity extends FragmentActivity implements AppCompatCallback,
TaskStackBuilder.SupportParentable, ActionBarDrawerToggle.DelegateProvider {
}
public class Activity extends ContextThemeWrapper
implements LayoutInflater.Factory2,
Window.Callback, KeyEvent.Callback,
OnCreateContextMenuListener, ComponentCallbacks2,
Window.OnWindowDismissedCallback, WindowControllerCallback,
AutofillManager.AutofillClient {
}
接着查看AppCompatActivity的onCreate方法会发现其内部的多个方法都是用AppCompatDelegate对象来实现,重点是delegate.installViewFactory();方法,它就是向LayoutInflater注册了View创建工厂对象。
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
delegate.installViewFactory();
delegate.onCreate(savedInstanceState);
if (delegate.applyDayNight() && mThemeId != 0) {
if (Build.VERSION.SDK_INT >= 23) {
onApplyThemeResource(getTheme(), mThemeId, false);
} else {
setTheme(mThemeId);
}
}
super.onCreate(savedInstanceState);
}
查看getDelegate()方法它会根据当前系统的版本生成某种适配代理对象,可以看到目前主要适配的就是AppCompatDelegateImplV14、AppCompatDelegateImplV23和AppCompatDelegateImplN。
public static AppCompatDelegate create(Activity activity, AppCompatCallback callback) {
return create(activity, activity.getWindow(), callback);
}
private static AppCompatDelegate create(Context context, Window window,
AppCompatCallback callback) {
if (Build.VERSION.SDK_INT >= 24) {
return new AppCompatDelegateImplN(context, window, callback);
} else if (Build.VERSION.SDK_INT >= 23) {
return new AppCompatDelegateImplV23(context, window, callback);
} else {
return new AppCompatDelegateImplV14(context, window, callback);
}
}
查看AppCompatDelegateImplV14的实现源码会发现它继承自AppCompatDelegateImplV9,而AppCompatDelegateImplV9也同时实现了LayoutInflater.Factory2接口,后面版本的兼容代理对象也都是继承自前一个版本的代理对象。总之这些代理类都继承自AppCompatDelegateImplV9类,它们都能够作为LayoutInflater的View创建工厂,我们只需要查看AppCompatDelegateImplV9类的实现就可以了。
class AppCompatDelegateImplV9 extends AppCompatDelegateImplBase
implements MenuBuilder.Callback, LayoutInflater.Factory2 {
}
class AppCompatDelegateImplV14 extends AppCompatDelegateImplV9 {
}
class AppCompatDelegateImplV23 extends AppCompatDelegateImplV14 {
}
class AppCompatDelegateImplN extends AppCompatDelegateImplV23 {
}
查看AppCompatDelegateImplV9的installViewFactory();方法它将自己作为LayoutInflater的View创建工厂,LayoutInflater在从XML文件中解析布局文件时会首先调用factory执行创建View,如果创建的View为空就说明工厂无法创建,这时才会使用默认的构造函数创建View。
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory2(layoutInflater, this);
} else {
if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImplV9)) {
Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
+ " so we can not install AppCompat's");
}
}
}
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
.....
try {
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 (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 (Exception e) {
throw e;
}
}
系统中的LayoutInflater对象实际是注册的单例服务对象,只要向它注册了View创建工厂就能够拦截View的创建过程,如果需要做某种全局替换或者界面样式统一修改都可以使用这个功能。查看View创建工厂的两个接口方法实现,会发现最终调用的第一个onCreateView方法,在它内部会首先判断mOriginalWindowCallback能否成功创建View,还记的前面提到的Activity也实现了LayoutInflater.Factory2接口,查看源码发现AppCompatActivity这一系的Activity都无法创建View,因而会直接执行最后的createView方法逻辑。
@Override
public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
// First let the Activity's Factory try and inflate the view
final View view = callActivityOnCreateView(parent, name, context, attrs);
if (view != null) {
return view;
}
// If the Factory didn't handle it, let our createView() method try
return createView(parent, name, context, attrs);
}
/**
* From {@link LayoutInflater.Factory2}.
*/
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return onCreateView(null, name, context, attrs);
}
View callActivityOnCreateView(View parent, String name, Context context, AttributeSet attrs) {
// Let the Activity's LayoutInflater.Factory try and handle it
if (mOriginalWindowCallback instanceof LayoutInflater.Factory) {
final View result = ((LayoutInflater.Factory) mOriginalWindowCallback)
.onCreateView(name, context, attrs);
if (result != null) {
return result;
}
}
return null;
}
在createView方法中会利用AppCompatViewInflater来创建所有的View对象,替换普通的View为AppCompatView的实现逻辑就是在AppCompatViewInflater的createView方法中。通过到目前的分析我们知道设置的View工厂其实就是调用了AppCompatDelegate的createView方法解析创建XML布局文件里的View对象。
@Override
public View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
// 根据条件创建mAppCompatViewInflater
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方法的内部会根据XML标签名称做替换操作将TextView对象替换成AppCompatTextView这样就能支持高版本的某些特性。
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;
}
.....
return view;
}
总结
AppCompatActivity中会为全局单例的LayoutInflater对象安装View工厂对象,这个View工厂会调用AppCompatViewInflater对象的createView方法解析XML布局文件中的View对象,实际生成的View会被替换成AppCompat类型的View,这样不需要修改原始的XML布局文件就能够支持高版本的特性。