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    期待有建设性的意见改善这个实现。

android中listView下拉刷新的更多相关文章

  1. 【转载】Android中ListView下拉刷新的实现

    在网上看到一个下拉刷新的例子,很的很棒,转载和更多的人分享学习 原文:http://blog.csdn.net/loongggdroid/article/details/9385535 ListVie ...

  2. Android中ListView下拉刷新的实现

    ListView中的下拉刷新是非常常见的,也是经常使用的,看到有很多同学想要,那我就整理一下,供大家参考.那我就不解释,直接上代码了. 这里需要自己重写一下ListView,重写代码如下: packa ...

  3. Android中实现下拉刷新

    需求:项目中的消息列表界面要求实现类似sina微博的下拉刷新: 思路:一般的消息列表为ListView类型,将list加载到adapter中,再将adapter加载到 ListView中,从而实现消息 ...

  4. ListView下拉刷新、上拉载入更多之封装改进

    在Android中ListView下拉刷新.上拉载入更多示例一文中,Maxwin兄给出的控件比较强大,前面有详细介绍,但是有个不足就是,里面使用了一些资源文件,包括图片,String,layout,这 ...

  5. Android—自定义控件实现ListView下拉刷新

    这篇博客为大家介绍一个android常见的功能——ListView下拉刷新(参考自他人博客,网址忘记了,阅读他的代码自己理解注释的,希望能帮助到大家): 首先下拉未松手时候手机显示这样的界面: 下面的 ...

  6. Android ListView下拉刷新时卡的问题解决小技巧

    问题:ListView下拉刷新时看上去非常的卡 解决方案: 在BaseAdapter的getView方法中,有三个参数 public View getView(int position, View c ...

  7. ListView下拉刷新上拉加载更多实现

    这篇文章将带大家了解listview下拉刷新和上拉加载更多的实现过程,先看效果(注:图片中listview中的阴影可以加上属性android:fadingEdge="none"去掉 ...

  8. listview下拉刷新上拉加载扩展(二)-仿美团外卖

    经过前几篇的listview下拉刷新上拉加载讲解,相信你对其实现机制有了一个深刻的认识了吧,那么这篇文章我们来实现一个高级的listview下拉刷新上拉加载-仿新版美团外卖的袋鼠动画: 项目结构: 是 ...

  9. 自定义ListView下拉刷新上拉加载更多

    自定义ListView下拉刷新上拉加载更多 自定义RecyclerView下拉刷新上拉加载更多 Listview现在用的很少了,基本都是使用Recycleview,但是不得不说Listview具有划时 ...

随机推荐

  1. nginx 安装配置及使用 启动权限拒绝问题

    安装 yum install -y nginx 查看安装的路径 whereis nginx 可能会有所不同 需要根据自己的查看 执行目录:/usr/sbin/nginx 模块所在目录:/usr/lib ...

  2. gPRC基本介绍

    1.说明 gRPC英文全名为Google Remote Procedure Call, 即Google远程过程调用, 是Google发布的一个高性能.通用的开源RPC框架, 2.gRPC定义 gRPC ...

  3. .net core中Grpc使用报错:The remote certificate is invalid according to the validation procedure.

    因为Grpc采用HTTP/2作为通信协议,默认采用LTS/SSL加密方式传输,比如使用.net core启动一个服务端(被调用方)时: public static IHostBuilder Creat ...

  4. Lomsat gelral

    题目描述 You are given a rooted tree with root in vertex 11 . Each vertex is coloured in some colour. Le ...

  5. Selenium_使用execute_script执行JavaScript(11)

    selenium的包含的方法已能完全满足UI自动化,但是有些时候又不得不用到执行JS的情况,比如在一个富文本框中输入1W个字,使用send_keys方法将经历漫长的输入过程,如果换成使用JS的inne ...

  6. vue-cli axios封装(element-ui)

    1.http.js /** * * http配置 */ import axios from 'axios'import { Loading, Message } from 'element-ui' / ...

  7. stm32单片机利用ntc热敏电阻温度转换公式C语言版

    首先 我们需要明确电路结构 热敏电阻的原理就不再赘述,本文不凑字数,只讲干货 必须要知道的就是串联电阻R9程序中定义为resistanceInSeries ,精度越高越好 为了方便,先在程序中定义好你 ...

  8. 毫米转像素dpi

    public static double MillimeterToPixel_X(double length) //length是毫米,1厘米=10毫米 { System.Windows.Forms. ...

  9. [STM32F4xx 学习] SPI小结

    一.STM32F4xx系列的SPI特点: 1. 支持全双工的3线SPI模式(即SCK, MISO, MOSI) 2. 支持单工2线传输,同时数据线可以设置成单向或者双向模式 3. 8-bit, 16- ...

  10. [FatFs 学习] SD卡总结-SPI模式

    SD卡为移动设备提供了安全的,大容量存储解决方法.它本身可以通过两种总线模式和MCU进行数据传输,一种是称为SD BUS的4位串行数据模式,另一种就是大家熟知的4线SPI Bus模式.一些廉价,低端的 ...