在上一篇博客中,我们从源码的角度出发分析了Android下inflate的过程,但整篇博客基本都是源码,并没有使用实际的代码和图片来进行说明与对比,感觉有点摸不着头脑,所以在这篇博客中我将会通过几个实例来对比和证实一下上一篇博客的几个结论。
我们使用ListView的Adapter中的getView()方法来做说明(这里只展示getView()方法的代码,Adapter中其他的代码以及Activity中通过findViewById()获取ListView对象以及给ListView对象设置适配器的代码就省略了,我相信大家都会。):
直接看代码:
首先是条目的xml布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="40dp">
<TextView
android:id="@+id/textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
我们给item的最外层布局指定了高度为40dp,接下来是getView()方法中的代码
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(context).inflate(R.layout.test_item,null);
}
TextView textView = (TextView) convertView.findViewById(R.id.textview);
textView.setText(datas.get(position));
return convertView;
}
使用的是inflate(resId,null)方法,第二个参数传递的null,我们看一下效果:
设置的高度40dp并没有生效。
如何让我们的条目高度显示40dp呢?在这里有2种做法
① getView()方法不变,我们在item的布局文件最外层在嵌套一层布局:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="40dp">
<TextView
android:id="@+id/textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</FrameLayout>
运行的结果如下:
这种做法从界面上看,高度值好像生效了,但实际上我们最外层的高度值依然没有生效,让高度值发生改变的是它的子节点。
补充一点:对于root为null时,不管第三个参数是否设置(没有设置时默认值为root != null),还是设置了true或false,对结果都没有影响,大家可以自己写着看一下。
② item的布局文件不发生改变,改变getView()方法中的inflate()方法参数:
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
// 改变参数
convertView = LayoutInflater.from(context).inflate(R.layout.test_item,parent,false);
}
TextView textView = (TextView) convertView.findViewById(R.id.textview);
textView.setText(datas.get(position));
return convertView;
}
运行的结果如下:
inflate的第二个参数有值,第三个参数设置为false,从界面上看结果和上面一样,高度值生效了,但是这两个方法的本质是不同的,上面一种最外层的高度是不会生效的,就像最开始的例子一样,而生效的是它的孩子节点,也就是item布局中的第二层节点;但是下面这种改变inflate()方法参数让高度值生效的是让最外层的属性生效了,减少了xml文件的层级。
其实这也就是我们在Adapter中解决高度值不生效问题的2种常用方法。
注意我们在Adapter中不能这样写:
convertView = LayoutInflater.from(context).inflate(R.layout.test_item,parent,true);
// 或者不写第三个参数,默认是(root != null),所以还是为true
convertView = LayoutInflater.from(context).inflate(R.layout.test_item,parent);
运行会报如下错误:
FATAL EXCEPTION: main
java.lang.UnsupportedOperationException: addView(View, LayoutParams) is not supported in AdapterView
所以,我们在Activity中来测试这个方法:
需要被inflate成View对象的布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="40dp">
<TextView
android:id="@+id/textview"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="#80800000"
android:text="测试数据" />
</LinearLayout>
Activity的布局文件,只有一个根节点:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/ll_test"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".activity.MainActivity">
</LinearLayout>
Activity中的代码,也很简单:
// initView()方法在onCreate()方法中调用
protected void initView() {
test = (LinearLayout) findViewById(R.id.ll_test);
View view = LayoutInflater.from(this).inflate(R.layout.test_item,test,true);
}
运行结果如下:
高度值生效了。但是细心的人可能发现了,在这里我们并没有把通过inflate得到的view在使用test.addView(view)方法将它添加到Activity的根布局上,但是运行的结果却已经增加到上面了,这是为什么呢?如果我们调用test.addView(view)方法又会是什么情况呢?
下面就来看一下,我们调用test.addView(view)方法会发生什么事:
// initView()方法在onCreate()方法中调用
protected void initView() {
test = (LinearLayout) findViewById(R.id.ll_test);
View view = LayoutInflater.from(this).inflate(R.layout.test_item,test,true);
test.addView(view);// 调用Activity布局根节点的addView()方法
}
运行查看结果,报错了:
FATAL EXCEPTION: main
java.lang.RuntimeException: Unable to start activity ComponentInfo{"项目包名"/"报错类的全路径名"}: java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
大概意思为:这个指定的孩子已经有一个父亲了,你必须首先在让孩子的父亲调用removeView()方法。
这个错误应该也算是一个比较常见的错误了,解决办法:
protected void initView() {
test = (LinearLayout) findViewById(R.id.ll_test);
View view = LayoutInflater.from(this).inflate(R.layout.test_item,test,true);
// 获取Activity的根节点的父节点
ViewParent parent = test.getParent();
// 判断是不是ViewGroup的子类对象
if (parent != null && parent instanceof ViewGroup) {
// 强转,然后调用removeAllViews()方法移出所有的孩子节点
ViewGroup group = (ViewGroup) parent;
group.removeAllViews();
// 将返回的节点增加到父节点上
group.addView(view);
}
}
现在这样运行起来结果和上面就是一样的了,效果图就不贴出来了。
下面来看一下另外一个问题,为什么不用调用addView()方法,内容也会添加到界面上,查看inflate源码:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
...
// 通过createViewFromTag()方法用于根据节点名来创建View对象,这里是创建根视图。在方法内部调用了createView()方法,createView()方法中通过反射创建View对象并返回。
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
// 定义result作为返回值,默认就是root
View result = root;
try {
...
if (root != null && attachToRoot) {
// 如果root不为null并且attachToRoot为true,就把temp增加到root上
root.addView(temp, params);
}
...
}...
// 返回result,也就是返回了root,这时候已经将temp增加到root上了
return result;
}
}
以上的代码省略了大部分,只是将与问题相关的部分提取出来了,通过以上的代码就能知道为什么不用调用addView()方法,内容也会添加到界面上。整个方法的详细注释可以查看《
Android inflate解析》这篇博客。
上面就是对于inflate()方法传不同的参数时对结果View有不同影响的几个简单实例,或许单单看完这篇文章还是有一些地方理解不到。不过没关系,自己写一遍,通过不断改变inflate方法的参数值和变化xml文件的相关地方查看结果,最后在结合《 Android inflate解析》这篇博客细心查看inflate的源码,我相信你对于inflate一定会有更深的理解。