转自:http://www.cnblogs.com/xiaoran1129/archive/2012/07/04/2576221.html
Android的ListView是应用最广的一个组件,功能强大,扩展性灵活(不局限于ListView本身一个类),前面的文章有介绍分组,拖拽,3D立体,游标,圆角,而今天我们要介绍的是另外一个扩展ListView:下拉刷新的ListView。
下拉刷新界面最初流行于iphone应用界面,如图:
然后在Android中也逐渐被应用,比如微博,资讯类。
所以,今天要实现的结果应该也是类似的,先贴出最终完成效果,如下图,接下来我们一步一步实现。
1. 流程分析
下拉刷新最主要的流程是:
(1). 下拉,显示提示头部界面(HeaderView),这个过程提示用户"下拉刷新"
(2). 下拉到一定程度,超出了刷新最基本的下拉界限,我们认为达到了刷新的条件,提示用户可以"松手刷新"了,效果上允许用户继续下拉
(3). 用户松手,可能用户下拉远远不止提示头部界面,所以这一步,先反弹回仅显示提示头部界面,然后提示用户"正在加载"。
(4). 加载完成后,隐藏提示头部界面。
示意图如下:
2. 实现分析
当前我们要实现上述流程,是基于ListView的,所以对应ListView本身的功能我们来分析一下实现原理:
(1). 下拉,显示提示头部界面,这个过程提示用户"下拉刷新"
a. 下拉的操作,首先是监听滚动,ListView提供了onScroll()方法
b. 与下拉类似一个动作向下飞滑,所以ListView的scrollState有3种值:SCROLL_STATE_IDLE, SCROLL_STATE_TOUCH_SCROLL, SCROLL_STATE_FLING,意思容易理解,而我们要下拉的触发条件是SCROLL_STATE_TOUCH_SCROLL。判断当前的下拉操作状态,ListView提供了public void onScrollStateChanged(AbsListView view, int scrollState) {}。
c. 下拉的过程中,我们可能还需要下拉到多少的边界值处理,重写onTouchEvent(MotionEvent ev){}方法,可依据ACTION_DOWN,ACTION_MOVE,ACTION_UP实现更精细的判断。
(2). 下拉到一定程度,超出了刷新最基本的下拉界限,我们认为达到了刷新的条件,提示用户可以"松手刷新"了,效果上允许用户继续下拉
a. 达到下拉刷新界限,一般指达到header的高度的,所以有两步,第一,获取header的高度,第二,当header.getBottom()>=header的高度时,我们认为就达到了刷新界限值
b. 继续允许用户下拉,当header完全下拉后,默认无法继续下拉,但是可以增加header的PaddingTop实现这种效果
(3). 用户松手,可能用户下拉远远不止提示头部界面,所以这一步,先反弹回仅显示提示头部界面,然后提示用户"正在加载"。
a. 松手后反弹,这个不能一下子弹回去,看上去太突然,需要一步一步柔性的弹回去,像弹簧一样,我们可以new一个Thread循环计算减少PaddingTop,直到PaddingTop为0,反弹结束。
b. 正在加载,在子线程里处理后台任务
(4). 加载完成后,隐藏提示头部界面。
a. 后台任务完成后,我们需要隐藏header,setSelection(1)即实现了从第2项开始显示,间接隐藏了header。
上面我们分析了实现过程的轮廓,接下来,我们通过细节说明和代码具体实现。
3. 初始化
一切状态显示都是用HeaderView显示的,所以我们需要一个HeaderView的layout,使用addHeaderView方法添加到ListView中。
同时,默认状态下,HeaderView是不显示的,只是在下拉后才显示,所以我们需要隐藏HeaderView且不影响后续的下拉显示,用setSelection(1)。
refresh_list_header.xml布局如下:
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
|
<?
xml
version
=
"1.0"
encoding
=
"utf-8"
?>
<
LinearLayout
xmlns:android
=
"http://schemas.android.com/apk/res/android"
android:layout_width
=
"fill_parent"
android:layout_height
=
"wrap_content"
android:gravity
=
"center"
>
<
ProgressBar
android:id
=
"@+id/refresh_list_header_progressbar"
android:layout_width
=
"wrap_content"
android:layout_height
=
"wrap_content"
android:layout_gravity
=
"center"
style
=
"?android:attr/progressBarStyleSmall"
android:visibility
=
"gone"
>
</
ProgressBar
>
<
ImageView
android:id
=
"@+id/refresh_list_header_pull_down"
android:layout_width
=
"9dip"
android:layout_height
=
"25dip"
android:layout_gravity
=
"center"
android:src
=
"@drawable/refresh_list_pull_down"
/>
<
ImageView
android:id
=
"@+id/refresh_list_header_release_up"
android:layout_width
=
"9dip"
android:layout_height
=
"25dip"
android:layout_gravity
=
"center"
android:src
=
"@drawable/refresh_list_release_up"
android:visibility
=
"gone"
/>
<
RelativeLayout
android:layout_width
=
"180dip"
android:layout_height
=
"wrap_content"
>
<
TextView
android:id
=
"@+id/refresh_list_header_text"
android:layout_width
=
"fill_parent"
android:layout_height
=
"wrap_content"
android:gravity
=
"center"
android:layout_alignParentTop
=
"true"
android:textSize
=
"12dip"
android:textColor
=
"#192F06"
android:paddingTop
=
"8dip"
android:text
=
"@string/app_list_header_refresh_down"
/>
<
TextView
android:id
=
"@+id/refresh_list_header_last_update"
android:layout_width
=
"fill_parent"
android:layout_height
=
"wrap_content"
android:gravity
=
"center"
android:layout_below
=
"@id/refresh_list_header_text"
android:textSize
=
"12dip"
android:textColor
=
"#192F06"
android:paddingBottom
=
"8dip"
android:text
=
"@string/app_list_header_refresh_last_update"
/>
</
RelativeLayout
>
</
LinearLayout
>
|
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
|
private
LinearLayout mHeaderLinearLayout =
null
;
private
TextView mHeaderTextView =
null
;
private
TextView mHeaderUpdateText =
null
;
private
ImageView mHeaderPullDownImageView =
null
;
private
ImageView mHeaderReleaseDownImageView =
null
;
private
ProgressBar mHeaderProgressBar =
null
;
public
RefreshListView(Context context) {
this
(context,
null
);
}
public
RefreshListView(Context context, AttributeSet attrs) {
super
(context, attrs);
init(context);
}
void
init(
final
Context context) {
mHeaderLinearLayout = (LinearLayout) LayoutInflater.from(context).inflate(R.layout.refresh_list_header,
null
);
addHeaderView(mHeaderLinearLayout);
mHeaderTextView = (TextView) findViewById(R.id.refresh_list_header_text);
mHeaderUpdateText = (TextView) findViewById(R.id.refresh_list_header_last_update);
mHeaderPullDownImageView = (ImageView) findViewById(R.id.refresh_list_header_pull_down);
mHeaderReleaseDownImageView = (ImageView) findViewById(R.id.refresh_list_header_release_up);
mHeaderProgressBar = (ProgressBar) findViewById(R.id.refresh_list_header_progressbar);
setSelection( 1 );
setOnScrollListener(this);
} 默认就显示完成了。
|
4. HeaderView的默认高度测量
因为下拉到HeaderView全部显示出来,就由提示"下拉刷新"变为"松手刷新",全部显示的出来的测量标准就是header.getBottom()>=header的高度。
所以,首先我们需要测量HeaderView的默认高度。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
//因为是在构造函数里测量高度,应该先measure一下
private
void
measureView(View child) {
ViewGroup.LayoutParams p = child.getLayoutParams();
if
(p ==
null
) {
p =
new
ViewGroup.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
int
childWidthSpec = ViewGroup.getChildMeasureSpec(
0
,
0
+
0
, p.width);
int
lpHeight = p.height;
int
childHeightSpec;
if
(lpHeight >
0
) {
childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight,
MeasureSpec.EXACTLY);
}
else
{
childHeightSpec = MeasureSpec.makeMeasureSpec(
0
,
MeasureSpec.UNSPECIFIED);
}
child.measure(childWidthSpec, childHeightSpec);
}
|
然后在init的上述代码后面加上调用measureView后,使用getMeasureHeight()方法获取header的高度:
1
2
3
4
5
6
|
private
int
mHeaderHeight;
void
init(
final
Context context) {
... ...
measureView(mHeaderLinearLayout);
mHeaderHeight = mHeaderLinearLayout.getMeasuredHeight();
}
|
5. scrollState监听记录
scrollState有3种,使用onScrollStateChanged()方法监听记录。
1
2
3
4
5
|
private
int
mCurrentScrollState;
@Override
public
void
onScrollStateChanged(AbsListView view,
int
scrollState) {
mCurrentScrollState = scrollState;
}
|
然后即可使用mCurrentScrollState作为后面判断的条件了。
6. 刷新状态分析
因为一些地方需要知道我们处在正常状态下还是进入下拉刷新状态还是松手反弹状态,比如,
(1). 在非正常的状态下,我们不小心飞滑了一下(松手的瞬间容易出现这种情况),我们不能setSelection(1)的,否则总是松手后header跳的一下消失掉了。
(2). 下拉后要做一个下拉效果的特殊处理,需要用到OVER_PULL_REFRESH(松手刷新状态下)
(3). 松手反弹后要做一个反弹效果的特殊处理,需要用到OVER_PULL_REFRESH和ENTER_PULL_REFRESH。
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
|
private
final
static
int
NONE_PULL_REFRESH =
0
;
//正常状态
private
final
static
int
ENTER_PULL_REFRESH =
1
;
//进入下拉刷新状态
private
final
static
int
OVER_PULL_REFRESH =
2
;
//进入松手刷新状态
private
final
static
int
EXIT_PULL_REFRESH =
3
;
//松手后反弹后加载状态
private
int
mPullRefreshState =
0
;
//记录刷新状态
@Override
public
void
onScroll(AbsListView view,
int
firstVisibleItem,
int
visibleItemCount,
int
totalItemCount) {
if
(mCurrentScrollState ==SCROLL_STATE_TOUCH_SCROLL
&& firstVisibleItem ==
0
&& (mHeaderLinearLayout.getBottom() >=
0
&& mHeaderLinearLayout.getBottom() < mHeaderHeight)) {
//进入且仅进入下拉刷新状态
if
(mPullRefreshState == NONE_PULL_REFRESH) {
mPullRefreshState = ENTER_PULL_REFRESH;
}
}
else
if
(mCurrentScrollState ==SCROLL_STATE_TOUCH_SCROLL
&& firstVisibleItem ==
0
&& (mHeaderLinearLayout.getBottom() >= mHeaderHeight)) {
//下拉达到界限,进入松手刷新状态
if
(mPullRefreshState == ENTER_PULL_REFRESH || mPullRefreshState == NONE_PULL_REFRESH) {
mPullRefreshState = OVER_PULL_REFRESH;
//下面是进入松手刷新状态需要做的一个显示改变
mDownY = mMoveY;
//用于后面的下拉特殊效果
mHeaderTextView.setText(
"松手刷新"
);
mHeaderPullDownImageView.setVisibility(View.GONE);
mHeaderReleaseDownImageView.setVisibility(View.VISIBLE);
}
}
else
if
(mCurrentScrollState ==SCROLL_STATE_TOUCH_SCROLL && firstVisibleItem !=
0
) {
//不刷新了
if
(mPullRefreshState == ENTER_PULL_REFRESH) {
mPullRefreshState = NONE_PULL_REFRESH;
}
}
else
if
(mCurrentScrollState == SCROLL_STATE_FLING && firstVisibleItem ==
0
) {
//飞滑状态,不能显示出header,也不能影响正常的飞滑
//只在正常情况下才纠正位置
if
(mPullRefreshState == NONE_PULL_REFRESH) {
setSelection(
1
);
}
}
}
|
mPullRefreshState将是后面我们处理边界的重要变量。
6. 下拉效果的特殊处理
所谓的特殊处理,当header完全显示后,下拉只按下拉1/3的距离下拉,给用户一种艰难下拉,该松手的弹簧感觉。
这个在onTouchEvent里处理比较方便:
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
|
private
float
mDownY;
private
float
mMoveY;
@Override
public
boolean
onTouchEvent(MotionEvent ev) {
switch
(ev.getAction()) {
case
MotionEvent.ACTION_DOWN:
//记下按下位置
//改变
mDownY = ev.getY();
break
;
case
MotionEvent.ACTION_MOVE:
//移动时手指的位置
mMoveY = ev.getY();
if
(mPullRefreshState == OVER_PULL_REFRESH) {
//注意下面的mDownY在onScroll的第二个else中被改变了
mHeaderLinearLayout.setPadding(mHeaderLinearLayout.getPaddingLeft(),
(
int
)((mMoveY - mDownY)/
3
),
//1/3距离折扣
mHeaderLinearLayout.getPaddingRight(),
mHeaderLinearLayout.getPaddingBottom());
}
break
;
case
MotionEvent.ACTION_UP:
... ...
break
;
}
return
super
.onTouchEvent(ev);
}
//重复贴出下面这段需要注意的代码
@Override
public
void
onScroll(AbsListView view,
int
firstVisibleItem,
int
visibleItemCount,
int
totalItemCount) {
... ...
else
if
(mCurrentScrollState == SCROLL_STATE_TOUCH_SCROLL
&& firstVisibleItem ==
0
&& (mHeaderLinearLayout.getBottom() >= mHeaderHeight)) {
//下拉达到界限,进入松手刷新状态
if
(mPullRefreshState == ENTER_PULL_REFRESH || mPullRefreshState == NONE_PULL_REFRESH) {
mPullRefreshState = OVER_PULL_REFRESH;
mDownY = mMoveY;
//为下拉1/3折扣效果记录开始位置
mHeaderTextView.setText(
"松手刷新"
);
//显示松手刷新
mHeaderPullDownImageView.setVisibility(View.GONE);
//隐藏"下拉刷新"
mHeaderReleaseDownImageView.setVisibility(View.VISIBLE);
//显示向上的箭头
}
}
... ...
}
|
7. 反弹效果的特殊处理
松手后我们需要一个柔性的反弹效果,意味着我们弹回去的过程需要分一步步走,我的解决方案是:
在子线程里计算PaddingTop,并减少到原来的3/4,循环通知主线程,直到PaddingTop小于1(这个值取一个小值,合适即可)。
松手后,当然是在onTouchEvent的ACTION_UP条件下处理比较方便:
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
|
//因为涉及到handler数据处理,为方便我们定义如下常量
private
final
static
int
REFRESH_BACKING =
0
;
//反弹中
private
final
static
int
REFRESH_BACED =
1
;
//达到刷新界限,反弹结束后
private
final
static
int
REFRESH_RETURN =
2
;
//没有达到刷新界限,返回
private
final
static
int
REFRESH_DONE =
3
;
//加载数据结束
@Override
public
boolean
onTouchEvent(MotionEvent ev) {
switch
(ev.getAction()) {
... ...
case
MotionEvent.ACTION_UP:
//when you action up, it will do these:
//1. roll back util header topPadding is 0
//2. hide the header by setSelection(1)
if
(mPullRefreshState == OVER_PULL_REFRESH || mPullRefreshState == ENTER_PULL_REFRESH) {
new
Thread() {
public
void
run() {
Message msg;
while
(mHeaderLinearLayout.getPaddingTop() >
1
) {
msg = mHandler.obtainMessage();
msg.what = REFRESH_BACKING;
mHandler.sendMessage(msg);
try
{
sleep(
5
);
//慢一点反弹,别一下子就弹回去了
}
catch
(InterruptedException e) {
e.printStackTrace();
}
}
msg = mHandler.obtainMessage();
if
(mPullRefreshState == OVER_PULL_REFRESH) {
msg.what = REFRESH_BACED;
//加载数据完成,结束返回
}
else
{
msg.what = REFRESH_RETURN;
//未达到刷新界限,直接返回
}
mHandler.sendMessage(msg);
};
}.start();
}
break
;
}
return
super
.onTouchEvent(ev);
}
private
Handler mHandler =
new
Handler(){
@Override
public
void
handleMessage(Message msg) {
switch
(msg.what) {
case
REFRESH_BACKING:
mHeaderLinearLayout.setPadding(mHeaderLinearLayout.getPaddingLeft(),
(
int
) (mHeaderLinearLayout.getPaddingTop()*
0
.75f),
mHeaderLinearLayout.getPaddingRight(),
mHeaderLinearLayout.getPaddingBottom());
break
;
case
REFRESH_BACED:
mHeaderTextView.setText(
"正在加载..."
);
mHeaderProgressBar.setVisibility(View.VISIBLE);
mHeaderPullDownImageView.setVisibility(View.GONE);
mHeaderReleaseDownImageView.setVisibility(View.GONE);
mPullRefreshState = EXIT_PULL_REFRESH;
new
Thread() {
public
void
run() {
sleep(
2000
);
//处理后台加载数据
Message msg = mHandler.obtainMessage();
msg.what = REFRESH_DONE;
//通知主线程加载数据完成
mHandler.sendMessage(msg);
};
}.start();
break
;
case
REFRESH_RETURN:
//未达到刷新界限,返回
mHeaderTextView.setText(
"下拉刷新"
);
mHeaderProgressBar.setVisibility(View.INVISIBLE);
mHeaderPullDownImageView.setVisibility(View.VISIBLE);
mHeaderReleaseDownImageView.setVisibility(View.GONE);
mHeaderLinearLayout.setPadding(mHeaderLinearLayout.getPaddingLeft(),
0
,
mHeaderLinearLayout.getPaddingRight(),
mHeaderLinearLayout.getPaddingBottom());
mPullRefreshState = NONE_PULL_REFRESH;
setSelection(
1
);
break
;
case
REFRESH_DONE:
//刷新结束后,恢复原始默认状态
mHeaderTextView.setText(
"下拉刷新"
);
mHeaderProgressBar.setVisibility(View.INVISIBLE);
mHeaderPullDownImageView.setVisibility(View.VISIBLE);
mHeaderReleaseDownImageView.setVisibility(View.GONE);
mHeaderUpdateText.setText(getContext().getString(R.string.app_list_header_refresh_last_update,
mSimpleDateFormat.format(
new
Date())));
mHeaderLinearLayout.setPadding(mHeaderLinearLayout.getPaddingLeft(),
0
,
mHeaderLinearLayout.getPaddingRight(),
mHeaderLinearLayout.getPaddingBottom());
mPullRefreshState = NONE_PULL_REFRESH;
setSelection(
1
);
break
;
default
:
break
;
}
}
};
|
8. 切入数据加载过程
上面数据后台处理我们用sleep(2000)来处理,实际处理中,作为公共组件,我们也不好把具体代码直接写在这里,我们需要一个更灵活的分离:
(1). 定义接口
(2). 注入接口
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
|
//定义接口
public
interface
RefreshListener {
Object refreshing();
//加载数据
void
refreshed(Object obj);
//外部可扩展加载完成后的操作
}
//注入接口
private
Object mRefreshObject =
null
;
//传值
private
RefreshListener mRefreshListener =
null
;
public
void
setOnRefreshListener(RefreshListener refreshListener) {
this
.mRefreshListener = refreshListener;
}
//我们需要重写上面的mHandler如下代码
case
REFRESH_BACED:
... ...
new
Thread() {
public
void
run() {
if
(mRefreshListener !=
null
) {
mRefreshObject = mRefreshListener.refreshing();
}
Message msg = mHandler.obtainMessage();
msg.what = REFRESH_DONE;
mHandler.sendMessage(msg);
};
}.start();
break
;
case
REFRESH_DONE:
... ...
mPullRefreshState = NONE_PULL_REFRESH;
setSelection(
1
);
if
(mRefreshListener !=
null
) {
mRefreshListener.refreshed(mRefreshObject);
}
break
;
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public
xxx
implements
RefreshListener{
@Override
|