转载文章请注明出处:道龙的博客
我们不管是在ListView、RecyclerView、甚至自定义布局的时候,都会通过View.inflate(......);方法加载布局,其实这是偷懒的方式,有些时候,通过这种偷懒的方式反而带来意想不到的bug。比如空指针异常,非法状态异常。接下来就通过源码角度,分析为何不建议使用这种方式。
伪代码示例:
public class MyStaggedRecyclerAdapter extends RecyclerView.Adapter{ private List list; private List heights; public MyStaggedRecyclerAdapter(List list) { // TODO Auto-generated constructor stub this.list = list; heights = new ArrayList (); for (int i = 0; i < list.size(); i++) { heights.add((int) (200 + Math.random() * 50)); } } class MyViewHolder extends RecyclerView.ViewHolder { TextView tv; public MyViewHolder(View view) { super(view); tv = (TextView) view.findViewById(android.R.id.text1); } } @Override public int getItemCount() { // TODO Auto-generated method stub return list.size(); } @Override public void onBindViewHolder(MyViewHolder holder, int position) { //绑定数据 LayoutParams params = holder.tv.getLayoutParams(); params.height = heights.get(position); holder.tv.setBackgroundColor(Color.rgb(100, (int) (Math.random() * 255), (int) (Math.random() * 255))); holder.tv.setLayoutParams(params); holder.tv.setText(list.get(position)); } @Override public MyViewHolder onCreateViewHolder(ViewGroup viewGroup, int arg1) { // 创建ViewHolder MyViewHolder holder = new MyViewHolder(View.inflate(viewGroup.getContext(), android.R.layout.simle_list_item_1, null)); //MyViewHolder holder = new MyViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(android.R.layout.simple_list_item_1, viewGroup, false)); return holder; } }
这里的代码非常简单,是为了给RecycleView设置流式布局的适配器类。流式布局需要给孩子设置宽高,这里通过动态随机给item孩子设置高度的方式,这样展示流式布局显得更加高大上一点~ 首先,在onCreateViewHolder方法中,先通过以往偷懒的方式加载item孩子布局的。运行程序,我们会发现报错:
NullPointException
经过log日志,或者debug,锁定到LayoutParams为空,接下来设置宽高,也就没法执行了。看看为空情况,眼见为实:
这里就很头疼了,我按照标准写法写的,为什么为空呢?这里,其实存在着不小的坑。对于这个坑,需要通过源码角度理解。进入源码:
public static View inflate(Context context, @LayoutRes int resource, ViewGroup root) { LayoutInflater factory = LayoutInflater.from(context); return factory.inflate(resource, root); }
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) { return inflate(resource, root, root != null); }我们发现,通过调用View.inflate本质上调用的是LayoutInflater的inflate(resource, root, root != null);方法。由于传入的根布局root为nulll,因此这里本质上为inflate(resource, null, false);。继续跟进源码
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) { final Resources res = getContext().getResources(); if (DEBUG) { Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" (" + Integer.toHexString(resource) + ")"); } final XmlResourceParser parser = res.getLayout(resource); try { return inflate(parser, root, attachToRoot); } finally { parser.close(); } }这里就有意思了,inflate三个参数含义前两个很明显,第三个看参数名称也很明显,含义是:是否绑定传入的RootView?我们本质上传入的是false,所以这个值以后都为fallse了。方法里面是通过xml序列化器,对自定义传入的res布局文件预进行解析。具体的解析,继续跟进源码:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;
try {
// Look for the root node.
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
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(" 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;
}
}
} catch (XmlPullParserException e) {
final InflateException ie = new InflateException(e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(parser.getPositionDescription()
+ ": " + e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} finally {
// Don't retain static reference on context.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
return result;
}
}
在这个方法里面,就是对传入的item布局进行全面的解析了。看一下红色位置标注处,params = root.generateLayoutParams(attrs);哈哈,终于把你揪出来来了。我们传入的root为null,更不可能去生成该item的params了,因此,刚开始获取params为null很明显了了。
同样的我们加载的布局他压根就没有父亲,这样他的layout_width和layout_height等属性都需要有父亲才有效。没有params为null也就没有什么可以疑问的了。
我们先不直接使用LayoutInflater.from,我们想到,既然root为空,那么我传入一个root让其不是空,不就解决了这个bug吗?做如下修改:
MyViewHolder holder = new MyViewHolder(View.inflate(viewGroup.getContext(), android.R.layout.simple_list_item_1, viewGroup));
还是报错:
java.lang.IllegalStateException:
The specified child already has a parent. You must call removeView() on the child's parent first.
错误情况也很明朗,因为,我们指定的孩子view已经有了父容器了(RecyclerView会默认成为父亲),我们传入的root不能直接成为父亲。你必须先让原来孩子的父亲把孩子去掉,才能再找一个父亲。这尼玛真实一言不合就换父亲啊~~
我们还是要看看为何报这个错误:
首先:由于我们传入了root,也就不为空,最初默认调用还是默认还是调用的:
inflate(resource, root, root != null);
inflate(resource, root, true);
接着定位最终调用源码绿色位置:
看源码就知道:多做了一个事情就是
if (root != null && attachToRoot) {
root.addView(temp, params);
}
由于RecyclerView/ListView会自动将child添加到它里面去成为父亲,并最终一起添加到viewGroup(成为爷爷)。可以找出错误的原因有两个:1、给布局添加父亲,只能存在一个父亲。2、(这个解释有点牵强但是可以增加理解)孩子自己就想添加到viewGroup,想直接把爷爷当做父亲,这伦理上也说不过去啊,难免自作多情!这个时候,就会报非法异常了。
那么最后看解决办法:
解决办法1,还是使用View.inflate():
MyViewHolder holder = new MyViewHolder(View.inflate(viewGroup.getContext(), R.layout.listview_item, null));
布局使用自己定义的布局:
xml version="1.0" encoding="utf-8"?>这样的,我们给TextView制定了父布局,那么通过最初的方式就能完成任务。xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> android:gravity="center" android:textSize="20sp" android:id="@+id/tv" android:layout_width="match_parent" android:layout_height="match_parent"/>
解决方式2:
通过建议的方式,使用
MyViewHolder holder = new MyViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.listview_item, viewGroup, false));
使用这种方式,无论加载什么布局,都能完成View的加载,当然,这也是本文所说的结论,如果想要加载布局,不想带来可能存在的“危机”,就使用这种方式加载布局吧!
其他解决方式:
可以通过ViewTreeObserver.addGlob..Listener()方法,设置页面布局绘制完毕监听器,在里面进行获取组件的宽高一定不会报错。至于使用方式,自定百度~
对于这一块的更具体、详细的源码,等笔者能力提高了,再进入两万行的代码里带大家遨游一番吧~
觉得有作用,就点个赞价加个关注呗~~