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。
分析完这些,我们就可以很简单的推断出上面的各种情况了。
结论
-
如果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."); }
- View的inflate(Context context, int resource, ViewGroup root)方法填充布局时,如果root不为null,则这时候都不需要在是使用linearLayout.addView(view)方法再次填充了,该方法调用了LayoutInflater中的方法,只要root不为空,attachToRoot一定是true。
- 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系统就给抛出异常。