android UI进阶之实现listview的下拉加载

时间:2023-03-09 01:27:25
android UI进阶之实现listview的下拉加载

关于listview的操作五花八门,有下拉刷新,分级显示,分页列表,逐页加载等,以后会陆续和大家分享这些技术,今天讲下下拉加载这个功能的实现。

最初的下拉加载应该是ios上的效果,现在很多应用如新浪微博等都加入了这个操作。即下拉listview刷新列表,这无疑是一个非常友好的操作。今天就和大家分享下这个操作的实现。

先看下运行效果:

android UI进阶之实现listview的下拉加载  android UI进阶之实现listview的下拉加载 android UI进阶之实现listview的下拉加载

android UI进阶之实现listview的下拉加载 
 android UI进阶之实现listview的下拉加载

代码参考国外朋友Johan Nilsson的实现,http://johannilsson.com/2011/03/13/android-pull-to-refresh-update.html

主要原理为监听触摸和滑动操作,在listview
头部加载一个视图。那要做的其实很简单:1.写好加载到listview头部的view
2.重写listview,实现onTouchEvent方法和onScroll方法,监听滑动状态。计算headview全部显示出来即可实行加载动
作,加载完成即刷新列表。重新隐藏headview。

首先写下headview的xml代码:

  1. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. android:layout_width="fill_parent"
  3. android:layout_height="fill_parent"
  4. android:paddingTop="10dip"
  5. android:paddingBottom="15dip"
  6. android:gravity="center"
  7. android:id="@+id/pull_to_refresh_header"
  8. >
  9. <ProgressBar
  10. android:id="@+id/pull_to_refresh_progress"
  11. android:indeterminate="true"
  12. android:layout_width="wrap_content"
  13. android:layout_height="wrap_content"
  14. android:layout_marginLeft="30dip"
  15. android:layout_marginRight="20dip"
  16. android:layout_marginTop="10dip"
  17. android:visibility="gone"
  18. android:layout_centerVertical="true"
  19. style="?android:attr/progressBarStyleSmall"
  20. />
  21. <ImageView
  22. android:id="@+id/pull_to_refresh_image"
  23. android:layout_width="wrap_content"
  24. android:layout_height="wrap_content"
  25. android:layout_marginLeft="30dip"
  26. android:layout_marginRight="20dip"
  27. android:visibility="gone"
  28. android:layout_gravity="center"
  29. android:gravity="center"
  30. android:src="@drawable/ic_pulltorefresh_arrow"
  31. />
  32. <TextView
  33. android:id="@+id/pull_to_refresh_text"
  34. android:textAppearance="?android:attr/textAppearanceMedium"
  35. android:textStyle="bold"
  36. android:paddingTop="5dip"
  37. android:layout_width="fill_parent"
  38. android:layout_height="wrap_content"
  39. android:layout_gravity="center"
  40. android:gravity="center"
  41. />
  42. <TextView
  43. android:id="@+id/pull_to_refresh_updated_at"
  44. android:layout_below="@+id/pull_to_refresh_text"
  45. android:visibility="gone"
  46. android:textAppearance="?android:attr/textAppearanceSmall"
  47. android:layout_width="fill_parent"
  48. android:layout_height="wrap_content"
  49. android:layout_gravity="center"
  50. android:gravity="center"
  51. />
  52. </RelativeLayout>

代码比较简单,即headview包括一个进度条一个箭头和两段文字(一个显示加载状态,另一个显示最后刷新时间,本例就不设置了)。

而后重写listview,代码如下:

  1. package com.notice.pullrefresh;
  2. import android.content.Context;
  3. import android.util.AttributeSet;
  4. import android.view.LayoutInflater;
  5. import android.view.MotionEvent;
  6. import android.view.View;
  7. import android.view.ViewGroup;
  8. import android.view.animation.LinearInterpolator;
  9. import android.view.animation.RotateAnimation;
  10. import android.widget.AbsListView;
  11. import android.widget.AbsListView.OnScrollListener;
  12. import android.widget.ImageView;
  13. import android.widget.ListAdapter;
  14. import android.widget.ListView;
  15. import android.widget.ProgressBar;
  16. import android.widget.RelativeLayout;
  17. import android.widget.TextView;
  18. public class PullToRefreshListView extends ListView implements OnScrollListener {
  19. // 状态
  20. private static final int TAP_TO_REFRESH = 1;
  21. private static final int PULL_TO_REFRESH = 2;
  22. private static final int RELEASE_TO_REFRESH = 3;
  23. private static final int REFRESHING = 4;
  24. private OnRefreshListener mOnRefreshListener;
  25. // 监听对listview的滑动动作
  26. private OnScrollListener mOnScrollListener;
  27. private LayoutInflater mInflater;
  28. //顶部刷新时出现的控件
  29. private RelativeLayout mRefreshView;
  30. private TextView mRefreshViewText;
  31. private ImageView mRefreshViewImage;
  32. private ProgressBar mRefreshViewProgress;
  33. private TextView mRefreshViewLastUpdated;
  34. // 当前滑动状态
  35. private int mCurrentScrollState;
  36. // 当前刷新状态
  37. private int mRefreshState;
  38. // 箭头动画效果
  39. private RotateAnimation mFlipAnimation;
  40. private RotateAnimation mReverseFlipAnimation;
  41. private int mRefreshViewHeight;
  42. private int mRefreshOriginalTopPadding;
  43. private int mLastMotionY;
  44. private boolean mBounceHack;
  45. public PullToRefreshListView(Context context) {
  46. super(context);
  47. init(context);
  48. }
  49. public PullToRefreshListView(Context context, AttributeSet attrs) {
  50. super(context, attrs);
  51. init(context);
  52. }
  53. public PullToRefreshListView(Context context, AttributeSet attrs, int defStyle) {
  54. super(context, attrs, defStyle);
  55. init(context);
  56. }
  57. /**
  58. * 初始化控件和箭头动画(这里直接在代码中初始化动画而不是通过xml)
  59. */
  60. private void init(Context context) {
  61. mFlipAnimation = new RotateAnimation(0, -180,
  62. RotateAnimation.RELATIVE_TO_SELF, 0.5f,
  63. RotateAnimation.RELATIVE_TO_SELF, 0.5f);
  64. mFlipAnimation.setInterpolator(new LinearInterpolator());
  65. mFlipAnimation.setDuration(250);
  66. mFlipAnimation.setFillAfter(true);
  67. mReverseFlipAnimation = new RotateAnimation(-180, 0,
  68. RotateAnimation.RELATIVE_TO_SELF, 0.5f,
  69. RotateAnimation.RELATIVE_TO_SELF, 0.5f);
  70. mReverseFlipAnimation.setInterpolator(new LinearInterpolator());
  71. mReverseFlipAnimation.setDuration(250);
  72. mReverseFlipAnimation.setFillAfter(true);
  73. mInflater = (LayoutInflater) context.getSystemService(
  74. Context.LAYOUT_INFLATER_SERVICE);
  75. mRefreshView = (RelativeLayout) mInflater.inflate(
  76. R.layout.pull_to_refresh_header, this, false);
  77. mRefreshViewText =
  78. (TextView) mRefreshView.findViewById(R.id.pull_to_refresh_text);
  79. mRefreshViewImage =
  80. (ImageView) mRefreshView.findViewById(R.id.pull_to_refresh_image);
  81. mRefreshViewProgress =
  82. (ProgressBar) mRefreshView.findViewById(R.id.pull_to_refresh_progress);
  83. mRefreshViewLastUpdated =
  84. (TextView) mRefreshView.findViewById(R.id.pull_to_refresh_updated_at);
  85. mRefreshViewImage.setMinimumHeight(50);
  86. mRefreshOriginalTopPadding = mRefreshView.getPaddingTop();
  87. mRefreshState = TAP_TO_REFRESH;
  88. //为listview头部增加一个view
  89. addHeaderView(mRefreshView);
  90. super.setOnScrollListener(this);
  91. measureView(mRefreshView);
  92. mRefreshViewHeight = mRefreshView.getMeasuredHeight();
  93. }
  94. @Override
  95. protected void onAttachedToWindow() {
  96. setSelection(1);
  97. }
  98. @Override
  99. public void setAdapter(ListAdapter adapter) {
  100. super.setAdapter(adapter);
  101. setSelection(1);
  102. }
  103. /**
  104. * 设置滑动监听器
  105. *
  106. */
  107. @Override
  108. public void setOnScrollListener(AbsListView.OnScrollListener l) {
  109. mOnScrollListener = l;
  110. }
  111. /**
  112. * 注册一个list需要刷新时的回调接口
  113. *
  114. */
  115. public void setOnRefreshListener(OnRefreshListener onRefreshListener) {
  116. mOnRefreshListener = onRefreshListener;
  117. }
  118. /**
  119. * 设置标签显示何时最后被刷新
  120. *
  121. * @param lastUpdated
  122. *            Last updated at.
  123. */
  124. public void setLastUpdated(CharSequence lastUpdated) {
  125. if (lastUpdated != null) {
  126. mRefreshViewLastUpdated.setVisibility(View.VISIBLE);
  127. mRefreshViewLastUpdated.setText(lastUpdated);
  128. } else {
  129. mRefreshViewLastUpdated.setVisibility(View.GONE);
  130. }
  131. }
  132. // 实现该方法处理触摸
  133. @Override
  134. public boolean onTouchEvent(MotionEvent event) {
  135. final int y = (int) event.getY();
  136. mBounceHack = false;
  137. switch (event.getAction()) {
  138. case MotionEvent.ACTION_UP:
  139. if (!isVerticalScrollBarEnabled()) {
  140. setVerticalScrollBarEnabled(true);
  141. }
  142. if (getFirstVisiblePosition() == 0 && mRefreshState != REFRESHING) {
  143. // 拖动距离达到刷新需要
  144. if ((mRefreshView.getBottom() >= mRefreshViewHeight
  145. || mRefreshView.getTop() >= 0)
  146. && mRefreshState == RELEASE_TO_REFRESH) {
  147. // 把状态设置为正在刷新
  148. mRefreshState = REFRESHING;
  149. // 准备刷新
  150. prepareForRefresh();
  151. // 刷新
  152. onRefresh();
  153. } else if (mRefreshView.getBottom() < mRefreshViewHeight
  154. || mRefreshView.getTop() <= 0) {
  155. // 中止刷新
  156. resetHeader();
  157. setSelection(1);
  158. }
  159. }
  160. break;
  161. case MotionEvent.ACTION_DOWN:
  162. // 获得按下y轴位置
  163. mLastMotionY = y;
  164. break;
  165. case MotionEvent.ACTION_MOVE:
  166. // 计算边距
  167. applyHeaderPadding(event);
  168. break;
  169. }
  170. return super.onTouchEvent(event);
  171. }
  172. // 获得header的边距
  173. private void applyHeaderPadding(MotionEvent ev) {
  174. int pointerCount = ev.getHistorySize();
  175. for (int p = 0; p < pointerCount; p++) {
  176. if (mRefreshState == RELEASE_TO_REFRESH) {
  177. if (isVerticalFadingEdgeEnabled()) {
  178. setVerticalScrollBarEnabled(false);
  179. }
  180. int historicalY = (int) ev.getHistoricalY(p);
  181. // 计算申请的边距,除以1.7使得拉动效果更好
  182. int topPadding = (int) (((historicalY - mLastMotionY)
  183. - mRefreshViewHeight) / 1.7);
  184. mRefreshView.setPadding(
  185. mRefreshView.getPaddingLeft(),
  186. topPadding,
  187. mRefreshView.getPaddingRight(),
  188. mRefreshView.getPaddingBottom());
  189. }
  190. }
  191. }
  192. /**
  193. * 将head的边距重置为初始的数值
  194. */
  195. private void resetHeaderPadding() {
  196. mRefreshView.setPadding(
  197. mRefreshView.getPaddingLeft(),
  198. mRefreshOriginalTopPadding,
  199. mRefreshView.getPaddingRight(),
  200. mRefreshView.getPaddingBottom());
  201. }
  202. /**
  203. * 重置header为之前的状态
  204. */
  205. private void resetHeader() {
  206. if (mRefreshState != TAP_TO_REFRESH) {
  207. mRefreshState = TAP_TO_REFRESH;
  208. resetHeaderPadding();
  209. // 将刷新图标换成箭头
  210. mRefreshViewImage.setImageResource(R.drawable.ic_pulltorefresh_arrow);
  211. // 清除动画
  212. mRefreshViewImage.clearAnimation();
  213. // 隐藏图标和进度条
  214. mRefreshViewImage.setVisibility(View.GONE);
  215. mRefreshViewProgress.setVisibility(View.GONE);
  216. }
  217. }
  218. // 估算headview的width和height
  219. private void measureView(View child) {
  220. ViewGroup.LayoutParams p = child.getLayoutParams();
  221. if (p == null) {
  222. p = new ViewGroup.LayoutParams(
  223. ViewGroup.LayoutParams.FILL_PARENT,
  224. ViewGroup.LayoutParams.WRAP_CONTENT);
  225. }
  226. int childWidthSpec = ViewGroup.getChildMeasureSpec(0,
  227. 0 + 0, p.width);
  228. int lpHeight = p.height;
  229. int childHeightSpec;
  230. if (lpHeight > 0) {
  231. childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
  232. } else {
  233. childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
  234. }
  235. child.measure(childWidthSpec, childHeightSpec);
  236. }
  237. @Override
  238. public void onScroll(AbsListView view, int firstVisibleItem,
  239. int visibleItemCount, int totalItemCount) {
  240. // 在refreshview完全可见时,设置文字为松开刷新,同时翻转箭头
  241. if (mCurrentScrollState == SCROLL_STATE_TOUCH_SCROLL
  242. && mRefreshState != REFRESHING) {
  243. if (firstVisibleItem == 0) {
  244. mRefreshViewImage.setVisibility(View.VISIBLE);
  245. if ((mRefreshView.getBottom() >= mRefreshViewHeight + 20
  246. || mRefreshView.getTop() >= 0)
  247. && mRefreshState != RELEASE_TO_REFRESH) {
  248. mRefreshViewText.setText("松开加载...");
  249. mRefreshViewImage.clearAnimation();
  250. mRefreshViewImage.startAnimation(mFlipAnimation);
  251. mRefreshState = RELEASE_TO_REFRESH;
  252. } else if (mRefreshView.getBottom() < mRefreshViewHeight + 20
  253. && mRefreshState != PULL_TO_REFRESH) {
  254. mRefreshViewText.setText("下拉刷新...");
  255. if (mRefreshState != TAP_TO_REFRESH) {
  256. mRefreshViewImage.clearAnimation();
  257. mRefreshViewImage.startAnimation(mReverseFlipAnimation);
  258. }
  259. mRefreshState = PULL_TO_REFRESH;
  260. }
  261. } else {
  262. mRefreshViewImage.setVisibility(View.GONE);
  263. resetHeader();
  264. }
  265. } else if (mCurrentScrollState == SCROLL_STATE_FLING
  266. && firstVisibleItem == 0
  267. && mRefreshState != REFRESHING) {
  268. setSelection(1);
  269. mBounceHack = true;
  270. } else if (mBounceHack && mCurrentScrollState == SCROLL_STATE_FLING) {
  271. setSelection(1);
  272. }
  273. if (mOnScrollListener != null) {
  274. mOnScrollListener.onScroll(view, firstVisibleItem,
  275. visibleItemCount, totalItemCount);
  276. }
  277. }
  278. @Override
  279. public void onScrollStateChanged(AbsListView view, int scrollState) {
  280. mCurrentScrollState = scrollState;
  281. if (mCurrentScrollState == SCROLL_STATE_IDLE) {
  282. mBounceHack = false;
  283. }
  284. if (mOnScrollListener != null) {
  285. mOnScrollListener.onScrollStateChanged(view, scrollState);
  286. }
  287. }
  288. public void prepareForRefresh() {
  289. resetHeaderPadding();// 恢复header的边距
  290. mRefreshViewImage.setVisibility(View.GONE);
  291. // 注意加上,否则仍然显示之前的图片
  292. mRefreshViewImage.setImageDrawable(null);
  293. mRefreshViewProgress.setVisibility(View.VISIBLE);
  294. // 设置文字
  295. mRefreshViewText.setText("加载中...");
  296. mRefreshState = REFRESHING;
  297. }
  298. public void onRefresh() {
  299. if (mOnRefreshListener != null) {
  300. mOnRefreshListener.onRefresh();
  301. }
  302. }
  303. /**
  304. * 重置listview为普通的listview,该方法设置最后更新时间
  305. *
  306. * @param lastUpdated
  307. *            Last updated at.
  308. */
  309. public void onRefreshComplete(CharSequence lastUpdated) {
  310. setLastUpdated(lastUpdated);
  311. onRefreshComplete();
  312. }
  313. /**
  314. * 重置listview为普通的listview,不设置最后更新时间
  315. */
  316. public void onRefreshComplete() {
  317. resetHeader();
  318. // 如果refreshview在加载结束后可见,下滑到下一个条目
  319. if (mRefreshView.getBottom() > 0) {
  320. invalidateViews();
  321. setSelection(1);
  322. }
  323. }
  324. /**
  325. * 刷新监听器接口
  326. */
  327. public interface OnRefreshListener {
  328. /**
  329. * list需要被刷新时调用
  330. */
  331. public void onRefresh();
  332. }
  333. }

相信我注释已经写的比较详细了,主要注意
onTouchEvent和onScroll方法,在这里面计算头部边距,从而通过用户的手势实现“下拉刷新”到“松开加载”以及“加载”三个状态的切
换。其中还有一系列和header有关的方法,用来设置header的显示以及取得header的边距。于此同时,代码留出了接口以供调用。

那么现在写一个测试Activity来试验下效果:

  1. package com.notice.pullrefresh;
  2. import java.util.Arrays;
  3. import java.util.LinkedList;
  4. import android.app.ListActivity;
  5. import android.os.AsyncTask;
  6. import android.os.Bundle;
  7. import android.widget.ArrayAdapter;
  8. import com.notice.pullrefresh.PullToRefreshListView.OnRefreshListener;
  9. public class PullrefreshActivity extends ListActivity {
  10. private LinkedList<String> mListItems;
  11. ArrayAdapter<String> adapter;
  12. /** Called when the activity is first created. */
  13. @Override
  14. public void onCreate(Bundle savedInstanceState) {
  15. super.onCreate(savedInstanceState);
  16. setContentView(R.layout.pull_to_refresh);
  17. // list需要刷新时调用
  18. ((PullToRefreshListView) getListView())
  19. .setOnRefreshListener(new OnRefreshListener() {
  20. @Override
  21. public void onRefresh() {
  22. // 在这执行后台工作
  23. new GetDataTask().execute();
  24. }
  25. });
  26. mListItems = new LinkedList<String>();
  27. mListItems.addAll(Arrays.asList(mStrings));
  28. adapter = new ArrayAdapter<String>(this,
  29. android.R.layout.simple_list_item_1, mListItems);
  30. setListAdapter(adapter);
  31. }
  32. private class GetDataTask extends AsyncTask<Void, Void, String[]> {
  33. @Override
  34. protected String[] doInBackground(Void... params) {
  35. // 在这里可以做一些后台工作
  36. try {
  37. Thread.sleep(2000);
  38. } catch (InterruptedException e) {
  39. e.printStackTrace();
  40. }
  41. return mStrings;
  42. }
  43. @Override
  44. protected void onPostExecute(String[] result) {
  45. // 下拉后增加的内容
  46. mListItems.addFirst("Added after refresh...");
  47. // 刷新完成调用该方法复位
  48. ((PullToRefreshListView) getListView()).onRefreshComplete();
  49. super.onPostExecute(result);
  50. }
  51. }
  52. private String[] mStrings = { "normal data1", "normal data2",
  53. "nomal data3", "normal data4", "norma data5", "normal data6" };
  54. }

代码通过asyncTask实现一个异步操作,并通过设置onRefreshListener监听器调用onRefresh方法实现下拉时刷新,并在刷新完成后调用onRefreshComplete做复位处理。