Android自定义控件下拉刷新实例代码

来源:互联网 发布:战斗妖精雪风小说淘宝 编辑:程序博客网 时间:2024/05/18 03:28

实现效果:

图片素材:

--> 首先, 写先下拉刷新时的刷新布局 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 extendsLinearLayout implementsView.OnTouchListener {
privatestatic 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);// 刷新完成或未刷新状态
privateint status; // 状态
PULL_STATUS(intvalue) {
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 的键值
privatestatic final String UPDATED_AT = "updated_at";
// 下拉刷新的回调接口
privatePullToRefreshListener mListener;
privateSharedPreferences preferences; // 用于存储上次更新时间
privateView header; // 下拉头的View
privateListView listView; // 需要去下拉刷新的ListView
privateProgressBar progressBar; // 刷新时显示的进度条
privateImageView arrow; // 指示下拉和释放的箭头
privateTextView description; // 指示下拉和释放的文字描述
privateTextView updateAt; // 上次更新时间的文字描述
privateMarginLayoutParams headerLayoutParams; // 下拉头的布局参数
privatelong lastUpdateTime; // 上次更新时间的毫秒值
// 为了防止不同界面的下拉刷新在上次更新时间上互相有冲突,使用id来做区分
privateint mId = -1;
privateint hideHeaderHeight; // 下拉头的高度
/**
* 当前处理什么状态,可选值有 STATUS_PULL_TO_REFRESH, STATUS_RELEASE_TO_REFRESH, STATUS_REFRESHING 和 STATUS_REFRESH_FINISHED
*/
privatePULL_STATUS currentStatus = PULL_STATUS.STATUS_REFRESH_FINISHED;
// 记录上一次的状态是什么,避免进行重复操作
privatePULL_STATUS lastStatus = currentStatus;
privatefloat yDown; // 手指按下时的屏幕纵坐标
privateint touchSlop; // 在被判定为滚动之前用户手指可以移动的最大值。
privateboolean loadOnce; // 是否已加载过一次layout,这里onLayout中的初始化只需加载一次
privateboolean 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
protectedvoid onLayout(booleanchanged, int l, int t, int r, intb) {
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
publicboolean 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 轴的位置
floatyMove = event.getRawY();
// 获取从按下到移动过程中移动的距离
int distance = (int) (yMove - yDown);
// 如果手指是上滑状态,并且下拉头是完全隐藏的,就屏蔽下拉事件
if (distance <= 0 && headerLayoutParams.topMargin <= hideHeaderHeight) {
returnfalse;
}
if (distance < touchSlop) {
returnfalse;
}
// 判断是否已经在刷新状态
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();
} elseif (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 的滚动事件
returntrue;
}
}
returnfalse;
}
/**
* 给下拉刷新控件注册一个监听器
*
* @param listener 监听器的实现
* @param id 为了防止不同界面的下拉刷新在上次更新时间上互相有冲突,不同界面在注册下拉刷新监听器时一定要传入不同的 id
*/
publicvoid setOnRefreshListener(PullToRefreshListener listener,int id) {
mListener = listener;
mId = id;
}
/**
* 当所有的刷新逻辑完成后,记录调用一下,否则你的 ListView 将一直处于正在刷新状态
*/
publicvoid finishRefreshing() {
currentStatus = PULL_STATUS.STATUS_REFRESH_FINISHED;
preferences.edit().putLong(UPDATED_AT + mId, System.currentTimeMillis()).commit();
new HideHeaderTask().execute();
}
/**
* 根据当前 ListView 的滚动状态来设定 {@link #ableToPull}
* 的值,每次都需要在 onTouch 中第一个执行,这样可以判断出当前应该是滚动 ListView,还是应该进行下拉
*/
privatevoid 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;
}
}
/**
* 更新下拉头中的信息
*/
privatevoid 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();
} elseif (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();
} elseif (currentStatus == PULL_STATUS.STATUS_REFRESHING) {
description.setText(getResources().getString(R.string.refreshing));
progressBar.setVisibility(View.VISIBLE);
arrow.clearAnimation();
arrow.setVisibility(View.GONE);
}
refreshUpdatedAtValue();
}
}
/**
* 根据当前的状态来旋转箭头
*/
privatevoid rotateArrow() {
floatpivotX = arrow.getWidth() / 2f;
floatpivotY = arrow.getHeight() / 2f;
floatfromDegrees = 0f;
floattoDegrees = 0f;
if (currentStatus == PULL_STATUS.STATUS_PULL_TO_REFRESH) {
fromDegrees = 180f;
toDegrees = 360f;
} elseif (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);
}
/**
* 刷新下拉头中上次更新时间的文字描述
*/
privatevoid 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);
} elseif (timePassed < 0) {
updateAtValue = getResources().getString(R.string.time_error);
} elseif (timePassed < ONE_MINUTE) {
updateAtValue = getResources().getString(R.string.updated_just_now);
} elseif (timePassed < ONE_HOUR) {
timeIntoFormat = timePassed / ONE_MINUTE;
String value = timeIntoFormat +"分钟";
updateAtValue = String.format(getResources().getString(R.string.updated_at), value);
} elseif (timePassed < ONE_DAY) {
timeIntoFormat = timePassed / ONE_HOUR;
String value = timeIntoFormat +"小时";
updateAtValue = String.format(getResources().getString(R.string.updated_at), value);
} elseif (timePassed < ONE_MONTH) {
timeIntoFormat = timePassed / ONE_DAY;
String value = timeIntoFormat +"天";
updateAtValue = String.format(getResources().getString(R.string.updated_at), value);
} elseif (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);
}
/**
* 正在刷新的任务,在此任务中会去回调注册进来的下拉刷新监听器
*/
classRefreshingTask extendsAsyncTask<Void, Integer, Void> {
@Override
protectedVoid 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();
}
returnnull;
}
@Override
protectedvoid onProgressUpdate(Integer... topMargin) {
updateHeaderView();
headerLayoutParams.topMargin = topMargin[0];
header.setLayoutParams(headerLayoutParams);
}
}
/**
* 隐藏下拉头的任务,当未进行下拉刷新或下拉刷新完成后,此任务将会使下拉头重新隐藏
*/
classHideHeaderTask extendsAsyncTask<Void, Integer, Integer> {
@Override
protectedInteger doInBackground(Void... params) {
int topMargin = headerLayoutParams.topMargin;
while(true) {
topMargin = topMargin + SCROLL_SPEED;
if (topMargin <= hideHeaderHeight) {
topMargin = hideHeaderHeight;
break;
}
publishProgress(topMargin);
SystemClock.sleep(10);
}
returntopMargin;
}
@Override
protectedvoid onProgressUpdate(Integer ... topMargin) {
headerLayoutParams.topMargin = topMargin[0];
header.setLayoutParams(headerLayoutParams);
}
@Override
protectedvoid onPostExecute(Integer topMargin) {
headerLayoutParams.topMargin = topMargin;
header.setLayoutParams(headerLayoutParams);
currentStatus = PULL_STATUS.STATUS_REFRESH_FINISHED;
}
}
/**
* 下拉刷新的监听器,使用下拉刷新的地方应该注册此监听器来获取刷新回调
*/
publicinterface 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"
  xmlns:tools="http://schemas.android.com/tools"
  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 extendsAppCompatActivity {
  RefreshView refreshableView;
  ListView listView;
  ArrayAdapter<String> adapter;
  privateWebView webView;
  privatestatic int NUM = 30;
  String[] items =new String[NUM];
  @Override
  protectedvoid 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(newRefreshView.PullToRefreshListener() {
      @Override
      publicvoid onRefresh() {
        SystemClock.sleep(3000);
        refreshableView.finishRefreshing();
      }
    },0);
  }
}

程序 Demo: 链接:http://pan.baidu.com/s/1ge6Llw3 密码:skna

原创粉丝点击