ListView
实现不同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()
几个方法,如下:01 | public class MyCursorAdapter extends CursorAdapter { |
03 | publicMyCursorAdapter(Context context, Cursor c, boolean autoRequery) { |
05 | super (context, c,autoRequery); |
07 | // TODO Auto-generatedconstructor stub |
13 | publicMyCursorAdapter(Context context, Cursor c, int flags) { |
15 | super (context, c, flags); |
17 | // TODO Auto-generatedconstructor stub |
23 | public void bindView(Viewarg0, Context arg1, Cursor arg2) { |
32 | publicView newView(Context arg0, Cursor arg1, ViewGroup arg2) { |
54 | public int getItemViewType( int position) { |
56 | StringitemValue = getCursor().getString(position); |
63 | //另外,这个序号是从0开始索引的,由于我们有3种类型的item,所以返回0,1,2,请参考getViewTypeCount() |
65 | if (itemValue.equals( "接收文字信息" )){ //如果是接受文字信息,则显示布局1 |
69 | } else if (itemValue.equals( "发送文字信息" )){ //如果是发送文字信息,则显示布局2 |
84 | public intgetViewTypeCount() { |
86 | return 3 ; //有3种类型的item,所以返回3 |
嗯,所做的差不多就是这么多,另外就是要准备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
代码03 | * The data set used to store unused viewsthat should be reused during the next layout |
05 | * to avoid creating new ones |
09 | final RecycleBin mRecycler = newRecycleBin(); |
10 | View obtainView( int position, boolean [] isScrap) { |
16 | final View scrapView =mRecycler.getScrapView(position); |
在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; |
04 | private ArrayList<View> mCurrentScrap; |
07 | View getScrapView( int position) { |
09 | if (mViewTypeCount == 1 ) { |
11 | return retrieveFromScrap(mCurrentScrap,position); |
15 | int whichScrap =mAdapter.getItemViewType(position); |
17 | if (whichScrap>= 0 && whichScrap < mScrapViews.length) { |
19 | returnretrieveFromScrap(mScrapViews[whichScrap], position); |
这个viewTypeCount
就是ListAdapter
的getViewTypeCount()
方法返回的,默认实现就是返回1
,如果没有重写的话,在setAdapter
的时候调用,代表的是listview
里面会有多少种类型的item
,如下:01 | public void setAdapter(ListAdapter adapter) { |
05 | super .setAdapter(adapter); |
14 | mRecycler.setViewTypeCount(mAdapter.getViewTypeCount()); |
如果为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
代码:003 | * Track a motion scroll |
007 | * @param deltaY Amount tooffset mMotionView. This is the accumulated delta since the motion |
010 | began. Positive numbers mean the user'sfinger is moving down the screen. |
012 | * @param incrementalDeltaYChange in deltaY from the previous event. |
018 | boolean trackMotionScroll(intdeltaY, int incrementalDeltaY) { |
027 | int top =-incrementalDeltaY; |
031 | for ( int i = 0 ; i< childCount; i++) { |
033 | final View child= getChildAt(i); |
035 | if (child.getBottom() >= top) { |
043 | int position= firstPosition + i; |
045 | if (position>= headerViewsCount && position < footerViewsStart) { |
047 | // Theview will be rebound to new data, clear any |
049 | //system-managed transient state. |
051 | if (child.isAccessibilityFocused()) { |
053 | child.clearAccessibilityFocus(); |
057 | mRecycler.addScrapView(child, position); |
069 | int bottom = getHeight() -incrementalDeltaY; |
071 | if ((mGroupFlags& CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { |
073 | bottom -=listPadding.bottom; |
077 | for ( int i =childCount - 1 ; i >= 0 ; i--) { |
080 | final View child = getChildAt(i); |
082 | if (child.getTop() <= bottom) { |
092 | int position= firstPosition + i; |
094 | if (position>= headerViewsCount && position < footerViewsStart) { |
096 | // Theview will be rebound to new data, clear any |
098 | //system-managed transient state. |
100 | if (child.isAccessibilityFocused()) { |
102 | child.clearAccessibilityFocus(); |
106 | mRecycler.addScrapView(child, position); |
如果向上滚动的话,那么就判断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) { |
再排除是否是listview
的header
或者footer
,如果不是的话,那就是listview
的内容item
了,应该添加到垃圾箱里面。05 | int position= firstPosition + i; |
07 | if (position>= headerViewsCount && position < footerViewsStart) { |
09 | // Theview will be rebound to new data, clear any |
11 | //system-managed transient state. |
13 | if (child.isAccessibilityFocused()) { |
15 | child.clearAccessibilityFocus(); |
19 | mRecycler.addScrapView(child,position); |
为了清理内存,它会先清理掉这个itemview
的一些属性,然后调用mRecycler.addScrapView(child, position);
添加到垃圾箱。那如果是向下滑动呢?根据上面手机的坐标系,这个时候肯定是判断item
的top
和整个ListView
的高度以及滚动距离。应该是top+
滚动距离 >
整个ListView
的高度,这个时候说明item
已经不可见;如果top +
滚动距离 <=
整个ListView
的高度,就说明这个item
还是可见的。1 | int bottom = getHeight() -incrementalDeltaY; |
2 | if (child.getTop() <=bottom) { //仍然可见 |
好,分析完这个滚动的计算逻辑后,来看看如何把view
添加到垃圾箱的。01 | void addScrapView(View scrap, int position) { |
07 | if (mViewTypeCount == 1 ) { |
11 | mCurrentScrap.add(scrap); |
13 | } else { //如果有多种item类型,找到viewType对应的垃圾箱添加 |
16 | mScrapViews[viewType].add(scrap); |
四 小结1. 这篇帖子总结Listview
中如果有多种类型的item
的实现方式和原理。2. 多个Item
实现的原理主要就是AbsListView
中有个mScrapViews数组,它的大小对应着Item
类型的数目,也就是getItemTypeCount
的返回大小。这个mScrapViews
里面根据viewType
的值,把不同类型的item
存放在不同的ArrayList
里面;然后获取的时候再根据这个viewType
首先来找到对应的ArrayList
垃圾箱,然后再从ArrayList
垃圾箱里面找到同一个类型的缓存item
,当然如果没有找到,就会调用newView
新建。3. 分析了滚动的情况下,listview
判断item
是否可见的实现原理,它是根据item
的坐标来判断的。
本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请
点击举报。