LayoutInflater学习笔记

一次在做ListView渲染一个列表的时候,希望可以让每个列表都以指定的高度来渲染,可是发现设置自定义布局高度竟然不起作用,于是在网上搜索了一下,发现原来Android系统中控件本身并没有设置高宽的属性,所以我们在xml布局的时候都是用的android:layout_width或者android:layout_height来渲染控件的高宽,而不是android:width或android: height,它们的宽高都是有父布局统一分配的,先暂时这么理解控件本身的宽高。

LayoutInflater

在Android开发中会经常遇到这个LayoutInflater,多数是在自定义控件或布局中,inflate(int resource, ViewGroup root)或者inflate(int resource, ViewGroup root, boolean attachToRoot)。可能有时为了方面起见,直接使用View类下面的inflate(Context context, int resource, ViewGroup root)方法。查看一下源码,我们发现View类中的方法仍然是调用的LayoutInflater中的方法。

public static View inflate(Context context, int resource, ViewGroup root) {
	LayoutInflater factory = LayoutInflater.from(context);
	return factory.inflate(resource, root);
}

LayoutInflater就是将xml中layout布局文件实例化为View对象的,直译就是布局填充器,Android系统给我们提供了三种方式来获取一个LayoutInflater实例:

  • public static LayoutInflater from(Context context)

    LayoutInflater inflater =LayoutInflater.from(context);

  • public LayoutInflater getLayoutInflater()

    LayoutInflater inflater =context.getLayoutInflater();

  • public Object getSystemService(String name)

    LayoutInflater inflater =(LayoutInflater)context. getSystemService(Context.LAYOUT_INFLATER_SERVICE);

三种方式最终都归为一种,都是采用方式3来创建一个LayoutInflater实例的。

实例探讨

<!-- Button.xml -->
<Button xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="240dp"
    android:layout_height="80dp"
    android:text="Button" >
</Button>

<!-- Layout_main.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/linearLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >
</LinearLayout>

方式一

linearLayout = (LinearLayout) findViewById(R.id.linearLayout);
LayoutInflater inflater = LayoutInflater.from(this);
View view = inflater.inflate(R.layout.button, null);
linearLayout.addView(view);

我们发现按钮定义的宽高都没有起作用,按钮本身已经被添加到相应的布局当中,在这种方式中如果我们将inflater.inflate(R.layout.button, null)中的null改为linearLayout,这时候会抛出下面异常:

Caused by: java.lang.IllegalStateException: The specified child already has a parent.You must call removeView() on the child's parent first.

这里为什么会抛出异常,暂时先不解释,我们继续往下探讨,已经将null该为linearLayout,现在再将linearLayout.addView(view)这一行代码注释掉,运行结果如下图:

此时按钮不但已经被加入到目标布局中,而且按钮的宽高已经起了作用。继续走,inflater.inflate(R.layout.button, linearLayout),把linearLayout设置为空,把最后一行代码仍然注释掉,这时候我们发现按钮已经没有再被填充到布局中。为什么会出现上述情况呢,我们查看一下源代码:

public View inflate(int resource, ViewGroup root) {
        return inflate(resource, root, root != null);
}

到这里已经愈渐清晰了,两个参数的方法就是调用的三个参数的方法,三个参数就是接下来要探讨的方式二。

方式二

linearLayout = (LinearLayout) findViewById(R.id.linearLayout);
LayoutInflater inflater = LayoutInflater.from(this);
View view = inflater.inflate(R.layout.button, linearLayout, false);
linearLayout.addView(view);

刚一上来发现所有的执行效果都是我们所要的理想效果,按钮已经按照指定宽高填充进父布局。感觉天下太平,一片祥和,可以笙歌艳舞了。接着往下走,inflater.inflate(R.layout.button, linearLayout, false),将false该为true,这时又抛出了同样的异常:

Caused by: java.lang.IllegalStateException: The specified child already has a parent.You must call removeView() on the child's parent first.

这时候再把linearLayout.addView(view)这一行代码注释掉,又跟刚才效果一模一样了,一切恢复正常了。继续走,把inflater.inflate(R.layout.button, linearLayout, false)中的linearLayout设置为null,这时候不管我们最后一个参数设置为true或者false都不会让按钮按照指定的宽高显示,结果都是如下图:

还剩下最后一种情况,就是inflater.inflate(R.layout.button, null, false),linearLayout.addView(view)这一行代码仍然注释掉,这时布局中已经没有了按钮的影子,按钮并没有被加到布局中去。

源码解析

上面两种方式我们实验了各种情况,出现了各种不同的结果,知其然知其所以然,还是回归源码吧,重点就在下面这个方法:

LayoutInflater中inflate(int resource, ViewGroup root, boolean attachToRoot)方法中,我们前面所有情况归根到底都是对后面两个参数不同情况的探讨。

public View inflate (int resource, ViewGroup root, boolean attachToRoot)

  • resource:将要加载的xml布局文件的ID
  • root:如果attachToRoot为true,返回生成视图的父,也就是root,如果attachToRoot为false,root仅仅返回的是包含已经生成视图的宽高参数的对象。
  • attachToRoot:如果为true,则返回填充视图的root,否则root仅仅被当做父布局创建子布局xml的布局参数。

方法返回值是一个已经渲染的层级视图,如果最后一个参数attachToRoot为true,返回的是root,如果设置为false,则仅仅返回resource布局文件设置的视图View。

该方法又调用了下面的方法:

public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            Context lastContext = (Context)mConstructorArgs[0];
            mConstructorArgs[0] = mContext;
            View result = root;
			...
            try {
              View temp;
				if (TAG_1995.equals(name)) {
					temp = new BlinkLayout(mContext, attrs);
				} else {
					temp = createViewFromTag(root, name, 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
				rInflate(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;
				}
			}
			…
            return result;
        }
    }

先看下面几行代码:

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);
	}
}

先看temp对象,temp对象是我们resource的xml布局文件返回的根视图,如果root不为空,首先创建布局参数params,如果attachToRoot为false,temp这个View临时对象才会设置布局参数params。

接下来分析下面代码:

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;
}

如果root不为null并且attachToRoot为true,此时会将填充视图的布局参数params和布局视图添加进入root中去。

如果root为null并且attachToRoot为true,此时仅仅是将子视图返回,而且视图无布局参数,因为上面一个代码片段我们已经看到只有root为null是才创建布局参数params。

分析完这些,我们就可以很简单的推断出上面的各种情况了。

结论

  1. 如果root不为null,attachToRoot设置为true,如果在执行linearLayout.addView(view)就会抛出如下异常:

    Caused by: java.lang.IllegalStateException: The specified child already has a parent.You must call removeView() on the child's parent first.

    因为这时候infalte已经将我们子视图填充进了父布局中,而且ViewGroup中已经判断,如果子布局已经填充进了一个父布局,一个孩子只有一个亲爹嘛,再次填充就会抛出异常。

    if (child.getParent() != null) {
    	throw new IllegalStateException("The specified child already has a parent. " +
    			"You must call removeView() on the child's parent first.");
    }
    
  2. View的inflate(Context context, int resource, ViewGroup root)方法填充布局时,如果root不为null,则这时候都不需要在是使用linearLayout.addView(view)方法再次填充了,该方法调用了LayoutInflater中的方法,只要root不为空,attachToRoot一定是true。
  3. View的inflate(Context context, int resource, ViewGroup root)方法填充布局时,如果root不为null,此时在布局文件中指定的宽高都无意义了,因为只有root不为null是才会生成子布局的布局参数params。

这里就不做更多的总结了。

其它

文章开头说的ListView中每一个条目高度固定,包括在GridView中,虽然这中情况在开发中很少见,一般都要根据屏幕适配。在public View getView(int position, View convertView, ViewGroup parent)中采用自定义布局时,要注意一点,就是不管用那种方式 最后一定要返回自定义布局的视图,也就是绝对不可以返回root,否则会抛出下面异常:

java.lang.UnsupportedOperationException: addView(View, LayoutParams) is not supported in AdapterView

先不看源码,简单分析一下,这里抛出异常也是可以理解的,我们做适配器的目的就是为了返回一个自定义布局视图,然而这个时候如果我们将它放入了root中去,那么这个root是哪个root呢,我们一个ListView或GridView会有多个条目,从上面的分析中我们也知道一个子View只能有一个父亲,而这里我们自定义布局就一个ID,多个条目岂不是要多次添加,如果就这样也会在addView时也会抛出异常。当然了,我们还是看一下源码吧,ListView的继承了AdapterView,如果我们的root不为空并且attachToRoot为true的话,通过上面的分析,我们知道这里会执行ViewGroup中的addView方法,而AdapterView实现了ViewGroup的部分方法,其中就包括了addView方法。

public void addView(View child) {
        throw new UnsupportedOperationException("addView(View) is not supported in AdapterView");
}

到这里就明白了,只要执行addView系统就给抛出异常。

评论

您确定要删除吗?删除之后不可恢复