android中listView下拉刷新


 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 >
    代碼中在構造函數中添加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
     protected  void  onCreate(Bundle savedInstanceState) {
         super .onCreate(savedInstanceState);
         //類似如下
         ((RefreshListView) listView).setOnRefreshListener( this );
     }
 
     @Override
     public  Object refreshing() {
         String result = null ;
         //result = FileUtils.readTextFile(file);
         return  result;
     }
 
     @Override
     public  void  refreshed(Object obj) {
         if  (obj != null ) {
            //擴展操作
         }
     };
}
  很方便了。

9. 擴展"更多"功能
    下拉刷新之外,我們也可以通過相同方法使用FooterView切入底部"更多"過程,這里我就不詳細說明了

10. 源碼
    上面的每段代碼都看做是"零部件",需要組合一下。
    因為我們上面實現了下拉刷新,還增加了"更多"功能,我們直接命名這個類為RefreshListView吧:

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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
package  com.tianxia.lib.baseworld.widget;
 
import  java.text.SimpleDateFormat;
import  java.util.Date;
 
import  android.content.Context;
import  android.os.Handler;
import  android.os.Message;
import  android.util.AttributeSet;
import  android.view.LayoutInflater;
import  android.view.MotionEvent;
import  android.view.View;
import  android.view.ViewGroup;
import  android.widget.AbsListView;
import  android.widget.AbsListView.OnScrollListener;
import  android.widget.ImageView;
import  android.widget.LinearLayout;
import  android.widget.ListAdapter;
import  android.widget.ListView;
import  android.widget.ProgressBar;
import  android.widget.TextView;
 
import  com.tianxia.lib.baseworld.R;
 
/**
  * 下拉刷新,底部更多
  *
  */
public  class  RefreshListView extends  ListView implements  OnScrollListener{
 
     private  float  mDownY;
     private  float  mMoveY;
 
     private  int  mHeaderHeight;
 
     private  int  mCurrentScrollState;
 
     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 ;                 //記錄刷新狀態
 
     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 ;         //加載數據結束
 
     private  LinearLayout mHeaderLinearLayout = null ;
     private  LinearLayout mFooterLinearLayout = null ;
     private  TextView mHeaderTextView = null ;
     private  TextView mHeaderUpdateText = null ;
     private  ImageView mHeaderPullDownImageView = null ;
     private  ImageView mHeaderReleaseDownImageView = null ;
     private  ProgressBar mHeaderProgressBar = null ;
     private  TextView mFooterTextView = null ;
     private  ProgressBar mFooterProgressBar = null ;
 
     private  SimpleDateFormat mSimpleDateFormat;
 
     private  Object mRefreshObject = null ;
     private  RefreshListener mRefreshListener = null ;
     public  void  setOnRefreshListener(RefreshListener refreshListener) {
         this .mRefreshListener = refreshListener;
     }
 
     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);
 
         mFooterLinearLayout = (LinearLayout) LayoutInflater.from(context).inflate(R.layout.refresh_list_footer, null );
         addFooterView(mFooterLinearLayout);
         mFooterProgressBar = (ProgressBar) findViewById(R.id.refresh_list_footer_progressbar);
         mFooterTextView = (TextView) mFooterLinearLayout.findViewById(R.id.refresh_list_footer_text);
         mFooterLinearLayout.setOnClickListener( new  OnClickListener() {
             @Override
             public  void  onClick(View v) {
                 if  (context.getString(R.string.app_list_footer_more).equals(mFooterTextView.getText())) {
                     mFooterTextView.setText(R.string.app_list_footer_loading);
                     mFooterProgressBar.setVisibility(View.VISIBLE);
                     if  (mRefreshListener != null ) {
                         mRefreshListener.more();
                     }
                 }
             }
         });
 
         setSelection( 1 );
         setOnScrollListener( this );
         measureView(mHeaderLinearLayout);
         mHeaderHeight = mHeaderLinearLayout.getMeasuredHeight();
 
         mSimpleDateFormat = new  SimpleDateFormat( "yyyy-MM-dd hh:mm" );
         mHeaderUpdateText.setText(context.getString(R.string.app_list_header_refresh_last_update, mSimpleDateFormat.format( new  Date())));
     }
 
     @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) {
                     mHeaderLinearLayout.setPadding(mHeaderLinearLayout.getPaddingLeft(),
                             ( int )((mMoveY - mDownY)/ 3 ),
                             mHeaderLinearLayout.getPaddingRight(),
                             mHeaderLinearLayout.getPaddingBottom());
                 }
                 break ;
             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);
     }
 
     @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; //為下拉1/3折扣效果記錄開始位置
                 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 );
             }
         }
     }
 
     @Override
     public  void  onScrollStateChanged(AbsListView view, int  scrollState) {
         mCurrentScrollState = scrollState;
     }
 
     @Override
     public  void  setAdapter(ListAdapter adapter) {
         super .setAdapter(adapter);
         setSelection( 1 );
     }
 
     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);
     }
 
     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() {
                         if  (mRefreshListener != null ) {
                             mRefreshObject = mRefreshListener.refreshing();
                         }
                         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 );
                 if  (mRefreshListener != null ) {
                     mRefreshListener.refreshed(mRefreshObject);
                 }
                 break ;
             default :
                 break ;
             }
         }
     };
     public  interface  RefreshListener {
         Object refreshing();
         void  refreshed(Object obj);
         void  more();
     }
 
     public  void  finishFootView() {
         mFooterProgressBar.setVisibility(View.GONE);
         mFooterTextView.setText(R.string.app_list_footer_more);
     }
 
     public  void  addFootView() {
         if  (getFooterViewsCount() == 0 ) {
             addFooterView(mFooterLinearLayout);
         }
     }
 
     public  void  removeFootView() {
         removeFooterView(mFooterLinearLayout);
     }
}
11.小結

    這個只是一個原型,無論代碼風格和邏輯處理,我覺得還有改進的空間,我會在后續逐漸改善的。
    以下是例子效果
    https://github.com/openproject/world/blob/master/baseworld/src/com/tianxia/lib/baseworld/widget/RefreshListView.java
    https://github.com/openproject/world/blob/master/healthworld/src/com/tianxia/app/healthworld/infomation/InfomationTabActivity.java
    期待有建設性的意見改善這個實現。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM