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); }
网友评论