android中listView下拉刷新

转自:http://www.cnblogs.com/xiaoran1129/archive/2012/07/04/2576221.html

 Android的ListView是应用最广的一个组件,功能强大,扩展性灵活(不局限于ListView本身一个类),前面的文章有介绍分组,拖拽,3D立体,游标,圆角,而今天我们要介绍的是另外一个扩展ListView:下拉刷新的ListView。
    下拉刷新界面最初流行于iphone应用界面,如图:

android中listView下拉刷新_第1张图片
    然后在Android中也逐渐被应用,比如微博,资讯类。
    所以,今天要实现的结果应该也是类似的,先贴出最终完成效果,如下图,接下来我们一步一步实现。

android中listView下拉刷新_第2张图片

 

1. 流程分析
    下拉刷新最主要的流程是:
    (1). 下拉,显示提示头部界面(HeaderView),这个过程提示用户"下拉刷新"
    (2). 下拉到一定程度,超出了刷新最基本的下拉界限,我们认为达到了刷新的条件,提示用户可以"松手刷新"了,效果上允许用户继续下拉
    (3). 用户松手,可能用户下拉远远不止提示头部界面,所以这一步,先反弹回仅显示提示头部界面,然后提示用户"正在加载"。
    (4). 加载完成后,隐藏提示头部界面。
    示意图如下:

android中listView下拉刷新_第3张图片->android中listView下拉刷新_第4张图片->android中listView下拉刷新_第5张图片

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 >
    代码中在构造函数中添加init()方法加载如下:
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();
}
  后面我们就会用到这个mHeaderHeight.

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); //显示向上的箭头
         }
     }
     ... ...
}
  onScroll里监听到了进入松手刷新状态,onTouchEvent就开始在ACTION_MOVE中处理1/3折扣问题。

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 ;
    在其他地方我们就可以不修改这个listview组件的代码,使用如下:
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

你可能感兴趣的:(android中listView下拉刷新)