LayoutInflater
LayoutInflatre能将一个xml文件解析成对应的View并构建对应的View的关系结构。使用这个类在需要的时候才解析一个布局文件,来避免一开始就加载xml布局文件造成资源浪费。
1. 使用方法
LayoutInflater inflater = LayoutInflater.from(this);
View inflatedLayout = inflater.inflate(R.layout.inflate_layout, mParent, false);
首先通过静态方法传入一个上下文对象获取一个LayoutInflater实例,然后调用inflate方法来解析这个布局文件。这个方法传入三个参数:
-
resource:要解析的资源文件 -
root:解析出来的View的要添加到哪个容器中,相当于这个资源文件的上一层所表示的View -
attachToRoot:是否将解析出来的View作为子View添加到上一个参数传入的View容器中
2. 源码分析
2.1
View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
if (view != null) {
return view;
}
XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
先判断是否已经解析过该文件,如果已经解析过直接返回解析出来的View,否则就将资源文件准换成一个XmlResourceParser对象,然后调用inflate方法;
2.2
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;
try {
advanceToRootNode(parser);
final String name = parser.getName();
if (DEBUG) {
System.out.println("**************************");
System.out.println("Creating root view: "
+ name);
System.out.println("**************************");
}
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// 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 (DEBUG) {
System.out.println("-----> start inflating children");
}
// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);
if (DEBUG) {
System.out.println("-----> done inflating children");
}
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == null || !attachToRoot) {
result = temp;
}
}
这个方法用于对不同的标签类型来执行不同的解析策略,最终把返回一个解析出来的View.在解析过程中主要由以下几个步骤:
把解析出来的
View添加到父View中,并设置LayoutParam解析完成后调用
onFinishInflate方法,通知已经解析完成返回最终解析出来的最顶层的父
View,如果inflate参数传入的root不为空且attachToRoot为true,返回的View就是传入的root,否则就是解析出来的顶层的view-
如果
inflate方法传入的root不为空attachToRoot为true或不传入,就会把解析出来的View作为root下的一个子View.总结
从上面的流程中看出在
inflate阶段就会创建布局文件中的所有View实例,并按照层级关系组织好View之间的布局关系。最后返回一个这个布局文件的根View,通过这个View就可以遍历所有的View。3. findViewById
从上面的分析中可以看出在
inflate阶段就会实例化布局文件中的所有View, 那通过findViewById是怎么拿到对应的View的。public final <T extends View> T findViewById(@IdRes int id) { if (id == NO_ID) { return null; } return findViewTraversal(id); }这个方法是在
View.java这个类中定义的final方法,这个方法会调用另外一个findViewTraversal方法protected <T extends View> T findViewTraversal(@IdRes int id) { if (id == mID) { return (T) this; } return null; }这个方法是一个可以被重写的方法,在
ViewGourp中重写了这个方法,在调用的时候如果当前的View不是一个ViewGroup,就会执行上面的逻辑,判断传入的id是不是等于自己的id,如果是就返回本View否则就返回null;如果是一个
ViewGroup就执行下面的逻辑/** * {@hide} */ @Override protected <T extends View> T findViewTraversal(@IdRes int id) { if (id == mID) { return (T) this; } final View[] where = mChildren; final int len = mChildrenCount; for (int i = 0; i < len; i++) { View v = where[i]; if ((v.mPrivateFlags & PFLAG_IS_ROOT_NAMESPACE) == 0) { v = v.findViewById(id); if (v != null) { return (T) v; } } } return null; }在
ViewGroup中检查Id是否和自己相等,如果不等会遍历自己的所有子View/ViewGroup,然后依次调用他们的findViewById方法,如果子View是一个View就会判断id是否相等,如果相等就返回这个View,如果是ViewGroup就递归的调用这个方法来找子View中是否存在相同id的View,如果遍历完后也没有就说明这个根节点的View下是没有这个id的View。从这里可以看出通过findViewById这个方法查找某个View,这个View必须要是自己的子View,否则就会找不到,并不是在任意的View上调用findViewById方法来查找任意的View都能找到。3. 与inflate相关的几个问题
3.1
ResyclerView、ListView中的子项设置宽高属性失效在使用
inflate来加载ItemView子项的布局文件时,如果在inlfate参数中参数的root为null,那布局文件中设置的宽高可能就会失效。因为当传入的root参数为null时,就不会给这个布局文件的中的rootView设置layoutParams。代码:
// Temp is the root view that was found in the xml final View temp = createViewFromTag(root, name, inflaterContext, attrs); ViewGroup.LayoutParams params = null; if (root != null) { if (DEBUG) { System.out.println("Creating params from root: " + root); } // 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); } }看到这有当
rootView不为null,并且attachToRoot为false时才会设置布局文件的layoutParam。否则layoutParam就位null。inflate完成后当RecyclerView创建ViewHolder时发现如果当前View的LayoutParam为空时就是设置一个默认的LayoutParam。代码:
//从ViewAdapter中创建一个ViewHolder holder = mAdapter.createViewHolder(RecyclerView.this, type); //设置LayoutParam final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams(); final LayoutParams rvLayoutParams; if (lp == null) { rvLayoutParams = (LayoutParams) generateDefaultLayoutParams(); holder.itemView.setLayoutParams(rvLayoutParams); } else if (!checkLayoutParams(lp)) { rvLayoutParams = (LayoutParams) generateLayoutParams(lp); holder.itemView.setLayoutParams(rvLayoutParams); } else { rvLayoutParams = (LayoutParams) lp; }获取创建的
ViewHolder的LayoutParam,如果为空就生成一个默认的LayoutManager,实现的生成的方法在LayoutManager中定义,在LinearLayoutManager的实现为@Override public RecyclerView.LayoutParams generateDefaultLayoutParams() { if (mOrientation == HORIZONTAL) { return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT); } else { return new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); } }可以看到
LineaLayoutManager中会生成wrap_content的参数值,也就是所有的ViewHolder的布局宽高都是wrap_content的。3.2
RecyclerView inflate第三个参数传true报错报错的信息:
throw new IllegalStateException("ViewHolder views must not be attached when" + " created. Ensure that you are not passing 'true' to the attachToRoot" + " parameter of LayoutInflater.inflate(..., boolean attachToRoot)");从
inflate源码中可以看出,但传入的不为空且attachToRoot为true时,inflate完成返回的为传入的 Root ,holder = mAdapter.createViewHolder(RecyclerView.this, type)在创建一个ViewHodler时传入的root就是当前的ResyclerView对象,所以,如果inflate时传入true,那么返回的还是RecyclerView对象,这样在LayoutManager在调用下面的方法获取一个View时拿到还是当前的RecyclerView。----疑问:拿到RecyclerView也是可以获取到里面的ViewHolder的,为什么不可以呢。是不是无法确定哪个ViewHolder是新增的
@NonNull public View getViewForPosition(int position) { return getViewForPosition(position, false); }