Android ListView实现不同item的方法和原理分析

本文介绍Android ListView中处理多种类型Item的方法及原理,包括自定义Adapter实现和RecycleBin机制解析。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

问题抛出
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() 几个方法,如下:
01 public class MyCursorAdapter extends CursorAdapter {
02  
03 publicMyCursorAdapter(Context context, Cursor c, boolean autoRequery) {
04  
05 super(context, c,autoRequery);
06  
07 // TODO Auto-generatedconstructor stub
08  
09 }
10  
11  
12  
13 publicMyCursorAdapter(Context context, Cursor c, int flags) {
14  
15 super(context, c, flags);
16  
17 // TODO Auto-generatedconstructor stub
18  
19 }
20  
21  
22  
23 public void bindView(Viewarg0, Context arg1, Cursor arg2) {
24  
25  
26  
27 }
28  
29  
30  
31  
32 publicView newView(Context arg0, Cursor arg1, ViewGroup arg2) {
33  
34 if (接受文字){
35  
36 return 接受文字view
37  
38 }else if(发送文字){
39  
40 return 发送文字view
41  
42  
43 }else{
44  
45 return 圆角图片view
46  
47 }
48  
49 }
50  
51  
52  
53  
54 public int getItemViewType(int position) {
55  
56 StringitemValue = getCursor().getString(position);
57  
58  
59  
60 //下面的代码只是模拟判断逻辑
61  
62  
63 //另外,这个序号是从0开始索引的,由于我们有3种类型的item,所以返回0,1,2,请参考getViewTypeCount()
64  
65 if(itemValue.equals("接收文字信息")){//如果是接受文字信息,则显示布局1
66  
67 return0;
68  
69 }else if(itemValue.equals("发送文字信息")){//如果是发送文字信息,则显示布局2
70  
71 return1;
72  
73 }
74  
75  
76 return 2;//显示单张图片
77  
78  
79 }
80  
81  
82  
83  
84 public intgetViewTypeCount() {
85  
86 return 3;//有3种类型的item,所以返回3
87  
88 }
89 }
嗯,所做的差不多就是这么多,另外就是要准备 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
代码
01 /**
02  
03 * The data set used to store unused viewsthat should be reused during the next layout
04  
05 * to avoid creating new ones
06  
07 */
08  
09 final RecycleBin mRecycler = newRecycleBin();
10 View obtainView(int position,boolean[] isScrap) {
11  
12 ...
13  
14  
15  
16 final View scrapView =mRecycler.getScrapView(position);
17  
18 ...
19 }
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) 的实现
01 private ArrayList<View>[] mScrapViews;
02  
03  
04 private ArrayList<View> mCurrentScrap;
05  
06  
07 View getScrapView(int position) {
08  
09 if (mViewTypeCount ==1) {
10  
11 return retrieveFromScrap(mCurrentScrap,position);
12  
13 } else {
14  
15 int whichScrap =mAdapter.getItemViewType(position);
16  
17 if (whichScrap>= 0 && whichScrap < mScrapViews.length) {
18  
19 returnretrieveFromScrap(mScrapViews[whichScrap], position);
20  
21 }
22  
23 }
24  
25 return null;
26  
27 }
这个 viewTypeCount 就是 ListAdapter getViewTypeCount() 方法返回的,默认实现就是返回 1 ,如果没有重写的话,在 setAdapter 的时候调用,代表的是 listview 里面会有多少种类型的 item ,如下:
01 public void setAdapter(ListAdapter adapter) {
02  
03 ...
04  
05 super.setAdapter(adapter);
06  
07  
08  
09 if (mAdapter != null){
10  
11  
12  
13  
14 mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());
15  
16  
17  
18 }
19 ..
20 }
如果为 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
代码:
001 /**
002  
003 * Track a motion scroll
004  
005 *
006  
007 * @param deltaY Amount tooffset mMotionView. This is the accumulated delta since the motion
008  
009 *
010 began. Positive numbers mean the user'sfinger is moving down the screen.
011  
012 * @param incrementalDeltaYChange in deltaY from the previous event.
013  
014 * <a href="http://home.51cto.com/index.php?s=/space/34010" target="_blank">@return</a> true if we'realready at the beginning/end of the list and have nothing to do.
015  
016 */
017  
018 boolean trackMotionScroll(intdeltaY, int incrementalDeltaY) {
019  
020 ...
021  
022  
023  
024 if (down) {//向上滚动
025  
026  
027 int top =-incrementalDeltaY;
028  
029 ...
030  
031 for (int i = 0; i< childCount; i++) {
032  
033 final View child= getChildAt(i);
034  
035 if(child.getBottom() >= top) {
036  
037 break;
038  
039 } else {
040  
041 count++;
042  
043 int position= firstPosition + i;
044  
045 if (position>= headerViewsCount && position < footerViewsStart) {
046  
047 // Theview will be rebound to new data, clear any
048  
049 //system-managed transient state.
050  
051 if(child.isAccessibilityFocused()) {
052  
053 child.clearAccessibilityFocus();
054  
055 }
056  
057 mRecycler.addScrapView(child, position);
058  
059 }
060  
061 }
062  
063 }
064  
065 } else {//向下滚动
066  
067  
068  
069 int bottom = getHeight() -incrementalDeltaY;
070  
071 if ((mGroupFlags& CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
072  
073 bottom -=listPadding.bottom;
074  
075 }
076  
077 for (int i =childCount - 1; i >= 0; i--) {
078  
079  
080 final View child = getChildAt(i);
081  
082 if(child.getTop() <= bottom) {
083  
084 break;
085  
086 } else {
087  
088 start = i;
089  
090 count++;
091  
092 int position= firstPosition + i;
093  
094 if (position>= headerViewsCount && position < footerViewsStart) {
095  
096 // Theview will be rebound to new data, clear any
097  
098 //system-managed transient state.
099  
100 if(child.isAccessibilityFocused()) {
101  
102 child.clearAccessibilityFocus();
103  
104 }
105  
106 mRecycler.addScrapView(child, position);
107  
108 }
109  
110 }
111  
112 }
113  
114 }
如果向上滚动的话,那么就判断 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 也是不回收的。如下:
1 if (child.getBottom() >=top) {
2  
3  
4 break;
5  
6 }
再排除是否是 listview  header 或者 footer ,如果不是的话,那就是 listview 的内容 item 了,应该添加到垃圾箱里面。
01 else {
02  
03 count++;
04  
05 int position= firstPosition + i;
06  
07 if (position>= headerViewsCount && position < footerViewsStart) {
08  
09 // Theview will be rebound to new data, clear any
10  
11 //system-managed transient state.
12  
13 if(child.isAccessibilityFocused()) {
14  
15 child.clearAccessibilityFocus();
16  
17 }
18  
19 mRecycler.addScrapView(child,position);
20  
21 }
22  
23 }
为了清理内存,它会先清理掉这个 itemview 的一些属性,然后调用 mRecycler.addScrapView(child, position); 添加到垃圾箱。



那如果是向下滑动呢?
根据上面手机的坐标系,这个时候肯定是判断 item top 和整个 ListView 的高度以及滚动距离。应该是 top+  滚动距离 整个 ListView 的高度,这个时候说明 item 已经不可见;如果 top +  滚动距离  <=  整个 ListView 的高度,就说明这个 item 还是可见的。
1 int bottom = getHeight() -incrementalDeltaY;
2 if (child.getTop() <=bottom) {//仍然可见
3  
4  
5 break;
6 }
好,分析完这个滚动的计算逻辑后,来看看如何把 view 添加到垃圾箱的。
01 void addScrapView(View scrap,int position) {
02  
03 ...
04  
05  
06  
07 if(mViewTypeCount == 1) {
08 //如果只有一种item类型,直接添加
09  
10  
11 mCurrentScrap.add(scrap);
12  
13 } else {//如果有多种item类型,找到viewType对应的垃圾箱添加
14  
15  
16 mScrapViews[viewType].add(scrap);
17  
18 }
19  
20 ...
21 }
   小结
1.  这篇帖子总结 Listview 中如果有多种类型的 item 的实现方式和原理。
2.  多个 Item 实现的原理主要就是 AbsListView 中有个 mScrapViews 数组,它的大小对应着 Item 类型的数目,也就是 getItemTypeCount 的返回大小。这个 mScrapViews 里面根据 viewType 的值,把不同类型的 item 存放在不同的 ArrayList 里面;
然后获取的时候再根据这个 viewType 首先来找到对应的 ArrayList 垃圾箱,然后再从 ArrayList 垃圾箱里面找到同一个类型的缓存 item ,当然如果没有找到,就会调用 newView 新建。
3.  分析了滚动的情况下, listview 判断 item 是否可见的实现原理,它是根据 item 的坐标来判断的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值