一
问题抛出
Listview 是 android 里面的重要组件,用来显示一个竖向列表,这个没有什么问题;但是有个时候列表里面的 item 不是一样的,如下图,列表里面应该有 3 种类型的 item
1. 头像在左边的气泡 Item ,比如 ” 今天下午我就不出来了 ,...”
2. 头像在右边的气泡 Item ,比如 ” 那就等着我发你好吧 ”
3. 单张图片显示圆角图片 item
几种 Item 的风格是完全不同的,那么怎么实现呢?
二 实现方法
实现的方法我这里可以列举出两种
1. 每个 Item 的布局文件包含气泡,左右头像和圆角图片,然后根据不同的条件做不同的逻辑判断,控制不同 Item 的显示和隐藏。
比如如果是接受信息,而且是文字的话,就显示左图片和文字;隐藏图片和右边图片,等等。
显而易见,这种方法很繁琐,很笨重,而且会导致 item 的布局文件很大,从而影响 listview 的效率(加载 xml 文件是需要时间的)
2. 使用 listview 提供的方法,实现的步骤如下:
a. 主要重写 ListAdapter 的 newView(), getItemViewType(),getViewItemTypeCount() 几个方法,如下:
嗯,所做的差不多就是这么多,另外就是要准备
3
个
item
布局文件,也就是
newView
里面要调用的
3
个布局文件
对了,在 bindView 的时候最好对 view 进行 null 的检查,因为 3 个布局文件里面的 view 是不同的,或者要分开进行 bind ,不然有可能会有空指针异常。
三 原理分析
上面第 2 种实现方法确实比较灵活,那 listview 是怎么实现的呢?
而且我们知道 listview 的 item 是可以复用的,那么为什么它不会复用错位呢?比如第 2 种类型的 item ,结果找到了缓存中第 1 种类型的 item ,就像本来要显示一个发送图片,结果找到发送文字的 item ,那么复用的时候肯定有问题,因为发送文字的 item 中根本没有 ImageView ,只有 TextView 来显示文字的。
1. 文件路径
frameworks\base\core\java\android\widget\AbsListView.java
代码
在
ListView
的一个
item
要显示的时候,就会调用
AbsListView.obtainView()
方法,比如滑动的时候,滑动出一个
Item
AbsListview 会向 RecycleBin 请求一个 scrapView ,这个 RecycleBin 是 listview 里面的一个重要机制,简单来说,就是它缓存了那些不在屏幕内的 listview item ,相当于一个垃圾箱,然后当有新的 item 需要显示的时候,它会首先向垃圾箱里面请求一个已经不显示的 item ,如果有这样的 item 的话,就直接拿过来,然后调用下 bindView ,重新 bind 下数据就可以了。
如果没有这样的 item 就会调用 newView 去创建一个 item view 。
滑动的时候,它会不断地把滑出屏幕的 item 添加到 RecycleBin 这个垃圾箱里面。
这样就实现了一个循环, listview 不管有多少数据,不管滑动多少次,真正通过 newView 产生的 item view 其实就是一个屏幕内最多容纳的 item 数目,形成一个链条结构,不断回收,不断复用。
那接下来看看 mRecycler.getScrapView(position) 的实现
这个
viewTypeCount
就是
ListAdapter
的
getViewTypeCount()
方法返回的,默认实现就是返回
1
,如果没有重写的话,在
setAdapter
的时候调用,代表的是
listview
里面会有多少种类型的
item
,如下:
如果为
viewTypeCount==1
的话,也就是只有一种类型的
item,
那么直接从
mCurrentScrap
里面获取即可。
那如果有多个类型的 item 的话,怎么办呢?
首先,调用我们重写的 getItemViewType(int position) 来获取到这种类型的 item 的索引号
int whichScrap = mAdapter.getItemViewType(position);
然后根据这个索引号 whichScrap 从 mScrapView 数组里面获取到一个垃圾箱,然后再从垃圾箱里面去获取这个类型的被回收的 Item 。
这样就解决了复用错误的问题,比如把第 2 种类型的 item 复用了缓存中第 1 种类型的 Item ,这样就解决了第三章开头说的那个复用错位的问题。
2. Listview 是怎么把一个 item 添加到垃圾箱?
那么,我们来拿一个简单的情景来举例子,比如滑动的时候
文件路径:
frameworks\base\core\java\android\widget\AbsListView.java
代码:
如果向上滚动的话,那么就判断
item
的
bottom-
滚动距离
>=0
?如果是,那么说明这个
item
还是可见的,不应该添加到垃圾箱;否则就不可见了。
这个判断逻辑要结合下手机屏幕坐标系来理解,如下:
图有点丑,勿见怪,坐标原点 (0,0) 是在屏幕的左上方,这个有点特别。
那么如果是向上滑动话的,我们要判断某个 item 是否被滑动出了屏幕,就是判断这个 item 的 bottom – 向上滚动量 < 0? 比如一个 item 的最下面的边界是 50px 那个地方,然后向上滚动了 60px ,那么肯定已经滑动出了屏幕,对不对?
也就是 50 – 60 = -10 <0
如果只是滑动了 40px ,那么这个 item 应该还有 10px 留着屏幕上面,这个时候肯定不能被回收,因为它对于用户还是可见的。
也就是 50 – 40 = 10 >0
如果刚好滑动了 50px ,按照 listview 的逻辑,这个 item 也是不回收的。如下:
再排除是否是
listview
的
header
或者
footer
,如果不是的话,那就是
listview
的内容
item
了,应该添加到垃圾箱里面。
为了清理内存,它会先清理掉这个
itemview
的一些属性,然后调用
mRecycler.addScrapView(child, position);
添加到垃圾箱。
那如果是向下滑动呢?
根据上面手机的坐标系,这个时候肯定是判断 item 的 top 和整个 ListView 的高度以及滚动距离。应该是 top+ 滚动距离 > 整个 ListView 的高度,这个时候说明 item 已经不可见;如果 top + 滚动距离 <= 整个 ListView 的高度,就说明这个 item 还是可见的。
好,分析完这个滚动的计算逻辑后,来看看如何把
view
添加到垃圾箱的。
四
小结
1. 这篇帖子总结 Listview 中如果有多种类型的 item 的实现方式和原理。
2. 多个 Item 实现的原理主要就是 AbsListView 中有个 mScrapViews 数组,它的大小对应着 Item 类型的数目,也就是 getItemTypeCount 的返回大小。这个 mScrapViews 里面根据 viewType 的值,把不同类型的 item 存放在不同的 ArrayList 里面;
然后获取的时候再根据这个 viewType 首先来找到对应的 ArrayList 垃圾箱,然后再从 ArrayList 垃圾箱里面找到同一个类型的缓存 item ,当然如果没有找到,就会调用 newView 新建。
3. 分析了滚动的情况下, listview 判断 item 是否可见的实现原理,它是根据 item 的坐标来判断的。
Listview 是 android 里面的重要组件,用来显示一个竖向列表,这个没有什么问题;但是有个时候列表里面的 item 不是一样的,如下图,列表里面应该有 3 种类型的 item


1. 头像在左边的气泡 Item ,比如 ” 今天下午我就不出来了 ,...”
2. 头像在右边的气泡 Item ,比如 ” 那就等着我发你好吧 ”
3. 单张图片显示圆角图片 item
几种 Item 的风格是完全不同的,那么怎么实现呢?
二 实现方法
实现的方法我这里可以列举出两种
1. 每个 Item 的布局文件包含气泡,左右头像和圆角图片,然后根据不同的条件做不同的逻辑判断,控制不同 Item 的显示和隐藏。
比如如果是接受信息,而且是文字的话,就显示左图片和文字;隐藏图片和右边图片,等等。
显而易见,这种方法很繁琐,很笨重,而且会导致 item 的布局文件很大,从而影响 listview 的效率(加载 xml 文件是需要时间的)
2. 使用 listview 提供的方法,实现的步骤如下:
a. 主要重写 ListAdapter 的 newView(), getItemViewType(),getViewItemTypeCount() 几个方法,如下:
对了,在 bindView 的时候最好对 view 进行 null 的检查,因为 3 个布局文件里面的 view 是不同的,或者要分开进行 bind ,不然有可能会有空指针异常。
三 原理分析
上面第 2 种实现方法确实比较灵活,那 listview 是怎么实现的呢?
而且我们知道 listview 的 item 是可以复用的,那么为什么它不会复用错位呢?比如第 2 种类型的 item ,结果找到了缓存中第 1 种类型的 item ,就像本来要显示一个发送图片,结果找到发送文字的 item ,那么复用的时候肯定有问题,因为发送文字的 item 中根本没有 ImageView ,只有 TextView 来显示文字的。
1. 文件路径
frameworks\base\core\java\android\widget\AbsListView.java
代码
AbsListview 会向 RecycleBin 请求一个 scrapView ,这个 RecycleBin 是 listview 里面的一个重要机制,简单来说,就是它缓存了那些不在屏幕内的 listview item ,相当于一个垃圾箱,然后当有新的 item 需要显示的时候,它会首先向垃圾箱里面请求一个已经不显示的 item ,如果有这样的 item 的话,就直接拿过来,然后调用下 bindView ,重新 bind 下数据就可以了。
如果没有这样的 item 就会调用 newView 去创建一个 item view 。
滑动的时候,它会不断地把滑出屏幕的 item 添加到 RecycleBin 这个垃圾箱里面。
这样就实现了一个循环, listview 不管有多少数据,不管滑动多少次,真正通过 newView 产生的 item view 其实就是一个屏幕内最多容纳的 item 数目,形成一个链条结构,不断回收,不断复用。
那接下来看看 mRecycler.getScrapView(position) 的实现
那如果有多个类型的 item 的话,怎么办呢?
首先,调用我们重写的 getItemViewType(int position) 来获取到这种类型的 item 的索引号
int whichScrap = mAdapter.getItemViewType(position);
然后根据这个索引号 whichScrap 从 mScrapView 数组里面获取到一个垃圾箱,然后再从垃圾箱里面去获取这个类型的被回收的 Item 。
这样就解决了复用错误的问题,比如把第 2 种类型的 item 复用了缓存中第 1 种类型的 Item ,这样就解决了第三章开头说的那个复用错位的问题。
2. Listview 是怎么把一个 item 添加到垃圾箱?
那么,我们来拿一个简单的情景来举例子,比如滑动的时候
文件路径:
frameworks\base\core\java\android\widget\AbsListView.java
代码:
这个判断逻辑要结合下手机屏幕坐标系来理解,如下:

图有点丑,勿见怪,坐标原点 (0,0) 是在屏幕的左上方,这个有点特别。
那么如果是向上滑动话的,我们要判断某个 item 是否被滑动出了屏幕,就是判断这个 item 的 bottom – 向上滚动量 < 0? 比如一个 item 的最下面的边界是 50px 那个地方,然后向上滚动了 60px ,那么肯定已经滑动出了屏幕,对不对?
也就是 50 – 60 = -10 <0
如果只是滑动了 40px ,那么这个 item 应该还有 10px 留着屏幕上面,这个时候肯定不能被回收,因为它对于用户还是可见的。
也就是 50 – 40 = 10 >0
如果刚好滑动了 50px ,按照 listview 的逻辑,这个 item 也是不回收的。如下:
那如果是向下滑动呢?
根据上面手机的坐标系,这个时候肯定是判断 item 的 top 和整个 ListView 的高度以及滚动距离。应该是 top+ 滚动距离 > 整个 ListView 的高度,这个时候说明 item 已经不可见;如果 top + 滚动距离 <= 整个 ListView 的高度,就说明这个 item 还是可见的。
1. 这篇帖子总结 Listview 中如果有多种类型的 item 的实现方式和原理。
2. 多个 Item 实现的原理主要就是 AbsListView 中有个 mScrapViews 数组,它的大小对应着 Item 类型的数目,也就是 getItemTypeCount 的返回大小。这个 mScrapViews 里面根据 viewType 的值,把不同类型的 item 存放在不同的 ArrayList 里面;
然后获取的时候再根据这个 viewType 首先来找到对应的 ArrayList 垃圾箱,然后再从 ArrayList 垃圾箱里面找到同一个类型的缓存 item ,当然如果没有找到,就会调用 newView 新建。
3. 分析了滚动的情况下, listview 判断 item 是否可见的实现原理,它是根据 item 的坐标来判断的。