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设置为相应主题提供的资源文件就可以了,更为详细的介绍会在后续文章中继续…