Android LayoutInflater Factory
LayoutInflater.Factory简介
在之前一篇LayoutInflater的介绍中,提到 View 的 inflate 中有一个方法 createViewFromTag,会首先尝试通过 Factory 来 CreateView。
代码如下:
// these are optional, set by the caller private boolean mFactorySet; private Factory mFactory; private Factory2 mFactory2; private Factory2 mPrivateFactory; //... 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; } //... }
今天我们来跟一下这个Factory到底是干嘛的,以及到底在什么场景使用?
先看一下Factory源码的介绍:
public interface Factory { /** * Hook you can supply that is called when inflating from a LayoutInflater. * You can use this to customize the tag names available in your XML * layout files. * * <p> * Note that it is good practice to prefix these custom names with your * package (i.e., com.coolcompany.apps) to avoid conflicts with system * names. * * @param name Tag name to be inflated. * @param context The context the view is being created in. * @param attrs Inflation attributes as specified in XML file. * * @return View Newly created view. Return null for the default * behavior. */ public View onCreateView(String name, Context context, AttributeSet attrs); }
可以看到只有一个onCreateView的方法,参数分别是XML里边对应的Tag名字,context和attrs属性。
作用就是通过 LayoutInflater 创建View时候的一个回调,可以通过LayoutInflater.Factory来改造 XML 中存在的 Tag。
如果我们设置了LayoutInflater Factory ,在LayoutInflater 的 createViewFromTag 方法中就会通过这个 Factory 的 onCreateView 方法来创建(改变) View。
比如可以把一个TextView通过设置Factory后,强制转变成一个Button:
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { LayoutInflater.from(this).setFactory(new LayoutInflater.Factory() { @Override public View onCreateView(View parent, String name, Context context, AttributeSet attrs) { if(TextUtils.equals(name,"TextView")){ Button button = new Button(MainActivity.this); button.setAllCaps(false); return button; } //本来是TextView,结果create时候成了一个Button return getDelegate().createView(parent, name, context, attrs); } @Override public View onCreateView(String name, Context context, AttributeSet attrs) { return null; } }); super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } }
(这里注意,setFactory必须放在super.onCreate()之前,稍后我们会解释原因)
那还有一个Factory2到底是干嘛的,跟Factory到底有什么区别和联系呢?
还是看一下源码:
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 The parent that the created view will be placed * in; <em>note that this may be null</em>. * @param name Tag name to be inflated. * @param context The context the view is being created in. * @param attrs Inflation attributes as specified in XML file. * * @return View Newly created view. Return null for the default * behavior. */ public View onCreateView(View parent, String name, Context context, AttributeSet attrs); }
可以看到Factory2对比Factory,方法没有变化,只是多了一个参数parent。
说明Factory2可以对创建 View 的 Parent 进行控制。
LayoutInflater里边同时提供了setFactory和setFactory2方法。
然后我们看一下setFactory的方法:
/** * Attach a custom Factory interface for creating views while using * this LayoutInflater. This must not be null, and can only be set once; * after setting, you can not change the factory. This is * called on each element name as the xml is parsed. If the factory returns * a View, that is added to the hierarchy. If it returns null, the next * factory default {@link #onCreateView} method is called. * * <p>If you have an existing * LayoutInflater and want to add your own factory to it, use * {@link #cloneInContext} to clone the existing instance and then you * can use this function (once) on the returned new instance. This will * merge your own factory with whatever factory the original instance is * using. */ public void setFactory(Factory factory) { if (mFactorySet) { throw new IllegalStateException("A factory has already been set on this LayoutInflater"); } if (factory == null) { throw new NullPointerException("Given factory can not be null"); } mFactorySet = true; if (mFactory == null) { mFactory = factory; } else { mFactory = new FactoryMerger(factory, null, mFactory, mFactory2); } } /** * Like {@link #setFactory}, but allows you to set a {@link Factory2} * interface. */ public void setFactory2(Factory2 factory) { if (mFactorySet) { throw new IllegalStateException("A factory has already been set on this LayoutInflater"); } if (factory == null) { throw new NullPointerException("Given factory can not be null"); } mFactorySet = true; if (mFactory == null) { mFactory = mFactory2 = factory; } else { mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2); } }
可以看到如果已经有设置过,mFactorySet如果为true,则直接抛异常IllegalStateException。
是在哪里设置的呢?
我们来找一下,在AppCompatActivity的onCreate里边找到了:
protected void onCreate(@Nullable Bundle savedInstanceState) { AppCompatDelegate delegate = this.getDelegate(); //这里 delegate.installViewFactory(); delegate.onCreate(savedInstanceState); if (delegate.applyDayNight() && this.mThemeId != 0) { if (VERSION.SDK_INT >= 23) { this.onApplyThemeResource(this.getTheme(), this.mThemeId, false); } else { this.setTheme(this.mThemeId); } } super.onCreate(savedInstanceState); }
然后转到AppCompatDelegateImpl里边:
public void installViewFactory() { LayoutInflater layoutInflater = LayoutInflater.from(mContext); if (layoutInflater.getFactory() == null) { 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"); } } }
如果 layoutInflater.getFactory() 为空,则会自动设置一个 Factory2。
这里调用LayoutInflaterCompat这个兼容类的setFactory2设置了Factory2:
public static void setFactory2( @NonNull LayoutInflater inflater, @NonNull LayoutInflater.Factory2 factory) { inflater.setFactory2(factory); if (Build.VERSION.SDK_INT < 21) { final LayoutInflater.Factory f = inflater.getFactory(); if (f instanceof LayoutInflater.Factory2) { // The merged factory is now set to getFactory(), but not getFactory2() (pre-v21). // We will now try and force set the merged factory to mFactory2 forceSetFactory2(inflater, (LayoutInflater.Factory2) f); } else { // Else, we will force set the original wrapped Factory2 forceSetFactory2(inflater, factory); } } }
然后才是真正调用了LayoutInflater里边的setFactory2这个方法,导致标志位mFactorySet = true。
所以,这就解释了为什么需要在super.onCreate()之前调用setFactory的原因,否则直接抛出异常。
再看一下上边这段代码的说明部分,最后会强制转为
为什么要在onCreate里边自动设置一个Factory呢?
其实可以通过分析代码得知,其实Factory的目的就是通过onCreateView()创建View。
所以最终,我们通过Factory中的接口找到了AppCompatDelegateImpl里边的onCreateView:
/** * 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) { 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)) { // Either default class name or set explicitly to null. In both cases // create the base inflater (no reflection) //这里创建了一个AppCompatViewInflater mAppCompatViewInflater = new AppCompatViewInflater(); } else { try { Class viewInflaterClass = Class.forName(viewInflaterClassName); mAppCompatViewInflater = (AppCompatViewInflater) viewInflaterClass.getDeclaredConstructor() .newInstance(); } catch (Throwable t) { Log.i(TAG, "Failed to instantiate custom view inflater " + viewInflaterClassName + ". Falling back to default.", t); mAppCompatViewInflater = new AppCompatViewInflater(); } } } boolean inheritContext = false; if (IS_PRE_LOLLIPOP) { inheritContext = (attrs instanceof XmlPullParser) // If we have a XmlPullParser, we can detect where we are in the layout ? ((XmlPullParser) attrs).getDepth() > 1 // Otherwise we have to use the old heuristic : shouldInheritContext((ViewParent) parent); } //最后调用AppCompatViewInflater的createView 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 */ ); }
继续去看一下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; 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; } @NonNull protected AppCompatTextView createTextView(Context context, AttributeSet attrs) { return new AppCompatTextView(context, attrs); } @NonNull protected AppCompatImageView createImageView(Context context, AttributeSet attrs) { return new AppCompatImageView(context, attrs); } @NonNull protected AppCompatButton createButton(Context context, AttributeSet attrs) { return new AppCompatButton(context, attrs); } @NonNull protected AppCompatEditText createEditText(Context context, AttributeSet attrs) { return new AppCompatEditText(context, attrs); } @NonNull protected AppCompatSpinner createSpinner(Context context, AttributeSet attrs) { return new AppCompatSpinner(context, attrs); } @NonNull protected AppCompatImageButton createImageButton(Context context, AttributeSet attrs) { return new AppCompatImageButton(context, attrs); } @NonNull protected AppCompatCheckBox createCheckBox(Context context, AttributeSet attrs) { return new AppCompatCheckBox(context, attrs); } @NonNull protected AppCompatRadioButton createRadioButton(Context context, AttributeSet attrs) { return new AppCompatRadioButton(context, attrs); } @NonNull protected AppCompatCheckedTextView createCheckedTextView(Context context, AttributeSet attrs) { return new AppCompatCheckedTextView(context, attrs); }
到这里,一目了然:
设置Factory的目的就是把我们定义在xml里的View(TextView、ImageView等等),通过这个Factory工厂,转化成AppCompat前缀的View。
就是为了兼容。
总结:
LayoutInflater.Factory的意义:通过 LayoutInflater 创建 View 时候的一个回调,可以通过 LayoutInflater.Factory 来改造或定制创建 View 的过程。
LayoutInflater.setFactory 使用注意:不能在 super.onCreate 之后设置。
AppCompatActivity 为什么 setFactory ?向下兼容新版本中的View。