实现效果:
图片素材:
--> 首先, 写先下拉刷新时的刷新布局 pull_to_refresh.xml:
1 <resources>
2 <string name="app_name">PullToRefreshTest</string>
3 <string name="pull_to_refresh">下拉可以刷新</string>
4 <string name="release_to_refresh">释放立即刷新</string>
5 <string name="refreshing">正在刷新...</string>
6 <string name="not_updated_yet">暂未更新过</string>
7 <string name="updated_at">上次更新于%1$s前</string>
8 <string name="updated_just_now">刚刚更新</string>
9 <string name="time_error">时间有问题</string>
10 </resources>
strings
1 <?xml version="1.0" encoding="utf-8"?>
2 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 android:id="@+id/pull_to_refresh_head"
4 android:layout_width="match_parent"
5 android:layout_height="60dp">
6
7 <LinearLayout
8 android:layout_width="200dp"
9 android:layout_height="60dp"
10 android:layout_centerInParent="true"
11 android:orientation="horizontal">
12
13 <RelativeLayout
14 android:layout_width="0dp"
15 android:layout_height="60dp"
16 android:layout_weight="3">
17
18 <ImageView
19 android:id="@+id/arrow"
20 android:layout_width="wrap_content"
21 android:layout_height="wrap_content"
22 android:layout_centerInParent="true"
23 android:src="@mipmap/indicator_arrow" />
24
25 <ProgressBar
26 android:id="@+id/progress_bar"
27 android:layout_width="30dp"
28 android:layout_height="30dp"
29 android:layout_centerInParent="true"
30 android:visibility="gone" />
31 </RelativeLayout>
32
33 <LinearLayout
34 android:layout_width="0dp"
35 android:layout_height="60dp"
36 android:layout_weight="12"
37 android:orientation="vertical">
38
39 <TextView
40 android:id="@+id/description"
41 android:layout_width="match_parent"
42 android:layout_height="0dp"
43 android:layout_weight="1"
44 android:gravity="center_horizontal|bottom"
45 android:text="@string/pull_to_refresh" />
46
47 <TextView
48 android:id="@+id/updated_at"
49 android:layout_width="match_parent"
50 android:layout_height="0dp"
51 android:layout_weight="1"
52 android:gravity="center_horizontal|top"
53 android:text="@string/updated_at" />
54 </LinearLayout>
55 </LinearLayout>
56
57 </RelativeLayout>
pull_to_refresh
--> 然后, 也是主要的, 自定义下拉刷新的 View (包含下拉刷新所有操作) RefreshView.java:
1 package com.dragon.android.tofreshlayout;
2
3 import android.content.Context;
4 import android.content.SharedPreferences;
5 import android.os.AsyncTask;
6 import android.os.SystemClock;
7 import android.preference.PreferenceManager;
8 import android.util.AttributeSet;
9 import android.view.LayoutInflater;
10 import android.view.MotionEvent;
11 import android.view.View;
12 import android.view.ViewConfiguration;
13 import android.view.animation.RotateAnimation;
14 import android.widget.ImageView;
15 import android.widget.LinearLayout;
16 import android.widget.ListView;
17 import android.widget.ProgressBar;
18 import android.widget.TextView;
19
20 public class RefreshView extends LinearLayout implements View.OnTouchListener {
21
22 private static final String TAG = RefreshView.class.getSimpleName();
23
24 public enum PULL_STATUS {
25 STATUS_PULL_TO_REFRESH(0), // 下拉状态
26 STATUS_RELEASE_TO_REFRESH(1), // 释放立即刷新状态
27 STATUS_REFRESHING(2), // 正在刷新状态
28 STATUS_REFRESH_FINISHED(3); // 刷新完成或未刷新状态
29
30 private int status; // 状态
31
32 PULL_STATUS(int value) {
33 this.status = value;
34 }
35
36 public int getValue() {
37 return this.status;
38 }
39 }
40
41 // 下拉头部回滚的速度
42 public static final int SCROLL_SPEED = -20;
43 // 一分钟的毫秒值,用于判断上次的更新时间
44 public static final long ONE_MINUTE = 60 * 1000;
45 // 一小时的毫秒值,用于判断上次的更新时间
46 public static final long ONE_HOUR = 60 * ONE_MINUTE;
47 // 一天的毫秒值,用于判断上次的更新时间
48 public static final long ONE_DAY = 24 * ONE_HOUR;
49 // 一月的毫秒值,用于判断上次的更新时间
50 public static final long ONE_MONTH = 30 * ONE_DAY;
51 // 一年的毫秒值,用于判断上次的更新时间
52 public static final long ONE_YEAR = 12 * ONE_MONTH;
53 // 上次更新时间的字符串常量,用于作为 SharedPreferences 的键值
54 private static final String UPDATED_AT = "updated_at";
55
56 // 下拉刷新的回调接口
57 private PullToRefreshListener mListener;
58
59 private SharedPreferences preferences; // 用于存储上次更新时间
60 private View header; // 下拉头的View
61 private ListView listView; // 需要去下拉刷新的ListView
62
63 private ProgressBar progressBar; // 刷新时显示的进度条
64 private ImageView arrow; // 指示下拉和释放的箭头
65 private TextView description; // 指示下拉和释放的文字描述
66 private TextView updateAt; // 上次更新时间的文字描述
67
68 private MarginLayoutParams headerLayoutParams; // 下拉头的布局参数
69 private long lastUpdateTime; // 上次更新时间的毫秒值
70
71 // 为了防止不同界面的下拉刷新在上次更新时间上互相有冲突,使用id来做区分
72 private int mId = -1;
73
74 private int hideHeaderHeight; // 下拉头的高度
75
76 /**
77 * 当前处理什么状态,可选值有 STATUS_PULL_TO_REFRESH, STATUS_RELEASE_TO_REFRESH, STATUS_REFRESHING 和 STATUS_REFRESH_FINISHED
78 */
79 private PULL_STATUS currentStatus = PULL_STATUS.STATUS_REFRESH_FINISHED;
80
81 // 记录上一次的状态是什么,避免进行重复操作
82 private PULL_STATUS lastStatus = currentStatus;
83
84 private float yDown; // 手指按下时的屏幕纵坐标
85
86 private int touchSlop; // 在被判定为滚动之前用户手指可以移动的最大值。
87
88 private boolean loadOnce; // 是否已加载过一次layout,这里onLayout中的初始化只需加载一次
89
90 private boolean ableToPull; // 当前是否可以下拉,只有ListView滚动到头的时候才允许下拉
91
92 /**
93 * 下拉刷新控件的构造函数,会在运行时动态添加一个下拉头的布局
94 */
95 public RefreshView(Context context, AttributeSet attrs) {
96 super(context, attrs);
97
98 preferences = PreferenceManager.getDefaultSharedPreferences(context);
99 header = LayoutInflater.from(context).inflate(R.layout.pull_to_refresh, null, true);
100 progressBar = (ProgressBar) header.findViewById(R.id.progress_bar);
101 arrow = (ImageView) header.findViewById(R.id.arrow);
102 description = (TextView) header.findViewById(R.id.description);
103 updateAt = (TextView) header.findViewById(R.id.updated_at);
104 touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
105
106 refreshUpdatedAtValue();
107 setOrientation(VERTICAL);
108 addView(header, 0);
109
110 //Log.d(TAG, "RefreshView Constructor() getChildAt(0): " + getChildAt(0));
111 //Log.d(TAG, "RefreshView Constructor() getChildAt(0): " + getChildAt(1));
112
113 // listView = (ListView) getChildAt(1);
114 // listView.setOnTouchListener(this);
115 }
116
117 /**
118 * 进行一些关键性的初始化操作,比如:将下拉头向上偏移进行隐藏,给 ListView 注册 touch 事件
119 */
120 @Override
121 protected void onLayout(boolean changed, int l, int t, int r, int b) {
122 super.onLayout(changed, l, t, r, b);
123 if (changed && !loadOnce) {
124 hideHeaderHeight = -header.getHeight();
125
126 headerLayoutParams = (MarginLayoutParams) header.getLayoutParams();
127 headerLayoutParams.topMargin = hideHeaderHeight;
128 listView = (ListView) getChildAt(1);
129 //Log.d(TAG, "onLayout() getChildAt(0): " + getChildAt(0));
130 //Log.d(TAG, "onLayout() listView: " + listView);
131 listView.setOnTouchListener(this);
132 loadOnce = true;
133 }
134 }
135
136 /**
137 * 当 ListView 被触摸时调用,其中处理了各种下拉刷新的具体逻辑
138 */
139 @Override
140 public boolean onTouch(View v, MotionEvent event) {
141 setCanAbleToPull(event); // 判断是否可以下拉
142 if (ableToPull) {
143 switch (event.getAction()) {
144 case MotionEvent.ACTION_DOWN:
145 yDown = event.getRawY();
146 break;
147 case MotionEvent.ACTION_MOVE:
148 // 获取移动中的 Y 轴的位置
149 float yMove = event.getRawY();
150 // 获取从按下到移动过程中移动的距离
151 int distance = (int) (yMove - yDown);
152
153 // 如果手指是上滑状态,并且下拉头是完全隐藏的,就屏蔽下拉事件
154 if (distance <= 0 && headerLayoutParams.topMargin <= hideHeaderHeight) {
155 return false;
156 }
157 if (distance < touchSlop) {
158 return false;
159 }
160 // 判断是否已经在刷新状态
161 if (currentStatus != PULL_STATUS.STATUS_REFRESHING) {
162 // 判断设置的 topMargin 是否 > 0, 默认初始设置为 -header.getHeight()
163 if (headerLayoutParams.topMargin > 0) {
164 currentStatus = PULL_STATUS.STATUS_RELEASE_TO_REFRESH;
165 } else {
166 // 否则状态为下拉中的状态
167 currentStatus = PULL_STATUS.STATUS_PULL_TO_REFRESH;
168 }
169 // 通过偏移下拉头的 topMargin 值,来实现下拉效果
170 headerLayoutParams.topMargin = (distance / 2) + hideHeaderHeight;
171 header.setLayoutParams(headerLayoutParams);
172 }
173 break;
174 case MotionEvent.ACTION_UP:
175 default:
176 if (currentStatus == PULL_STATUS.STATUS_RELEASE_TO_REFRESH) {
177 // 松手时如果是释放立即刷新状态,就去调用正在刷新的任务
178 new RefreshingTask().execute();
179 } else if (currentStatus == PULL_STATUS.STATUS_PULL_TO_REFRESH) {
180 // 松手时如果是下拉状态,就去调用隐藏下拉头的任务
181 new HideHeaderTask().execute();
182 }
183 break;
184 }
185 // 时刻记得更新下拉头中的信息
186 if (currentStatus == PULL_STATUS.STATUS_PULL_TO_REFRESH
187 || currentStatus == PULL_STATUS.STATUS_RELEASE_TO_REFRESH) {
188 updateHeaderView();
189 // 当前正处于下拉或释放状态,要让 ListView 失去焦点,否则被点击的那一项会一直处于选中状态
190 listView.setPressed(false);
191 listView.setFocusable(false);
192 listView.setFocusableInTouchMode(false);
193 lastStatus = currentStatus;
194 // 当前正处于下拉或释放状态,通过返回 true 屏蔽掉 ListView 的滚动事件
195 return true;
196 }
197 }
198 return false;
199 }
200
201 /**
202 * 给下拉刷新控件注册一个监听器
203 *
204 * @param listener 监听器的实现
205 * @param id 为了防止不同界面的下拉刷新在上次更新时间上互相有冲突,不同界面在注册下拉刷新监听器时一定要传入不同的 id
206 */
207 public void setOnRefreshListener(PullToRefreshListener listener, int id) {
208 mListener = listener;
209 mId = id;
210 }
211
212 /**
213 * 当所有的刷新逻辑完成后,记录调用一下,否则你的 ListView 将一直处于正在刷新状态
214 */
215 public void finishRefreshing() {
216 currentStatus = PULL_STATUS.STATUS_REFRESH_FINISHED;
217 preferences.edit().putLong(UPDATED_AT + mId, System.currentTimeMillis()).commit();
218 new HideHeaderTask().execute();
219 }
220
221 /**
222 * 根据当前 ListView 的滚动状态来设定 {@link #ableToPull}
223 * 的值,每次都需要在 onTouch 中第一个执行,这样可以判断出当前应该是滚动 ListView,还是应该进行下拉
224 */
225 private void setCanAbleToPull(MotionEvent event) {
226 View firstChild = listView.getChildAt(0);
227 if (firstChild != null) {
228 // 获取 ListView 中第一个Item的位置
229 int firstVisiblePos = listView.getFirstVisiblePosition();
230 // 判断第一个子控件的 Top 是否和第一个 Item 位置相等
231 if (firstVisiblePos == 0 && firstChild.getTop() == 0) {
232 if (!ableToPull) {
233 // getRawY() 获得的是相对屏幕 Y 方向的位置
234 yDown = event.getRawY();
235 }
236 // 如果首个元素的上边缘,距离父布局值为 0,就说明 ListView 滚动到了最顶部,此时应该允许下拉刷新
237 ableToPull = true;
238 } else {
239 if (headerLayoutParams.topMargin != hideHeaderHeight) {
240 headerLayoutParams.topMargin = hideHeaderHeight;
241 header.setLayoutParams(headerLayoutParams);
242 }
243 ableToPull = false;
244 }
245 } else {
246 // 如果 ListView 中没有元素,也应该允许下拉刷新
247 ableToPull = true;
248 }
249 }
250
251 /**
252 * 更新下拉头中的信息
253 */
254 private void updateHeaderView() {
255 if (lastStatus != currentStatus) {
256 if (currentStatus == PULL_STATUS.STATUS_PULL_TO_REFRESH) {
257 description.setText(getResources().getString(R.string.pull_to_refresh));
258 arrow.setVisibility(View.VISIBLE);
259 progressBar.setVisibility(View.GONE);
260 rotateArrow();
261 } else if (currentStatus == PULL_STATUS.STATUS_RELEASE_TO_REFRESH) {
262 description.setText(getResources().getString(R.string.release_to_refresh));
263 arrow.setVisibility(View.VISIBLE);
264 progressBar.setVisibility(View.GONE);
265 rotateArrow();
266 } else if (currentStatus == PULL_STATUS.STATUS_REFRESHING) {
267 description.setText(getResources().getString(R.string.refreshing));
268 progressBar.setVisibility(View.VISIBLE);
269 arrow.clearAnimation();
270 arrow.setVisibility(View.GONE);
271 }
272 refreshUpdatedAtValue();
273 }
274 }
275
276 /**
277 * 根据当前的状态来旋转箭头
278 */
279 private void rotateArrow() {
280 float pivotX = arrow.getWidth() / 2f;
281 float pivotY = arrow.getHeight() / 2f;
282 float fromDegrees = 0f;
283 float toDegrees = 0f;
284 if (currentStatus == PULL_STATUS.STATUS_PULL_TO_REFRESH) {
285 fromDegrees = 180f;
286 toDegrees = 360f;
287 } else if (currentStatus == PULL_STATUS.STATUS_RELEASE_TO_REFRESH) {
288 fromDegrees = 0f;
289 toDegrees = 180f;
290 }
291 RotateAnimation animation = new RotateAnimation(fromDegrees, toDegrees, pivotX, pivotY);
292 animation.setDuration(100);
293 animation.setFillAfter(true);
294 arrow.startAnimation(animation);
295 }
296
297 /**
298 * 刷新下拉头中上次更新时间的文字描述
299 */
300 private void refreshUpdatedAtValue() {
301 lastUpdateTime = preferences.getLong(UPDATED_AT + mId, -1);
302 long currentTime = System.currentTimeMillis();
303 long timePassed = currentTime - lastUpdateTime;
304 long timeIntoFormat;
305 String updateAtValue;
306 if (lastUpdateTime == -1) {
307 updateAtValue = getResources().getString(R.string.not_updated_yet);
308 } else if (timePassed < 0) {
309 updateAtValue = getResources().getString(R.string.time_error);
310 } else if (timePassed < ONE_MINUTE) {
311 updateAtValue = getResources().getString(R.string.updated_just_now);
312 } else if (timePassed < ONE_HOUR) {
313 timeIntoFormat = timePassed / ONE_MINUTE;
314 String value = timeIntoFormat + "分钟";
315 updateAtValue = String.format(getResources().getString(R.string.updated_at), value);
316 } else if (timePassed < ONE_DAY) {
317 timeIntoFormat = timePassed / ONE_HOUR;
318 String value = timeIntoFormat + "小时";
319 updateAtValue = String.format(getResources().getString(R.string.updated_at), value);
320 } else if (timePassed < ONE_MONTH) {
321 timeIntoFormat = timePassed / ONE_DAY;
322 String value = timeIntoFormat + "天";
323 updateAtValue = String.format(getResources().getString(R.string.updated_at), value);
324 } else if (timePassed < ONE_YEAR) {
325 timeIntoFormat = timePassed / ONE_MONTH;
326 String value = timeIntoFormat + "个月";
327 updateAtValue = String.format(getResources().getString(R.string.updated_at), value);
328 } else {
329 timeIntoFormat = timePassed / ONE_YEAR;
330 String value = timeIntoFormat + "年";
331 updateAtValue = String.format(getResources().getString(R.string.updated_at), value);
332 }
333 updateAt.setText(updateAtValue);
334 }
335
336 /**
337 * 正在刷新的任务,在此任务中会去回调注册进来的下拉刷新监听器
338 */
339 class RefreshingTask extends AsyncTask<Void, Integer, Void> {
340
341 @Override
342 protected Void doInBackground(Void... params) {
343 int topMargin = headerLayoutParams.topMargin;
344 while (true) {
345 topMargin = topMargin + SCROLL_SPEED;
346 if (topMargin <= 0) {
347 topMargin = 0;
348 break;
349 }
350 publishProgress(topMargin);
351 SystemClock.sleep(10);
352 }
353 currentStatus = PULL_STATUS.STATUS_REFRESHING;
354 publishProgress(0);
355 if (mListener != null) {
356 mListener.onRefresh();
357 }
358 return null;
359 }
360
361 @Override
362 protected void onProgressUpdate(Integer... topMargin) {
363 updateHeaderView();
364 headerLayoutParams.topMargin = topMargin[0];
365 header.setLayoutParams(headerLayoutParams);
366 }
367
368 }
369
370 /**
371 * 隐藏下拉头的任务,当未进行下拉刷新或下拉刷新完成后,此任务将会使下拉头重新隐藏
372 */
373 class HideHeaderTask extends AsyncTask<Void, Integer, Integer> {
374
375 @Override
376 protected Integer doInBackground(Void... params) {
377 int topMargin = headerLayoutParams.topMargin;
378 while (true) {
379 topMargin = topMargin + SCROLL_SPEED;
380 if (topMargin <= hideHeaderHeight) {
381 topMargin = hideHeaderHeight;
382 break;
383 }
384 publishProgress(topMargin);
385 SystemClock.sleep(10);
386 }
387 return topMargin;
388 }
389
390 @Override
391 protected void onProgressUpdate(Integer ... topMargin) {
392 headerLayoutParams.topMargin = topMargin[0];
393 header.setLayoutParams(headerLayoutParams);
394 }
395
396 @Override
397 protected void onPostExecute(Integer topMargin) {
398 headerLayoutParams.topMargin = topMargin;
399 header.setLayoutParams(headerLayoutParams);
400 currentStatus = PULL_STATUS.STATUS_REFRESH_FINISHED;
401 }
402 }
403
404 /**
405 * 下拉刷新的监听器,使用下拉刷新的地方应该注册此监听器来获取刷新回调
406 */
407 public interface PullToRefreshListener {
408 // 刷新时会去回调此方法,在方法内编写具体的刷新逻辑。注意此方法是在子线程中调用的, 可以不必另开线程来进行耗时操作
409 void onRefresh();
410 }
411 }
--> 第三步, 写主布局:
1 <?xml version="1.0" encoding="utf-8"?>
2 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:tools="http://schemas.android.com/tools"
4 android:layout_width="match_parent"
5 android:layout_height="match_parent"
6 tools:context=".MainActivity" >
7
8 <com.dragon.android.tofreshlayout.RefreshView
9 android:id="@+id/refreshable_view"
10 android:layout_width="match_parent"
11 android:layout_height="match_parent" >
12
13 <ListView
14 android:id="@+id/list_view"
15 android:layout_width="match_parent"
16 android:layout_height="match_parent" >
17 </ListView>
18
19 </com.dragon.android.tofreshlayout.RefreshView>
20
21 </RelativeLayout>
activity_main
--> 最后, Java 代码添加 ListView 的数据:
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);
}
}