[转]Android下拉刷新完全解析,教你如何一分钟实现下拉刷新功能

时间:2023-03-08 22:47:38
[转]Android下拉刷新完全解析,教你如何一分钟实现下拉刷新功能

版权声明:本文出自郭霖的博客,转载必须注明出处。

转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/9255575

最近项目中需要用到ListView下拉刷新的功能,一开始想图省事,在网上直接找一个现成的,可是尝试了网上多个版本的下拉刷新之后发现效果都不怎么理想。有些是因为功能不完整或有Bug,有些是因为使用起来太复杂,十全十美的还真没找到。因此我也是放弃了在网上找现成代码的想法,自己花功夫编写了一种非常简单的下拉刷新实现方案,现在拿出来和大家分享一下。相信在阅读完本篇文章之后,大家都可以在自己的项目中一分钟引入下拉刷新功能。

首先讲一下实现原理。这里我们将采取的方案是使用组合View的方式,先自定义一个布局继承自LinearLayout,然后在这个布局中加入下拉头和ListView这两个子元素,并让这两个子元素纵向排列。初始化的时候,让下拉头向上偏移出屏幕,这样我们看到的就只有ListView了。然后对ListView的touch事件进行监听,如果当前ListView已经滚动到顶部并且手指还在向下拉的话,那就将下拉头显示出来,松手后进行刷新操作,并将下拉头隐藏。原理示意图如下:

[转]Android下拉刷新完全解析,教你如何一分钟实现下拉刷新功能

那我们现在就来动手实现一下,新建一个项目起名叫PullToRefreshTest,先在项目中定义一个下拉头的布局文件pull_to_refresh.xml,代码如下所示:

  1. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. xmlns:tools="http://schemas.android.com/tools"
  3. android:id="@+id/pull_to_refresh_head"
  4. android:layout_width="fill_parent"
  5. android:layout_height="60dip" >
  6. <LinearLayout
  7. android:layout_width="200dip"
  8. android:layout_height="60dip"
  9. android:layout_centerInParent="true"
  10. android:orientation="horizontal" >
  11. <RelativeLayout
  12. android:layout_width="0dip"
  13. android:layout_height="60dip"
  14. android:layout_weight="3"
  15. >
  16. <ImageView
  17. android:id="@+id/arrow"
  18. android:layout_width="wrap_content"
  19. android:layout_height="wrap_content"
  20. android:layout_centerInParent="true"
  21. android:src="@drawable/arrow"
  22. />
  23. <ProgressBar
  24. android:id="@+id/progress_bar"
  25. android:layout_width="30dip"
  26. android:layout_height="30dip"
  27. android:layout_centerInParent="true"
  28. android:visibility="gone"
  29. />
  30. </RelativeLayout>
  31. <LinearLayout
  32. android:layout_width="0dip"
  33. android:layout_height="60dip"
  34. android:layout_weight="12"
  35. android:orientation="vertical" >
  36. <TextView
  37. android:id="@+id/description"
  38. android:layout_width="fill_parent"
  39. android:layout_height="0dip"
  40. android:layout_weight="1"
  41. android:gravity="center_horizontal|bottom"
  42. android:text="@string/pull_to_refresh" />
  43. <TextView
  44. android:id="@+id/updated_at"
  45. android:layout_width="fill_parent"
  46. android:layout_height="0dip"
  47. android:layout_weight="1"
  48. android:gravity="center_horizontal|top"
  49. android:text="@string/updated_at" />
  50. </LinearLayout>
  51. </LinearLayout>
  52. </RelativeLayout>

在这个布局中,我们包含了一个下拉指示箭头,一个下拉状态文字提示,和一个上次更新的时间。当然,还有一个隐藏的旋转进度条,只有正在刷新的时候我们才会将它显示出来。
布局中所有引用的字符串我们都放在strings.xml中,如下所示:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <resources>
  3. <string name="app_name">PullToRefreshTest</string>
  4. <string name="pull_to_refresh">下拉可以刷新</string>
  5. <string name="release_to_refresh">释放立即刷新</string>
  6. <string name="refreshing">正在刷新…</string>
  7. <string name="not_updated_yet">暂未更新过</string>
  8. <string name="updated_at">上次更新于%1$s前</string>
  9. <string name="updated_just_now">刚刚更新</string>
  10. <string name="time_error">时间有问题</string>
  11. </resources>

然后新建一个RefreshableView继承自LinearLayout,代码如下所示:

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

这个类是整个下拉刷新功能中最重要的一个类,注释已经写得比较详细了,我再简单解释一下。首先在RefreshableView的构造函数中动态添加了刚刚定义的pull_to_refresh这个布局作为下拉头,然后在onLayout方法中将下拉头向上偏移出了屏幕,再给ListView注册了touch事件。之后每当手指在ListView上滑动时,onTouch方法就会执行。在onTouch方法中的第一行就调用了setIsAbleToPull方法来判断ListView是否滚动到了最顶部,只有滚动到了最顶部才会执行后面的代码,否则就视为正常的ListView滚动,不做任何处理。当ListView滚动到了最顶部时,如果手指还在向下拖动,就会改变下拉头的偏移值,让下拉头显示出来,下拉的距离设定为手指移动距离的1/2,这样才会有拉力的感觉。如果下拉的距离足够大,在松手的时候就会执行刷新操作,如果距离不够大,就仅仅重新隐藏下拉头。

具体的刷新操作会在RefreshingTask中进行,其中在doInBackground方法中回调了PullToRefreshListener接口的onRefresh方法,这也是大家在使用RefreshableView时必须要去实现的一个接口,因为具体刷新的逻辑就应该写在onRefresh方法中,后面会演示使用的方法。

另外每次在下拉的时候都还会调用updateHeaderView方法来改变下拉头中的数据,比如箭头方向的旋转,下拉文字描述的改变等。更加深入的理解请大家仔细去阅读RefreshableView中的代码。
现在我们已经把下拉刷新的所有功能都完成了,接下来就要看一看如何在项目中引入下拉刷新了。打开或新建activity_main.xml作为程序主界面的布局,加入如下代码:

  1. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. xmlns:tools="http://schemas.android.com/tools"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent"
  5. tools:context=".MainActivity" >
  6. <com.example.pulltorefreshtest.RefreshableView
  7. android:id="@+id/refreshable_view"
  8. android:layout_width="fill_parent"
  9. android:layout_height="fill_parent" >
  10. <ListView
  11. android:id="@+id/list_view"
  12. android:layout_width="fill_parent"
  13. android:layout_height="fill_parent" >
  14. </ListView>
  15. </com.example.pulltorefreshtest.RefreshableView>
  16. </RelativeLayout>

可以看到,我们在自定义的RefreshableView中加入了一个ListView,这就意味着给这个ListView加入了下拉刷新的功能,就是这么简单!
然后我们再来看一下程序的主Activity,打开或新建MainActivity,加入如下代码:

  1. public class MainActivity extends Activity {
  2. RefreshableView refreshableView;
  3. ListView listView;
  4. ArrayAdapter<String> adapter;
  5. String[] items = { "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L" };
  6. @Override
  7. protected void onCreate(Bundle savedInstanceState) {
  8. super.onCreate(savedInstanceState);
  9. requestWindowFeature(Window.FEATURE_NO_TITLE);
  10. setContentView(R.layout.activity_main);
  11. refreshableView = (RefreshableView) findViewById(R.id.refreshable_view);
  12. listView = (ListView) findViewById(R.id.list_view);
  13. adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, items);
  14. listView.setAdapter(adapter);
  15. refreshableView.setOnRefreshListener(new PullToRefreshListener() {
  16. @Override
  17. public void onRefresh() {
  18. try {
  19. Thread.sleep(3000);
  20. } catch (InterruptedException e) {
  21. e.printStackTrace();
  22. }
  23. refreshableView.finishRefreshing();
  24. }
  25. }, 0);
  26. }
  27. }

可以看到,我们通过调用RefreshableView的setOnRefreshListener方法注册了一个监听器,当ListView正在刷新时就会回调监听器的onRefresh方法,刷新的具体逻辑就在这里处理。而且这个方法已经自动开启了线程,可以直接在onRefresh方法中进行耗时操作,比如向服务器请求最新数据等,在这里我就简单让线程睡眠3秒钟。另外在onRefresh方法的最后,一定要调用RefreshableView中的finishRefreshing方法,这个方法是用来通知RefreshableView刷新结束了,不然我们的ListView将一直处于正在刷新的状态。
不知道大家有没有注意到,setOnRefreshListener这个方法其实是有两个参数的,我们刚刚也是传入了一个不起眼的0。那这第二个参数是用来做什么的呢?由于RefreshableView比较智能,它会自动帮我们记录上次刷新完成的时间,然后下拉的时候会在下拉头中显示距上次刷新已过了多久。这是一个非常好用的功能,让我们不用再自己手动去记录和计算时间了,但是却存在一个问题。如果当前我们的项目中有三个地方都使用到了下拉刷新的功能,现在在一处进行了刷新,其它两处的时间也都会跟着改变!因为刷新完成的时间是记录在配置文件中的,由于在一处刷新更改了配置文件,导致在其它两处读取到的配置文件时间已经是更改过的了。那解决方案是什么?就是每个用到下拉刷新的地方,给setOnRefreshListener方法的第二个参数中传入不同的id就行了。这样各处的上次刷新完成时间都是单独记录的,相互之间就不会再有影响。
好了,全部的代码都在这里了,让我们来运行一下,看看效果吧。

[转]Android下拉刷新完全解析,教你如何一分钟实现下拉刷新功能
效果看起来还是非常不错的。我们最后再来总结一下,在项目中引入ListView下拉刷新功能只需三步:

1. 在Activity的布局文件中加入自定义的RefreshableView,并让ListView包含在其中。

2. 在Activity中调用RefreshableView的setOnRefreshListener方法注册回调接口。

3. 在onRefresh方法的最后,记得调用RefreshableView的finishRefreshing方法,通知刷新结束。

从此以后,在项目的任何地方,一分钟引入下拉刷新功能妥妥的。