ListView滑动删除实现之一——merge标签与LayoutInflater.inflate()

来源:互联网 发布:杨玉环 知乎 编辑:程序博客网 时间:2024/05/17 06:47

转自:http://blog.csdn.net/harvic880925/article/details/45155965 

从今天开始带着大家做一个滑动删除的listView控件,先拿效果来吸引下大家:


看着是不是挺好玩的,万丈高楼平地起,今天先讲讲有关merge与LayoutInflater.inflate()的用法

一、merge标签

merge标签就是通过删减多余或者额外的层级,从而优化整个Android Layout的结构。核心功能就是减少冗余的层次从而达到优化UI的目的!
看不懂?那看这里吧;merge标签,单从英文来看,意思为“合并”,那它用来合并什么呢?下面我们用例子来说明它可以合并什么。

将通过一个例子来了解这个标签实际所产生的作用,这样可以更直观的了解< merge/>的用法。

1、首先,我们新建一个工程,内容非常简单,界面如下:


当然它的布局代码如下:activity_main.xml

[html] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     xmlns:tools="http://schemas.android.com/tools"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent"  
  5.     android:orientation="vertical"  
  6.     tools:context="com.example.blogmerge.MainActivity" >  
  7.   
  8.     <TextView  
  9.         android:layout_width="wrap_content"  
  10.         android:layout_height="wrap_content"  
  11.         android:text="@string/hello_world" />  
  12.       
  13.     <ImageView   
  14.         android:layout_width="wrap_content"  
  15.         android:layout_height="wrap_content"  
  16.         android:src="@drawable/ic_launcher"/>  
  17.   
  18. </LinearLayout>  
代码非常简单,一个垂直布局下面有一个TextView控件和一个ImageView控件,效果图就是上面的那个
然后我们利用HierarchyViewer工具来看看它的结构:
(由于我用的是Genymotion模拟器,死活出不来模拟器下面的进程,只有用DDMS里的Dump工具来看结构了,大家也可以在SDK/tools文件下找到monitor.bat来启动它,位置如下图:)

有关HierarchyViewer工具的使用,请参考:

1、《Android UI 优化——使用HierarchyViewer工具》

2、《Android UI 调试工具 Hierarchy Viewer》

请注意,HierarchyViewer在调试真机时,一般是会不成功的,因为只有在开发版的 Android 设备上才有这样的功能。

这时候的控件的层次关系是这样的:


最上层的FrameLayout和LinearLayout都是系统自带的布局,右下方的三个蓝色的控件布局是我们activity_main.xml里的控件布局。
从这里可以看到在我们的LinearLayout之上,系统已经给了一层FrameLayout布局,所以我们这里的LinearLayout控件是完全可以删除的,那要怎么删除呢,这里就是merge标签的作用了,我们尝试把我们布局中的LinearLayout标签换成merge标签,再看一下层次结构图;
2、先看看下面activity_main.xml中,根标签换成merge后的代码:

[html] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. <merge xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     xmlns:tools="http://schemas.android.com/tools"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent"  
  5.     tools:context="com.example.blogmerge.MainActivity" >  
  6.   
  7.     <TextView  
  8.         android:layout_width="wrap_content"  
  9.         android:layout_height="wrap_content"  
  10.         android:text="@string/hello_world" />  
  11.   
  12.     <ImageView  
  13.         android:layout_width="wrap_content"  
  14.         android:layout_height="wrap_content"  
  15.         android:src="@drawable/ic_launcher" />  
  16.   
  17. </merge>  
运行出来之后,可以看出与上面的垂直布局已经不一样的,textView与ImageView叠加在了一起,效果是这样的:

然后再看看控件层次图:


从控件层次图中可以明显看出:TextView控件与ImageView控件直接连在了上层的FrameLayout控件上!所以这就少了一个层次,由于直接连在了FrameLayout控件上了,所以这两个控件的布局就是使用的FrameLayout帧布局,这也就是为什么效果图中TextView控件与ImageView控件叠加在一起的原因!

可见:merge标签能够将该标签中的所有控件直接连在上一级布局上面,从而减少布局层级

merge标签有下面两个使用限制:

  • < merge />只能作为XML布局的根标签使用
  • 当Inflate以< merge />开头的布局文件时,必须指定一个父ViewGroup,并且必须设定attachToRoot为true(参看inflate(int, android.view.ViewGroup, Boolean)方法)。 必须为TRUE,是因为MERGE标签里没有可用的根结点 

至于第二点,为什么attachToRoot必须设为TRUE,看完下一部分就自然懂了。

源码在文章底部给出

二、LayoutInflater.inflate()中的AttachToRoot参数

LayoutInflater.inflate()函数的声明方式如下:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. public View inflate(int resource, ViewGroup root, boolean attachToRoot)  
  • 返回值:我们第一个先看返回的VIEW的意义,返回的VIEW指向的根结点。大家可能会有个疑问,第二个参数root不就是根结点吗?那返回根结点的话,不就是我们传进去的root吗?这可不一定,大家知道返回根结点的VIEW之后,继续往下看参数的具体讲解。
  • 第一个参数resource:表示要将XML转成VIEW的layout布局
  • 二个参数root:表示根结点,它的作用主要取决于第三个参数
  • 第三个参数attachToRoot:表示是否将转换后的VIEW直接添加在根结点上,如果是TRUE,那么在转换出来VIEW之后,内部直接会调用root.addView()来将其添加到root结点上,然后返回根结点,当然是我们传进去的ROOT结点。如果设为FALSE,那只会将XML布局转换成对应的VIEW,不会将其添加的我们传进去的root结点上,大家可能会问,那我们传进去的root结点有什么用?在这种情况下,我们root结点的唯一作用,就是在从XML转换布局时,给它提供上层控件的layot_width、layout_height等布局参数,供它产生视图时计算长宽高的位置,由于新产生的视图不依附于root,所以就整个VIEW而言,根结点就是它自己,所以返回值就是XML产生的VIEW本身。
单纯看上面的解释可能有点难度,下面我们举个例子来看看效果:

1、首先,我们建一个工程,activity_mian.xml的代码如下:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     xmlns:tools="http://schemas.android.com/tools"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent"  
  5.     tools:context=".MainActivity"  
  6.     android:orientation="vertical"  
  7.     android:id="@+id/root">  
  8.   
  9.     <TextView  
  10.         android:text="@string/hello_world"  
  11.         android:layout_width="wrap_content"  
  12.         android:layout_height="wrap_content" />  
  13. </LinearLayout>  

2、然后再建一个布局:add_layout.xml

[html] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     android:layout_width="match_parent"  
  3.     android:layout_height="match_parent"  
  4.     android:background="#ff00ff">  
  5.   
  6.     <TextView  
  7.         android:layout_width="match_parent"  
  8.         android:layout_height="match_parent"  
  9.         android:textSize="30dp"  
  10.         android:gravity="center"  
  11.         android:text="xxxxx附加的" />  
  12. </LinearLayout>  
  • 首先,大家可以看到这两个布局的特点,activity_main.xml主布局上只有一个TEXTVIEW控件,而add_layout.xml整体布局上以紫色为背景色,其中有个TEXTVIEW来标识这是附加的
  • 我们的任务就是将add_layout.xml添加到activity_mian.xml布局的LinearLayout结点下面

3、将attachToRoot设为TRUE,看添加效果

看下添加代码:

[html] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. public class MainActivity extends Activity {  
  2.   
  3.     @Override  
  4.     protected void onCreate(Bundle savedInstanceState) {  
  5.         super.onCreate(savedInstanceState);  
  6.         setContentView(R.layout.activity_main);  
  7.         LayoutInflater layoutInflater = LayoutInflater.from(this);  
  8.         LinearLayout linearLayout = (LinearLayout)findViewById(R.id.root);  
  9.         layoutInflater.inflate(R.layout.add_layout,linearLayout,true);  
  10.     }  
  11. }  
在这段代码中,首先通过LayoutInflater.from(Context context);来获取LayoutInflater的实例。
[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. LayoutInflater layoutInflater = LayoutInflater.from(this);  
然后,由于我们要把转换的视图添加到activity_main.xml的linearLayout布局下面,我将它的ID命为root,所以它就是LayoutInflater.inflate()所需要的根结点
下面的代码是获取根结点的控件实例
[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. LinearLayout linearLayout = (LinearLayout)findViewById(R.id.root);  
最后,调用LayoutInflater.inflate()来转换布局,由于我们这里attachToRoot设为了True,所以,在add_layout.xml转换成View布局后,会调用root.addView()来将其添加到根结点视图的下面。所以在效果图显示时,我们是可以看得到add_layout.xml的效果的。
[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. layoutInflater.inflate(R.layout.add_layout,linearLayout,true);  
效果图如下:

4、将attachToRoot设为FALSE,看添加效果

我们来看看如果把attachToRoot设为FALSE,效果图是怎样的,首先代码如下:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. public class MainActivity extends Activity {  
  2.   
  3.     @Override  
  4.     protected void onCreate(Bundle savedInstanceState) {  
  5.         super.onCreate(savedInstanceState);  
  6.         setContentView(R.layout.activity_main);  
  7.         LayoutInflater layoutInflater = LayoutInflater.from(this);  
  8.         LinearLayout linearLayout = (LinearLayout)findViewById(R.id.root);  
  9.         layoutInflater.inflate(R.layout.add_layout,linearLayout,false);  
  10.     }  
  11. }  
大家可以看到,在代码上除了最后一句layoutInflater.inflate(R.layout.add_layout,linearLayout,false);中把true改成了false,其它没有任何变化
效果图是这样的


可以看到,我们的主布局没有任何变化,也就是说add_layout.xml的布局没有被添加到activity_mian.xml中;
我们开头就讲过,如果attachToRoot设为false,那转换后的布局是不会被添加到root中的,会作为结果返回。
其实attachToRoot设为TRUE的代码与下面的代码是等价的:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. public class MainActivity extends Activity {  
  2.   
  3.     @Override  
  4.     protected void onCreate(Bundle savedInstanceState) {  
  5.         super.onCreate(savedInstanceState);  
  6.         setContentView(R.layout.activity_main);  
  7.         LayoutInflater layoutInflater = LayoutInflater.from(this);  
  8.         LinearLayout linearLayout = (LinearLayout)findViewById(R.id.root);  
  9.         View view = layoutInflater.inflate(R.layout.add_layout,linearLayout,false);  
  10.         linearLayout.addView(view);  
  11.     }  
  12. }  
即将 layoutInflater.inflate(R.layout.add_layout,linearLayout,false);返回的VIEW通过addView(view);手动添加到根结点!
效果图跟attachToRoot一样,如下:

我们前面也讲过,当attachToRoot设为TRUE时,其内部会调用root.addView(view)来将布局添加到root中;

源码在文章底部给出

三、透过源码分析LayoutInflater.inflate()的过程

在大家看到上面的现象以后,我们透过源码的角度来分析,attachToRoot这个参数都做了些什么,root又用来做了些什么。
先看LayoutInflate.inflate()的实现:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. public View inflate(int resource, ViewGroup root, boolean attachToRoot) {  
  2.     XmlResourceParser parser = getContext().getResources().getLayout(resource);  
  3.     try {  
  4.         return inflate(parser, root, attachToRoot);  
  5.     } finally {  
  6.         parser.close();  
  7.     }  
  8. }  
首先,通过我们传进去的布局,透过XML分析器获取parser:
[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. XmlResourceParser parser = getContext().getResources().getLayout(resource);  
然后调用了inflate(parser, root, attachToRoot)函数,下面看看这个函数的实现:
(在源码的基础上,我去掉了很多非关键代码,首重给大家展示核心部分是怎么做的)
先看整体代码,然后再细讲:
[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {  
  2.     synchronized (mConstructorArgs) {  
  3.         //第一步:初始化  
  4.         final AttributeSet attrs = Xml.asAttributeSet(parser);  
  5.         Context lastContext = (Context)mConstructorArgs[0];  
  6.         mConstructorArgs[0] = mContext;  
  7.         //注意这里,在初始化时,result表示要返回的视图,默认是返回root  
  8.         View result = root;  
  9.           
  10.         …………  
  11.           
  12.         final String name = parser.getName();  
  13.           
  14.         //第二步:创建XML对应的空白VIEW:temp  
  15.         if (TAG_MERGE.equals(name)) {  
  16.         //如果是merge标签:抛出异常  
  17.             …………  
  18.         } else {  
  19.             View temp;  
  20.             if (TAG_1995.equals(name)) {  
  21.                 temp = new BlinkLayout(mContext, attrs);  
  22.             } else {  
  23.                 temp = createViewFromTag(root, name, attrs);  
  24.             }  
  25.   
  26.             //第三步:从根结点中,获取布局参数,设置到temp中  
  27.             ViewGroup.LayoutParams params = null;  
  28.             if (root != null) {  
  29.                 // Create layout params that match root, if supplied  
  30.                 params = root.generateLayoutParams(attrs);  
  31.                 if (!attachToRoot) {  
  32.                     // Set the layout params for temp if we are not  
  33.                     // attaching. (If we are, we use addView, below)  
  34.                     temp.setLayoutParams(params);  
  35.                 }  
  36.             }  
  37.   
  38.             //第四步:初始化temp中的子控件  
  39.             rInflate(parser, temp, attrs, true);  
  40.   
  41.             //第五步:如果root不为空,而且attachToRoot设为TRUE,则将其视图通过addView添加到root中  
  42.             if (root != null && attachToRoot) {  
  43.                 root.addView(temp, params);  
  44.             }  
  45.   
  46.             //第六步:如果root为空,或者attachToRoot设为FALSE,那么就将TEMP视图做为result返回  
  47.             if (root == null || !attachToRoot) {  
  48.                 result = temp;  
  49.             }  
  50.         }  
  51.   
  52.         return result;  
  53.     }  
  54. }  
从上面注释大家应该也可以看出来功能所在,还是再带着大家重复一遍整个流程吧

第一步:一进来是初始化部分:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. final AttributeSet attrs = Xml.asAttributeSet(parser);  
  2. Context lastContext = (Context)mConstructorArgs[0];  
  3. mConstructorArgs[0] = mContext;  
  4. //注意这里,在初始化时,result表示要返回的视图,默认是返回root  
  5. View result = root;  
通过XML文件获取布局中各个控件的属性集:AttributeSet,注意:这里通过XML只能知道布局里每个控件的布局参数。那整个LAYOUT的布局参数呢,虽然我们在XML的根结点的布局可能写的是layout_width:fill_parent,layout_height:fill_parent。但这只是很笼统的,系统要确实计算出它的宽高数来。这个宽高数的计算就是通过root中的属性来得出来的,下面代码中会提到。
一个很重要的部分在于最后一句话!!!
[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. View result = root;  
result表示最后返回的视图VIEW,所以这表示在默认情况下,返回root的视图!!注意这只是在默认情况下,下面会对result进行赋值的!

第二步:创建XML对应的空白视图temp

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. //第二步:创建XML对应的空白VIEW:temp  
  2. if (TAG_MERGE.equals(name)) {  
  3. //如果是merge标签,抛出异常  
  4.     …………  
  5. else {  
  6.     View temp;  
  7.     if (TAG_1995.equals(name)) {  
  8.         temp = new BlinkLayout(mContext, attrs);  
  9.     } else {  
  10.         temp = createViewFromTag(root, name, attrs);  
  11.     }  
在这里首先判断XML的根结点是不是merge标签,大家知道我们的merge标签的主要作用是用来将merge标签下的所有控件合并到其上层布局上,也就是说merge标签下是没有根控件的!因为merge标签下的控件都是并列关系,所以如果merge标签使用的inflate函数,那我们根本没有办法给它返回根视图,所以肯定是要抛出异常的
如果不是merge标签,就创建一个空白视图,返回给temp,这里的temp就是我们XML所对应的布局!

第三步:获取root的布局参数,设置到temp中

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. //第三步:从根结点中,获取布局参数,设置到temp中  
  2. ViewGroup.LayoutParams params = null;  
  3. if (root != null) {  
  4.     params = root.generateLayoutParams(attrs);  
  5.     if (!attachToRoot) {  
  6.         temp.setLayoutParams(params);  
  7.     }  
  8. }  
在这里看到,首先获取到了ROOT的布局参数赋值为params,然后当attachToRoot为FALSE时,将参数设置到temp中;

第四步:初始化temp中的子控件

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. //第四步:初始化temp中的子控件  
  2. rInflate(parser, temp, attrs, true);  
rInflate()其实是一个递归函数,用来递归建立temp下的所有控件的视图

第五步:如果root不为空,而且attachToRoot设为TRUE,则将其视图通过addView添加到root中

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. if (root != null && attachToRoot) {  
  2.     root.addView(temp, params);  
  3. }  
在这里,就是当root不为空、attachToRoot为TRUE时,将构建好的temp视图添加到root中,注意这里的参数仍然从root中获取的布局参数params!!!所以,无论attachToRoot值为如何,temp的布局参数都是从ROOT里获取的!!!!

第六步:如果root为空,或者attachToRoot设为FALSE,那么就将TEMP视图做为result返回

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. if (root == null || !attachToRoot) {  
  2.     result = temp;  
  3. }  
从这里可以看到,如果root为空,获取attachToRoot为FALSE,就会将temp做为结果返回!

到这里整个过程就分析结束了,下面我们总结一下:
1、root的最基本作用,就是给我们传进去的Layout提供布局参数信息
2、如果attachToRoot为TRUE,那么会将Layout产生的布局视图添加到root中,返回root
如果attachToRoot为FALSE,那么会将Layout产生的布局视图直接返回

好了,整篇文章也就结束了,下一篇将带大家初步使用scrollTo来实现滑动


0 0