LayoutInflater在开发中使用的场合非常多,常见的如在Adapter中getView()方法中,或者在Fragment中onCreateView()中等等。开发中使用的多数场景都是通过使用LayoutInflater的inflate()方法来生成一个View的实例,但是LayoutInflater.Factory开发中还是比较少见的,除非是开发多主题模式的应用,如包含了日间模式和夜间模式的应用。
在介绍LayoutInflater.Factory之前先看一下它的一个简单示例,在布局文件中只设置一个TextView,内容如下:
<TextViewxmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="48dp" android:background="#f00" android:gravity="center" android:text="@string/hello_world"/>
在Activity的onCreate方法中我们重新设置一下Factory,代码如下:
protected void onCreate(BundlesavedInstanceState) { LayoutInflaterCompat.setFactory(LayoutInflater.from(this), new LayoutInflaterFactory() { @Override public ViewonCreateView(Viewparent, String name, Contextcontext, AttributeSetattrs) { Buttonbtn=null; switch (name) { case "TextView": int count = attrs.getAttributeCount(); for (int i = 0; i < count; i++) { Log.d("MainActivity",attrs.getAttributeName(i) + ":" + attrs.getAttributeValue(i)); } btn=new Button(context); btn.setText("this is button"); break; default: break; } return btn; } }); super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); }
Log输出的日志如下:
gravity:0x11 background:#ffff0000 layout_width:-1 layout_height:48.0dip text:@2131361814
日志信息将布局中间中TextView的所有属性名称和属性值都打印出来了,在该示例中我们将TextView替换成了一个Button,更不用说在该方法中替换部分属性了,所以如果想要更改主题资源属性,关键就在该方法中,下面我们通过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 ViewonCreateView(String name, Contextcontext, AttributeSetattrs); } 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 ViewonCreateView(Viewparent, String name, Contextcontext, AttributeSetattrs); }
主要看一下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.
当我们使用LayoutInflater渲染View的时候便会回调该Hook方法,Hook可能不太好理解,实际上就是callback方法。我们可以在xml布局文件中使用自定义标签,需要注意的是不要使用系统命名空间。
Factory是一个接口,Factory2继承自Factory,是对Factory的扩展补充。Factory2是从API11开始引入的,一般在应用开发的时候不必考虑这些差异,可以直接使用support包中的LayoutInflaterCompat设置LayoutInflaterFactory。
我们通过布局文件来构建View的时候都会使用LayoutInflater的inflate方法,上面我们通过Activity中的onCreate方法中使用setContentView方法设置布局,该方法是调用的父类Activity的方法。
public void setContentView(@LayoutRes int layoutResID) { getWindow().setContentView(layoutResID); initWindowDecorActionBar(); }
getWindow()方法获取的是Window实例,然后我们进入Window方法看到setContentView()方法是一个抽象方法,PhoneWindow作为Window的唯一子类肯定实现了该方法,在PhoneWidnow中setContentView方法实现如下:
@Override public void setContentView(int layoutResID) { if (mContentParent == null) { installDecor(); } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) { mContentParent.removeAllViews(); } mLayoutInflater.inflate(layoutResID, mContentParent); final Callbackcb = getCallback(); if (cb != null && !isDestroyed()) { cb.onContentChanged(); } //... }
直接进入LayoutInflater的inflate方法内:
public Viewinflate(@LayoutRes int resource, @Nullable ViewGrouproot) { return inflate(resource, root, root != null); }
inflate调用了一个重载方法,然后又返回一个重载方法。
public Viewinflate(@LayoutRes int resource, @Nullable ViewGrouproot, boolean attachToRoot) { final Resourcesres = getContext().getResources(); if (DEBUG) { Log.d(TAG, "INFLATING from resource: /"" + res.getResourceName(resource) + "/" (" + Integer.toHexString(resource) + ")"); } final XmlResourceParserparser = res.getLayout(resource); try { return inflate(parser, root, attachToRoot); } finally { parser.close(); } }
返回重载方法可以简写如下:
public Viewinflate(XmlPullParserparser, @Nullable ViewGrouproot, boolean attachToRoot) { Viewresult = root; // Temp is the root view that was found in the xml //创建View final Viewtemp = createViewFromTag(root, name, inflaterContext, attrs); ViewGroup.LayoutParamsparams = null; if (root != null) { // Create layout params that match root, if supplied params = root.generateLayoutParams(attrs); if (!attachToRoot) { // Set the layout params for temp if we are not // attaching. (If we are, we use addView, below) temp.setLayoutParams(params); } } if (root == null || !attachToRoot) { result = temp; } return result; }
在inflate方法中先通过createViewFromTag方法创建一个临时View,然后将创建成功的LayoutParams设置给临时View,createViewFromTag的执行流程如下:
ViewcreateViewFromTag(Viewparent, String name, Contextcontext, AttributeSetattrs, boolean ignoreThemeAttr) { //... Viewview; 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; }
当我们使用LayoutInflater进行对XML文件解析渲染View的时候最终会进入createViewFromTag()方法创建View,如果设置了Factory2,则会回调Factory2的回调方法onCreateView()进行创建View,如果未设置Factory2则判断是否设置了Factory,执行Factory创建View的回调方法。如果未设置Factory2和Factory,则执行默认的View创建流程。
通过上面就可以知道文章开始处的示例执行机制了,因为我们通过LayoutInflaterCompat设置了Factory,所以返回的View是通过Factory回调方法onCreateVeiw()创建的View,但是我们通过View的名称进行了判断,如果是TextView就创建一个Button,所以呈现出来的是一个Button。
LayoutInflater设置Factory只可以设置一次,因为LayoutInflater有一个 私有属性mFactorySet ,一旦设置过之后mFactorySet的值就会置true,在setFactory中会对mFactorySet的值进行判断,一旦重复设置就会抛出异常 A factory has already been set on this LayoutInflater 。
public void setFactory(Factoryfactory) { 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); } }
在support包AppCompatActivity类中就默认设置了Factory,有关AppCompatActivity类Factory更多内容可以参看 Android tint着色器初探 ,因此如果继承了AppCompatActivity在使用Factory的时候必须谨慎,因为不同版本的support包对Factory的处理逻辑还是有细微差别的,下面就是support v22和support v23中实现的代码。
//support v22 public void installViewFactory() { LayoutInflaterlayoutInflater = LayoutInflater.from(mContext); if (layoutInflater.getFactory() == null) { LayoutInflaterCompat.setFactory(layoutInflater, this); } else {//未进行判断直接抛出异常 Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed" + " so we can not install AppCompat's"); } } //support v23 public void installViewFactory() { LayoutInflaterlayoutInflater = LayoutInflater.from(mContext); if (layoutInflater.getFactory() == null) { LayoutInflaterCompat.setFactory(layoutInflater, this); } else {//此处进行了从属类型判断 if (!(LayoutInflaterCompat.getFactory(layoutInflater) instanceof AppCompatDelegateImplV7)) { Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed" + " so we can not install AppCompat's"); } } }
因此如果我们导入的是support v23包,在使用LayoutInflaterCompat设置Factory的时候是不会抛出异常的,因为设置Factory的类就是AppCompatDelegateImplV7,但是在support v22中就不同了,它会直接抛出异常。但是上面已经说了 mFactorySet是私有属性 ,如果我们又必须再次使用Factory该怎么办呢?只能使用反射将该属性重新设置为false,代码如下:
Fieldfield = LayoutInflater.class.getDeclaredField("mFactorySet"); field.setAccessible(true); field.setBoolean(getLayoutInflater(), false);
必须使生成LayoutInflater实例的Context是同一个,一般情况下获取LayoutInflater有如下三种方式:
实际上前面两种方式最总也是使用context.getSystemService获取的,因此只要Context相同通过getSystemService获取的LayoutInflater实例也是相同的,否则在最后执行createViewFromTag()方法的时候Factory回调函数不会被调用,因为不同的Context生成的LayoutInflater实例不同。
本篇介绍的LayoutInflater.Factory就是为后面设置多主题模式做铺垫的,我们知道在AppCompatActivity中就是通过Factory然后将系统的View全部替换为了support兼容包中的AppCompatView,因此我们也可以使用Factory来实现主题的切换逻辑,只需要在不影响系统创建View的情况下,将需要切换主题的View设置为相应主题提供的资源文件就可以了,更为详细的介绍会在后续文章中继续…