Android自定义控件下拉刷新实例代码
实现效果:
图片素材:
--> 首先, 写先下拉刷新时的刷新布局 pull_to_refresh.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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
|
<resources>
<string name=
"app_name"
>PullToRefreshTest</string>
<string name=
"pull_to_refresh"
>下拉可以刷新</string>
<string name=
"release_to_refresh"
>释放立即刷新</string>
<string name=
"refreshing"
>正在刷新...</string>
<string name=
"not_updated_yet"
>暂未更新过</string>
<string name=
"updated_at"
>上次更新于%1$s前</string>
<string name=
"updated_just_now"
>刚刚更新</string>
<string name=
"time_error"
>时间有问题</string>
</resources>
<?xml version=
"1.0"
encoding=
"utf-8"
?>
<RelativeLayout xmlns:android=
"http://schemas.android.com/apk/res/android"
android:id=
"@+id/pull_to_refresh_head"
android:layout_width=
"match_parent"
android:layout_height=
"60dp"
>
<LinearLayout
android:layout_width=
"200dp"
android:layout_height=
"60dp"
android:layout_centerInParent=
"true"
android:orientation=
"horizontal"
>
<RelativeLayout
android:layout_width=
"0dp"
android:layout_height=
"60dp"
android:layout_weight=
"3"
>
<ImageView
android:id=
"@+id/arrow"
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:layout_centerInParent=
"true"
android:src=
"@mipmap/indicator_arrow"
/>
<ProgressBar
android:id=
"@+id/progress_bar"
android:layout_width=
"30dp"
android:layout_height=
"30dp"
android:layout_centerInParent=
"true"
android:visibility=
"gone"
/>
</RelativeLayout>
<LinearLayout
android:layout_width=
"0dp"
android:layout_height=
"60dp"
android:layout_weight=
"12"
android:orientation=
"vertical"
>
<TextView
android:id=
"@+id/description"
android:layout_width=
"match_parent"
android:layout_height=
"0dp"
android:layout_weight=
"1"
android:gravity=
"center_horizontal|bottom"
android:text=
"@string/pull_to_refresh"
/>
<TextView
android:id=
"@+id/updated_at"
android:layout_width=
"match_parent"
android:layout_height=
"0dp"
android:layout_weight=
"1"
android:gravity=
"center_horizontal|top"
android:text=
"@string/updated_at"
/>
</LinearLayout>
</LinearLayout>
</RelativeLayout>
|
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
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
|
strings
pull_to_refresh
--> 然后, 也是主要的, 自定义下拉刷新的 View (包含下拉刷新所有操作) RefreshView.java:
package
com.dragon.android.tofreshlayout;
import
android.content.Context;
import
android.content.SharedPreferences;
import
android.os.AsyncTask;
import
android.os.SystemClock;
import
android.preference.PreferenceManager;
import
android.util.AttributeSet;
import
android.view.LayoutInflater;
import
android.view.MotionEvent;
import
android.view.View;
import
android.view.ViewConfiguration;
import
android.view.animation.RotateAnimation;
import
android.widget.ImageView;
import
android.widget.LinearLayout;
import
android.widget.ListView;
import
android.widget.ProgressBar;
import
android.widget.TextView;
public
class
RefreshView
extends
LinearLayout
implements
View.OnTouchListener {
private
static
final
String TAG = RefreshView.
class
.getSimpleName();
public
enum
PULL_STATUS {
STATUS_PULL_TO_REFRESH(
0
),
// 下拉状态
STATUS_RELEASE_TO_REFRESH(
1
),
// 释放立即刷新状态
STATUS_REFRESHING(
2
),
// 正在刷新状态
STATUS_REFRESH_FINISHED(
3
);
// 刷新完成或未刷新状态
private
int
status;
// 状态
PULL_STATUS(
int
value) {
this
.status = value;
}
public
int
getValue() {
return
this
.status;
}
}
// 下拉头部回滚的速度
public
static
final
int
SCROLL_SPEED = -
20
;
// 一分钟的毫秒值,用于判断上次的更新时间
public
static
final
long
ONE_MINUTE =
60
*
1000
;
// 一小时的毫秒值,用于判断上次的更新时间
public
static
final
long
ONE_HOUR =
60
* ONE_MINUTE;
// 一天的毫秒值,用于判断上次的更新时间
public
static
final
long
ONE_DAY =
24
* ONE_HOUR;
// 一月的毫秒值,用于判断上次的更新时间
public
static
final
long
ONE_MONTH =
30
* ONE_DAY;
// 一年的毫秒值,用于判断上次的更新时间
public
static
final
long
ONE_YEAR =
12
* ONE_MONTH;
// 上次更新时间的字符串常量,用于作为 SharedPreferences 的键值
private
static
final
String UPDATED_AT =
"updated_at"
;
// 下拉刷新的回调接口
private
PullToRefreshListener mListener;
private
SharedPreferences preferences;
// 用于存储上次更新时间
private
View header;
// 下拉头的View
private
ListView listView;
// 需要去下拉刷新的ListView
private
ProgressBar progressBar;
// 刷新时显示的进度条
private
ImageView arrow;
// 指示下拉和释放的箭头
private
TextView description;
// 指示下拉和释放的文字描述
private
TextView updateAt;
// 上次更新时间的文字描述
private
MarginLayoutParams headerLayoutParams;
// 下拉头的布局参数
private
long
lastUpdateTime;
// 上次更新时间的毫秒值
// 为了防止不同界面的下拉刷新在上次更新时间上互相有冲突,使用id来做区分
private
int
mId = -
1
;
private
int
hideHeaderHeight;
// 下拉头的高度
/**
* 当前处理什么状态,可选值有 STATUS_PULL_TO_REFRESH, STATUS_RELEASE_TO_REFRESH, STATUS_REFRESHING 和 STATUS_REFRESH_FINISHED
*/
private
PULL_STATUS currentStatus = PULL_STATUS.STATUS_REFRESH_FINISHED;
// 记录上一次的状态是什么,避免进行重复操作
private
PULL_STATUS lastStatus = currentStatus;
private
float
yDown;
// 手指按下时的屏幕纵坐标
private
int
touchSlop;
// 在被判定为滚动之前用户手指可以移动的最大值。
private
boolean
loadOnce;
// 是否已加载过一次layout,这里onLayout中的初始化只需加载一次
private
boolean
ableToPull;
// 当前是否可以下拉,只有ListView滚动到头的时候才允许下拉
/**
* 下拉刷新控件的构造函数,会在运行时动态添加一个下拉头的布局
*/
public
RefreshView(Context context, AttributeSet attrs) {
super
(context, attrs);
preferences = PreferenceManager.getDefaultSharedPreferences(context);
header = LayoutInflater.from(context).inflate(R.layout.pull_to_refresh,
null
,
true
);
progressBar = (ProgressBar) header.findViewById(R.id.progress_bar);
arrow = (ImageView) header.findViewById(R.id.arrow);
description = (TextView) header.findViewById(R.id.description);
updateAt = (TextView) header.findViewById(R.id.updated_at);
touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
refreshUpdatedAtValue();
setOrientation(VERTICAL);
addView(header,
0
);
//Log.d(TAG, "RefreshView Constructor() getChildAt(0): " + getChildAt(0));
//Log.d(TAG, "RefreshView Constructor() getChildAt(0): " + getChildAt(1));
// listView = (ListView) getChildAt(1);
// listView.setOnTouchListener(this);
}
/**
* 进行一些关键性的初始化操作,比如:将下拉头向上偏移进行隐藏,给 ListView 注册 touch 事件
*/
@Override
protected
void
onLayout(
boolean
changed,
int
l,
int
t,
int
r,
int
b) {
super
.onLayout(changed, l, t, r, b);
if
(changed && !loadOnce) {
hideHeaderHeight = -header.getHeight();
headerLayoutParams = (MarginLayoutParams) header.getLayoutParams();
headerLayoutParams.topMargin = hideHeaderHeight;
listView = (ListView) getChildAt(
1
);
//Log.d(TAG, "onLayout() getChildAt(0): " + getChildAt(0));
//Log.d(TAG, "onLayout() listView: " + listView);
listView.setOnTouchListener(
this
);
loadOnce =
true
;
}
}
/**
* 当 ListView 被触摸时调用,其中处理了各种下拉刷新的具体逻辑
*/
@Override
public
boolean
onTouch(View v, MotionEvent event) {
setCanAbleToPull(event);
// 判断是否可以下拉
if
(ableToPull) {
switch
(event.getAction()) {
case
MotionEvent.ACTION_DOWN:
yDown = event.getRawY();
break
;
case
MotionEvent.ACTION_MOVE:
// 获取移动中的 Y 轴的位置
float
yMove = event.getRawY();
// 获取从按下到移动过程中移动的距离
int
distance = (
int
) (yMove - yDown);
// 如果手指是上滑状态,并且下拉头是完全隐藏的,就屏蔽下拉事件
if
(distance <=
0
&& headerLayoutParams.topMargin <= hideHeaderHeight) {
return
false
;
}
if
(distance < touchSlop) {
return
false
;
}
// 判断是否已经在刷新状态
if
(currentStatus != PULL_STATUS.STATUS_REFRESHING) {
// 判断设置的 topMargin 是否 > 0, 默认初始设置为 -header.getHeight()
if
(headerLayoutParams.topMargin >
0
) {
currentStatus = PULL_STATUS.STATUS_RELEASE_TO_REFRESH;
}
else
{
// 否则状态为下拉中的状态
currentStatus = PULL_STATUS.STATUS_PULL_TO_REFRESH;
}
// 通过偏移下拉头的 topMargin 值,来实现下拉效果
headerLayoutParams.topMargin = (distance /
2
) + hideHeaderHeight;
header.setLayoutParams(headerLayoutParams);
}
break
;
case
MotionEvent.ACTION_UP:
default
:
if
(currentStatus == PULL_STATUS.STATUS_RELEASE_TO_REFRESH) {
// 松手时如果是释放立即刷新状态,就去调用正在刷新的任务
new
RefreshingTask().execute();
}
else
if
(currentStatus == PULL_STATUS.STATUS_PULL_TO_REFRESH) {
// 松手时如果是下拉状态,就去调用隐藏下拉头的任务
new
HideHeaderTask().execute();
}
break
;
}
// 时刻记得更新下拉头中的信息
if
(currentStatus == PULL_STATUS.STATUS_PULL_TO_REFRESH
|| currentStatus == PULL_STATUS.STATUS_RELEASE_TO_REFRESH) {
updateHeaderView();
// 当前正处于下拉或释放状态,要让 ListView 失去焦点,否则被点击的那一项会一直处于选中状态
listView.setPressed(
false
);
listView.setFocusable(
false
);
listView.setFocusableInTouchMode(
false
);
lastStatus = currentStatus;
// 当前正处于下拉或释放状态,通过返回 true 屏蔽掉 ListView 的滚动事件
return
true
;
}
}
return
false
;
}
/**
* 给下拉刷新控件注册一个监听器
*
* @param listener 监听器的实现
* @param id 为了防止不同界面的下拉刷新在上次更新时间上互相有冲突,不同界面在注册下拉刷新监听器时一定要传入不同的 id
*/
public
void
setOnRefreshListener(PullToRefreshListener listener,
int
id) {
mListener = listener;
mId = id;
}
/**
* 当所有的刷新逻辑完成后,记录调用一下,否则你的 ListView 将一直处于正在刷新状态
*/
public
void
finishRefreshing() {
currentStatus = PULL_STATUS.STATUS_REFRESH_FINISHED;
preferences.edit().putLong(UPDATED_AT + mId, System.currentTimeMillis()).commit();
new
HideHeaderTask().execute();
}
/**
* 根据当前 ListView 的滚动状态来设定 {@link #ableToPull}
* 的值,每次都需要在 onTouch 中第一个执行,这样可以判断出当前应该是滚动 ListView,还是应该进行下拉
*/
private
void
setCanAbleToPull(MotionEvent event) {
View firstChild = listView.getChildAt(
0
);
if
(firstChild !=
null
) {
// 获取 ListView 中第一个Item的位置
int
firstVisiblePos = listView.getFirstVisiblePosition();
// 判断第一个子控件的 Top 是否和第一个 Item 位置相等
if
(firstVisiblePos ==
0
&& firstChild.getTop() ==
0
) {
if
(!ableToPull) {
// getRawY() 获得的是相对屏幕 Y 方向的位置
yDown = event.getRawY();
}
// 如果首个元素的上边缘,距离父布局值为 0,就说明 ListView 滚动到了最顶部,此时应该允许下拉刷新
ableToPull =
true
;
}
else
{
if
(headerLayoutParams.topMargin != hideHeaderHeight) {
headerLayoutParams.topMargin = hideHeaderHeight;
header.setLayoutParams(headerLayoutParams);
}
ableToPull =
false
;
}
}
else
{
// 如果 ListView 中没有元素,也应该允许下拉刷新
ableToPull =
true
;
}
}
/**
* 更新下拉头中的信息
*/
private
void
updateHeaderView() {
if
(lastStatus != currentStatus) {
if
(currentStatus == PULL_STATUS.STATUS_PULL_TO_REFRESH) {
description.setText(getResources().getString(R.string.pull_to_refresh));
arrow.setVisibility(View.VISIBLE);
progressBar.setVisibility(View.GONE);
rotateArrow();
}
else
if
(currentStatus == PULL_STATUS.STATUS_RELEASE_TO_REFRESH) {
description.setText(getResources().getString(R.string.release_to_refresh));
arrow.setVisibility(View.VISIBLE);
progressBar.setVisibility(View.GONE);
rotateArrow();
}
else
if
(currentStatus == PULL_STATUS.STATUS_REFRESHING) {
description.setText(getResources().getString(R.string.refreshing));
progressBar.setVisibility(View.VISIBLE);
arrow.clearAnimation();
arrow.setVisibility(View.GONE);
}
refreshUpdatedAtValue();
}
}
/**
* 根据当前的状态来旋转箭头
*/
private
void
rotateArrow() {
float
pivotX = arrow.getWidth() / 2f;
float
pivotY = arrow.getHeight() / 2f;
float
fromDegrees = 0f;
float
toDegrees = 0f;
if
(currentStatus == PULL_STATUS.STATUS_PULL_TO_REFRESH) {
fromDegrees = 180f;
toDegrees = 360f;
}
else
if
(currentStatus == PULL_STATUS.STATUS_RELEASE_TO_REFRESH) {
fromDegrees = 0f;
toDegrees = 180f;
}
RotateAnimation animation =
new
RotateAnimation(fromDegrees, toDegrees, pivotX, pivotY);
animation.setDuration(
100
);
animation.setFillAfter(
true
);
arrow.startAnimation(animation);
}
/**
* 刷新下拉头中上次更新时间的文字描述
*/
private
void
refreshUpdatedAtValue() {
lastUpdateTime = preferences.getLong(UPDATED_AT + mId, -
1
);
long
currentTime = System.currentTimeMillis();
long
timePassed = currentTime - lastUpdateTime;
long
timeIntoFormat;
String updateAtValue;
if
(lastUpdateTime == -
1
) {
updateAtValue = getResources().getString(R.string.not_updated_yet);
}
else
if
(timePassed <
0
) {
updateAtValue = getResources().getString(R.string.time_error);
}
else
if
(timePassed < ONE_MINUTE) {
updateAtValue = getResources().getString(R.string.updated_just_now);
}
else
if
(timePassed < ONE_HOUR) {
timeIntoFormat = timePassed / ONE_MINUTE;
String value = timeIntoFormat +
"分钟"
;
updateAtValue = String.format(getResources().getString(R.string.updated_at), value);
}
else
if
(timePassed < ONE_DAY) {
timeIntoFormat = timePassed / ONE_HOUR;
String value = timeIntoFormat +
"小时"
;
updateAtValue = String.format(getResources().getString(R.string.updated_at), value);
}
else
if
(timePassed < ONE_MONTH) {
timeIntoFormat = timePassed / ONE_DAY;
String value = timeIntoFormat +
"天"
;
updateAtValue = String.format(getResources().getString(R.string.updated_at), value);
}
else
if
(timePassed < ONE_YEAR) {
timeIntoFormat = timePassed / ONE_MONTH;
String value = timeIntoFormat +
"个月"
;
updateAtValue = String.format(getResources().getString(R.string.updated_at), value);
}
else
{
timeIntoFormat = timePassed / ONE_YEAR;
String value = timeIntoFormat +
"年"
;
updateAtValue = String.format(getResources().getString(R.string.updated_at), value);
}
updateAt.setText(updateAtValue);
}
/**
* 正在刷新的任务,在此任务中会去回调注册进来的下拉刷新监听器
*/
class
RefreshingTask
extends
AsyncTask<Void, Integer, Void> {
@Override
protected
Void doInBackground(Void... params) {
int
topMargin = headerLayoutParams.topMargin;
while
(
true
) {
topMargin = topMargin + SCROLL_SPEED;
if
(topMargin <=
0
) {
topMargin =
0
;
break
;
}
publishProgress(topMargin);
SystemClock.sleep(
10
);
}
currentStatus = PULL_STATUS.STATUS_REFRESHING;
publishProgress(
0
);
if
(mListener !=
null
) {
mListener.onRefresh();
}
return
null
;
}
@Override
protected
void
onProgressUpdate(Integer... topMargin) {
updateHeaderView();
headerLayoutParams.topMargin = topMargin[
0
];
header.setLayoutParams(headerLayoutParams);
}
}
/**
* 隐藏下拉头的任务,当未进行下拉刷新或下拉刷新完成后,此任务将会使下拉头重新隐藏
*/
class
HideHeaderTask
extends
AsyncTask<Void, Integer, Integer> {
@Override
protected
Integer doInBackground(Void... params) {
int
topMargin = headerLayoutParams.topMargin;
while
(
true
) {
topMargin = topMargin + SCROLL_SPEED;
if
(topMargin <= hideHeaderHeight) {
topMargin = hideHeaderHeight;
break
;
}
publishProgress(topMargin);
SystemClock.sleep(
10
);
}
return
topMargin;
}
@Override
protected
void
onProgressUpdate(Integer ... topMargin) {
headerLayoutParams.topMargin = topMargin[
0
];
header.setLayoutParams(headerLayoutParams);
}
@Override
protected
void
onPostExecute(Integer topMargin) {
headerLayoutParams.topMargin = topMargin;
header.setLayoutParams(headerLayoutParams);
currentStatus = PULL_STATUS.STATUS_REFRESH_FINISHED;
}
}
/**
* 下拉刷新的监听器,使用下拉刷新的地方应该注册此监听器来获取刷新回调
*/
public
interface
PullToRefreshListener {
// 刷新时会去回调此方法,在方法内编写具体的刷新逻辑。注意此方法是在子线程中调用的, 可以不必另开线程来进行耗时操作
void
onRefresh();
}
}
|
--> 第三步, 写主布局:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
<?xml version=
"1.0"
encoding=
"utf-8"
?>
<RelativeLayout xmlns:android=
"http://schemas.android.com/apk/res/android"
android:layout_width=
"match_parent"
android:layout_height=
"match_parent"
tools:context=
".MainActivity"
>
<com.dragon.android.tofreshlayout.RefreshView
android:id=
"@+id/refreshable_view"
android:layout_width=
"match_parent"
android:layout_height=
"match_parent"
>
<ListView
android:id=
"@+id/list_view"
android:layout_width=
"match_parent"
android:layout_height=
"match_parent"
>
</ListView>
</com.dragon.android.tofreshlayout.RefreshView>
</RelativeLayout>
|
--> 最后, Java 代码添加 ListView 的数据:
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
|
package
com.dragon.android.tofreshlayout;
import
android.os.Bundle;
import
android.os.SystemClock;
import
android.support.v7.app.AppCompatActivity;
import
android.webkit.WebView;
import
android.widget.ArrayAdapter;
import
android.widget.ListView;
public
class
MainActivity
extends
AppCompatActivity {
RefreshView refreshableView;
ListView listView;
ArrayAdapter<String> adapter;
private
WebView webView;
private
static
int
NUM =
30
;
String[] items =
new
String[NUM];
@Override
protected
void
onCreate(Bundle savedInstanceState) {
super
.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
getSupportActionBar().hide();
for
(
int
i =
0
; i < items.length; i++) {
items[i] =
"列表项"
+ i;
}
refreshableView = (RefreshView) findViewById(R.id.refreshable_view);
listView = (ListView) findViewById(R.id.list_view);
adapter =
new
ArrayAdapter<>(
this
, android.R.layout.simple_list_item_1, items);
listView.setAdapter(adapter);
refreshableView.setOnRefreshListener(
new
RefreshView.PullToRefreshListener() {
@Override
public
void
onRefresh() {
SystemClock.sleep(
3000
);
refreshableView.finishRefreshing();
}
},
0
);
}
}
|
程序 Demo: 链接:http://pan.baidu.com/s/1ge6Llw3 密码:skna