深入理解ListView工作原理(二)

ListView常用优化技巧

使用ViewHolder模式提高效率

ViewHolder模式是提高ListView效率的一个重要的方法。ViewHolder模式充分利用了ListView的视图缓存机制,避免了每次在调用getView()的时候都去通过findViewById()实例化控件。据测试,使用ViewHolder将提高50%以上的效率。使用ViewHolder模式来优化ListView非常简单,只需要在自定义Adapter中定义一个内部类ViewHolder,并将布局中的控件作为成员变量。

1
2
3
4
5
public final class ViewHolder {
public TextView mTv;
public ImageView mIv;
...
}

接下来,只要在getView()方法中通过视图缓存机制来重用以缓存即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder = null;
if (convertView == null) {
viewHolder = new ViewHolder();
convertView = mInflater.infalte(R.layout.viewholder_item, null);
viewHolder.mTv = (TextView) convertView.findViewById(R.id.tv);
viewHolder.mIv = (ImageView) convertView.findViewById(R.id.iv);
...
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
}
viewHolder.mTv.setText(mData.get(position));
viewHolder.mIv.setBackgroundResource(R.drawable.ic);
...
return convertView;
}
设置项目分隔线

ListView的各个项目之间,可以通过设置分割线来进行区分,系统提供了diver和diverHeight这样两个属性来实现这一功能。通过这两个属性,也可以控制机ListView之间的分割线和它的高度。当然,分割线不仅仅可以设置为一个颜色,同样也可以设置为一个图片资源,分割线的使用代码如下所示。

1
2
android:divider="颜色或图片资源"
android:dividerHeight="dp值"

特殊情况下,当设置分隔线为null时,就可以把分隔线设置为透明了。

1
android:divider="@null"

divider的源码实现如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
@Override
protected void dispatchDraw(Canvas canvas) {
if (mCachingStarted) {
mCachingActive = true;
}
final int dividerHeight = mDividerHeight;
final Drawable overscrollHeader = mOverScrollHeader;
final Drawable overscrollFooter = mOverScrollFooter;
final boolean drawOverscrollHeader = overscrollHeader != null;
final boolean drawOverscrollFooter = overscrollFooter != null;
final boolean drawDividers = dividerHeight > 0 && mDivider != null;
if (drawDividers || drawOverscrollHeader || drawOverscrollFooter) {
final Rect bounds = mTempRect;
bounds.left = mPaddingLeft;
bounds.right = mRight - mLeft - mPaddingRight;
final int count = getChildCount();
final int headerCount = getHeaderViewsCount();
final int itemCount = mItemCount;
final int footerLimit = (itemCount - mFooterViewInfos.size());
final boolean headerDividers = mHeaderDividersEnabled;
final boolean footerDividers = mFooterDividersEnabled;
final int first = mFirstPosition;
final boolean areAllItemsSelectable = mAreAllItemsSelectable;
final ListAdapter adapter = mAdapter;
final boolean fillForMissingDividers = isOpaque() && !super.isOpaque();
if (fillForMissingDividers && mDividerPaint == null && mIsCacheColorOpaque) {
mDividerPaint = new Paint();
mDividerPaint.setColor(getCacheColorHint());
}
final Paint paint = mDividerPaint;
int effectivePaddingTop = 0;
int effectivePaddingBottom = 0;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
effectivePaddingTop = mListPadding.top;
effectivePaddingBottom = mListPadding.bottom;
}
final int listBottom = mBottom - mTop - effectivePaddingBottom + mScrollY;
if (!mStackFromBottom) {
int bottom = 0;
final int scrollY = mScrollY;
if (count > 0 && scrollY < 0) {
if (drawOverscrollHeader) {
bounds.bottom = 0;
bounds.top = scrollY;
drawOverscrollHeader(canvas, overscrollHeader, bounds);
} else if (drawDividers) {
bounds.bottom = 0;
bounds.top = -dividerHeight;
drawDivider(canvas, bounds, -1);
}
}
for (int i = 0; i < count; i++) {
final int itemIndex = (first + i);
final boolean isHeader = (itemIndex < headerCount);
final boolean isFooter = (itemIndex >= footerLimit);
if ((headerDividers || !isHeader) && (footerDividers || !isFooter)) {
final View child = getChildAt(i);
bottom = child.getBottom();
final boolean isLastItem = (i == (count - 1));
if (drawDividers && (bottom < listBottom)
&& !(drawOverscrollFooter && isLastItem)) {
final int nextIndex = (itemIndex + 1);
if (adapter.isEnabled(itemIndex) && (headerDividers || !isHeader
&& (nextIndex >= headerCount)) && (isLastItem
|| adapter.isEnabled(nextIndex) && (footerDividers || !isFooter
&& (nextIndex < footerLimit)))) {
bounds.top = bottom;
bounds.bottom = bottom + dividerHeight;
drawDivider(canvas, bounds, i);
} else if (fillForMissingDividers) {
bounds.top = bottom;
bounds.bottom = bottom + dividerHeight;
canvas.drawRect(bounds, paint);
}
}
}
}
final int overFooterBottom = mBottom + mScrollY;
if (drawOverscrollFooter && first + count == itemCount &&
overFooterBottom > bottom) {
bounds.top = bottom;
bounds.bottom = overFooterBottom;
drawOverscrollFooter(canvas, overscrollFooter, bounds);
}
} else {
int top;
final int scrollY = mScrollY;
if (count > 0 && drawOverscrollHeader) {
bounds.top = scrollY;
bounds.bottom = getChildAt(0).getTop();
drawOverscrollHeader(canvas, overscrollHeader, bounds);
}
final int start = drawOverscrollHeader ? 1 : 0;
for (int i = start; i < count; i++) {
final int itemIndex = (first + i);
final boolean isHeader = (itemIndex < headerCount);
final boolean isFooter = (itemIndex >= footerLimit);
if ((headerDividers || !isHeader) && (footerDividers || !isFooter)) {
final View child = getChildAt(i);
top = child.getTop();
if (drawDividers && (top > effectivePaddingTop)) {
final boolean isFirstItem = (i == start);
final int previousIndex = (itemIndex - 1);
if (adapter.isEnabled(itemIndex) && (headerDividers || !isHeader
&& (previousIndex >= headerCount)) && (isFirstItem ||
adapter.isEnabled(previousIndex) && (footerDividers || !isFooter
&& (previousIndex < footerLimit)))) {
bounds.top = top - dividerHeight;
bounds.bottom = top;
drawDivider(canvas, bounds, i - 1);
} else if (fillForMissingDividers) {
bounds.top = top - dividerHeight;
bounds.bottom = top;
canvas.drawRect(bounds, paint);
}
}
}
}
if (count > 0 && scrollY > 0) {
if (drawOverscrollFooter) {
final int absListBottom = mBottom;
bounds.top = absListBottom;
bounds.bottom = absListBottom + scrollY;
drawOverscrollFooter(canvas, overscrollFooter, bounds);
} else if (drawDividers) {
bounds.top = listBottom;
bounds.bottom = listBottom + dividerHeight;
drawDivider(canvas, bounds, -1);
}
}
}
}
super.dispatchDraw(canvas);
}
1
2
3
4
5
void drawDivider(Canvas canvas, Rect bounds, int childIndex) {
final Drawable divider = mDivider;
divider.setBounds(bounds);
divider.draw(canvas);
}

在dispatchDraw()方法里面会计算出每一个divider的bounds范围最终通过drawDivider()方法绘制到canvas上面。

隐藏ListView的滚动条

默认的ListView在滚动时,在右边会显示滚动条,指示当前滑动的位置,我们可以设置scrollbars属性,控制ListView的滚动条状态。特别的,当设置scrollbars属性为none的时候,ListView滚动或者不滚动,就都不会出现滚动条了。

1
android:srcollbars="none"
取消ListView的item点击效果

当点击ListView中的一项时,系统会默认出现一个点击效果,在android5.x上是一个波纹效果,而在android5.x之下的版本则是一个改变背景颜色的效果,但可以通过修改listSelector属性来取消掉点击后的回馈效果。

1
android:listSelector="#00000000"

当然,也可以直接使用android自带的透明色来实现这个效果。

1
android:listSelector="@android:color/transparent"
设置ListView需要在显示在第几页

ListView以Item为单位进行显示,默认显示在第一个Item,当需要指定显示的Item时,可以通过如下代码来实现。

1
listView.setSelection(N);

其中N就是需要显示的第N个Item。

当然,这个方法类似srcollTo,是瞬间完成的移动。除此之外,还可以通过如下代码来实现平滑移动。

1
2
3
mListView.smoothSrcollBy(distance, duration);
mListView.smoothScrollByOffset(offset);
mListView.smoothScrollToPosition(index);
动态修改ListView

ListView中的数据在某些情况下是需要变化的,当然可以通过重新设置ListView的Adapter来更新ListView的显示,但这也就需要重新获取一下数据,相当于重新创建ListView,这样显然不是非常友好,而且效率也不会太高。因此,可以使用一个简单的方法来哦实现ListView的动态修改,代码如下所示。

1
2
mData.add("new");
mAdapter.notifyDataSetChanged();

当修改了传递给Adapter的映射List之后,只需要通过调用Adapter的notifyDataSetChanged()方法,通知ListView更改数据源即可完成对ListView的动态修改。不过,使用这个方法有一点需要注意的是,在使用mAdapter.notifyDataSetChanged()方法时,必须保证传进Adapter的数据List是同一个List而不能是其他对象,否则将无法实现该效果。

遍历ListView中所有item

ListView作为一个ViewGroup,为我们提供了操纵子View的各种方法,最常用的就是通过getChildAt()来获取第i个子View。

1
2
3
for (int i = 0; i < mListView.getChildCount(); i ++) {
View view = mListView.getChildAt(i);
}
处理空ListView

ListView用于展示列表数据,但当列表中无数据时,ListView不会显示任何数据或提示,按照完善用户体验得到需求,这里应该给以无数据的提示。ListView提供了一个方法——setEmptyView(),通过这个方法可以给ListView设置一个在空数据下显示的默认提示。

1
mListView.setEmptyView(findViewById(R.id.empty_view));

通过以上代码,就给ListView在空数据时显示一张默认的图片,用来提示用户,而在有数据时,就不会显示。

ListView滑动监听

ListView的滑动监听,是ListView中最重要的技巧,很多重写的ListView,基本上都是在滑动事件的处理上下功夫,通过判断滑动事件进行不同的逻辑处理。而为了更加准确地监听滑动事件,开发者通常还需要使用GestureDetector手势识别,VelocityTracker滑动速度检测等辅助类来完成更好的监听。这里介绍两种监听ListView滑动事件的方法,一个是通过OnTouchListener来实现监听,另一个是使用OnScrollListener来实现监听。

  • OnTouchListener

    OnTouchListener是View中的监听事件,通常监听ACTION_DOWN、ACTION_MOVE、ACTION_UP这三个事件发生时的坐标,就可以根据坐标判断用户滑动的方向,并在不同的事件中进行相应的逻辑处理,这种方式的使用代码如下所示。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    mListView.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
    break;
    case MotionEvent.ACTION_MOVE:
    break;
    case MotionEvent.ACTION_UP:
    break;
    default:
    break;
    }
    return false;
    }
    });
  • OnScrollListener

    OnScrollListener是AbsListView中的监听事件,他封装了很多与ListView相关的信息,使用起来也更加灵活。首先来看一下OnScrollListener的一般使用方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    mListView.setOnScrollListener(new AbsListView.OnScrollListener() {
    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {
    switch (scrollState) {
    case SCROLL_STATE_IDLE:
    // 滑动停止时
    break;
    case SCROLL_STATE_TOUCH_SCROLL:
    // 正在滚动
    break;
    case SCROLL_STATE_FLING:
    // 手指抛动时,即手指用例滑动,在离开后ListView由于惯性继续滑动
    break;
    default:
    break;
    }
    }
    @Override
    public void onScroll(AbsListView view, int firstVisibleItem,
    int visibleItemCount, int totalItemCount) {
    // 滚动时一直调用,firstVisibleItem表示当前的第一个Item的ID(从0开始), visibleItemCount表示当前能看见的Item总数,totalItemCount表示整个ListView的Item总数
    }
    });

    通过onScroll()方法提供的几个参数,可以方便地进行一些判断,比如判断是否滚动到最后一行,当前可视的第一个Item的ID加上当前当前可视Item的和等于Item总数的时候,即滚动到了最后一行。

    1
    2
    3
    if (firstVisiableItem + visibleItemCount == totalItemCount && totalItemCount > 0) {
    //滚动到最后一行
    }

    再比如,可以判断滚动的方向,通过一格成员变量lastVisiableItemPostion来记录上次第一个可视的Item的ID并与当前的可视Item的ID进行比较,即可知道当前滚动的方向。

    1
    2
    3
    4
    5
    6
    if (firstVisiableItem > lastVisiableItemPostion) {
    // 上滑
    } else if(firstVisiableItem < lastVisiableItemPostion) {
    // 下滑
    }
    lastVisiableItemPostion = firstVisiableItem;

    当然,ListView也给我们提供了一些封装的方法来获取当前可视的Item的位置等信息。

    1
    2
    3
    4
    // 获取可视区域最后一个Item的id
    mListView.getLastVisiablePosition();
    // 获取可视区域内第一个Item的id
    mListView.getFirstVisiablePosition();

ListView常用扩展

ListView虽然使用广泛,但系统原生的ListView显然是不能满足用户在审美、功能上不断提高需求的。不过也不要紧,android完全可以定制化,让我们非常方便地对原生ListView进行扩展、修改。于是,在开发者的创新下,ListView越来越丰富多彩,各种各样的基于原生ListView的扩展让人目不暇接。下面来看几个常用的ListView扩展。

具有弹性的ListView

android默认的ListView在滚动到顶端或者底端的时候,并没有很好的提示。在android5.x中,Google为这样的行为值添加了一个半月形的阴影效果。而在iOS系统中,列表都是具有弹性的,即滚动到底端后继续往下或者往上滑动一段距离。

重写ListView来实现弹性效果的方法有很多,比如增加HeaderView或者使用ScrollView进行嵌套。通过查看ListView源代码的时候可以发现,ListView中有一个控制滑动到边缘的处理方法。

1
2
3
4
5
6
7
protected boolean overScrollBy(int deltaX, int deltaY,
int scrollX, int scrollY,
int scrollRangeX, int scrollRangeY,
int maxOverScrollX, int maxOverScrollY,
boolean isTouchEvent) {
...
}

可以看见这样一个参数,maxOverScrollY,这个值默认为0,但其实只要修改这个参数的值,就可以让ListView具有弹性了。当然,为了能够满足多分辨率的需求,可以在修改的时候通过屏幕的density来计算具体的值,让不同分辨率的弹性距离基本一致。

自动显示、隐藏布局的ListView

Google+有这样一个效果,当我们在ListView上滑动的时候,顶部的ActionBar或者ToolBar就会相应的隐藏或显示。我们知道,让一个布局显示或者隐藏并带有动画效果,可以通过属性动画来很方便地实现,所以这个效果关键就在于如何获得ListView的各种滑动事件。所以借助OnTouchListener接口来监听ListView的滑动,通过比较与上次坐标的大小,来判断滑动的方向,并通过滑动的方向来判断是否需要显示或者隐藏对应的布局。在开始判断滑动事件之前,还要做一些准备工作,首先需要给ListView增加一个HeaderView,避免第一个Item被Toolbar遮挡。

1
2
3
View header = new View(this);
header.setLayoutParams(new AbsListView.LayoutParams(AbsListView.LayoutParams.MATCH_PARENT, (int)getResource().getDimension(R.dimen.abc_action_bar_default_height_material)));
mListView.addHeaderView(header);

另外,定义一个mTouchSlop变量用来获取系统认为的最低滑动距离,即超过这个距离的移动,系统就将其定义为滑动状态。

1
mTouchSlop = ViewConfiguration.get(this).getScaledTouchSlop();

有了前面的准备工作,下面就可以判断滑动的事件了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
View.OnTouchListener mTouchListener = new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mFirstY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
mCurrent = event.getY();
if (mCurrentY - mFirstY > mTouchSlop) {
direction = 0; // down
} else if(mFirstY - mCurrentY > mTouchSlop) {
direction = 1; // up
}
if (direction == 1) {
if (mShow) {
toolbarAnim(0); // hide
mShow = !mShow;
}
} else if (direction == 0) {
if (!mShow) {
toolbarAnim(1);
mShow = !mShow;
}
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
return false;
}
}

代码逻辑非常简单,只是通过滑动点的坐标改变大小,来判断移动的方向,并根据移动方向来执行不同的动画效果。

有了前面的分析,实现这样一个效果就非常简单了,最后加上控制布局显示隐藏的动画,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
private void toolbarAnim(int flag) {
if (mAnimator != null && mAnimator.isRunning()) {
mAnimator.cancel();
}
if (flag == 0) {
mAnimator = ObjectAnimator.ofFloat(mToolbar, "translationY",
mToolbar.getTraslationY(), 0);
} else {
mAnimator = ObjectAnimator.ofFloat(mToolbar, "translationY",
mToolbar.getTraslationY(), - mToolbar.getHeight());
}
mAnimator.start();
}