(转)android UI进阶之实现listview的下拉加载
关于listview的操作五花八门,有下拉刷新,分级显示,分页列表,逐页加载等,以后会陆续和大家分享这些技术,今天讲下下拉加载这个功能的实现。
最初的下拉加载应该是ios上的效果,现在很多应用如新浪微博等都加入了这个操作。即下拉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代码:
- <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- android:paddingTop="10dip"
- android:paddingBottom="15dip"
- android:gravity="center"
- android:id="@+id/pull_to_refresh_header"
- >
- <ProgressBar
- android:id="@+id/pull_to_refresh_progress"
- android:indeterminate="true"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginLeft="30dip"
- android:layout_marginRight="20dip"
- android:layout_marginTop="10dip"
- android:visibility="gone"
- android:layout_centerVertical="true"
- style="?android:attr/progressBarStyleSmall"
- />
- <ImageView
- android:id="@+id/pull_to_refresh_image"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginLeft="30dip"
- android:layout_marginRight="20dip"
- android:visibility="gone"
- android:layout_gravity="center"
- android:gravity="center"
- android:src="@drawable/ic_pulltorefresh_arrow"
- />
- <TextView
- android:id="@+id/pull_to_refresh_text"
- android:textAppearance="?android:attr/textAppearanceMedium"
- android:textStyle="bold"
- android:paddingTop="5dip"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:layout_gravity="center"
- android:gravity="center"
- />
- <TextView
- android:id="@+id/pull_to_refresh_updated_at"
- android:layout_below="@+id/pull_to_refresh_text"
- android:visibility="gone"
- android:textAppearance="?android:attr/textAppearanceSmall"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:layout_gravity="center"
- android:gravity="center"
- />
- </RelativeLayout>
代码比较简单,即headview包括一个进度条一个箭头和两段文字(一个显示加载状态,另一个显示最后刷新时间,本例就不设置了)。
而后重写listview,代码如下:
- package com.notice.pullrefresh;
- import android.content.Context;
- import android.util.AttributeSet;
- import android.view.LayoutInflater;
- import android.view.MotionEvent;
- import android.view.View;
- import android.view.ViewGroup;
- import android.view.animation.LinearInterpolator;
- import android.view.animation.RotateAnimation;
- import android.widget.AbsListView;
- import android.widget.AbsListView.OnScrollListener;
- import android.widget.ImageView;
- import android.widget.ListAdapter;
- import android.widget.ListView;
- import android.widget.ProgressBar;
- import android.widget.RelativeLayout;
- import android.widget.TextView;
- public class PullToRefreshListView extends ListView implements OnScrollListener {
- // 状态
- private static final int TAP_TO_REFRESH = 1;
- private static final int PULL_TO_REFRESH = 2;
- private static final int RELEASE_TO_REFRESH = 3;
- private static final int REFRESHING = 4;
- private OnRefreshListener mOnRefreshListener;
- // 监听对listview的滑动动作
- private OnScrollListener mOnScrollListener;
- private LayoutInflater mInflater;
- //顶部刷新时出现的控件
- private RelativeLayout mRefreshView;
- private TextView mRefreshViewText;
- private ImageView mRefreshViewImage;
- private ProgressBar mRefreshViewProgress;
- private TextView mRefreshViewLastUpdated;
- // 当前滑动状态
- private int mCurrentScrollState;
- // 当前刷新状态
- private int mRefreshState;
- // 箭头动画效果
- private RotateAnimation mFlipAnimation;
- private RotateAnimation mReverseFlipAnimation;
- private int mRefreshViewHeight;
- private int mRefreshOriginalTopPadding;
- private int mLastMotionY;
- private boolean mBounceHack;
- public PullToRefreshListView(Context context) {
- super(context);
- init(context);
- }
- public PullToRefreshListView(Context context, AttributeSet attrs) {
- super(context, attrs);
- init(context);
- }
- public PullToRefreshListView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- init(context);
- }
- /**
- * 初始化控件和箭头动画(这里直接在代码中初始化动画而不是通过xml)
- */
- private void init(Context context) {
- mFlipAnimation = new RotateAnimation(0, -180,
- RotateAnimation.RELATIVE_TO_SELF, 0.5f,
- RotateAnimation.RELATIVE_TO_SELF, 0.5f);
- mFlipAnimation.setInterpolator(new LinearInterpolator());
- mFlipAnimation.setDuration(250);
- mFlipAnimation.setFillAfter(true);
- mReverseFlipAnimation = new RotateAnimation(-180, 0,
- RotateAnimation.RELATIVE_TO_SELF, 0.5f,
- RotateAnimation.RELATIVE_TO_SELF, 0.5f);
- mReverseFlipAnimation.setInterpolator(new LinearInterpolator());
- mReverseFlipAnimation.setDuration(250);
- mReverseFlipAnimation.setFillAfter(true);
- mInflater = (LayoutInflater) context.getSystemService(
- Context.LAYOUT_INFLATER_SERVICE);
- mRefreshView = (RelativeLayout) mInflater.inflate(
- R.layout.pull_to_refresh_header, this, false);
- mRefreshViewText =
- (TextView) mRefreshView.findViewById(R.id.pull_to_refresh_text);
- mRefreshViewImage =
- (ImageView) mRefreshView.findViewById(R.id.pull_to_refresh_image);
- mRefreshViewProgress =
- (ProgressBar) mRefreshView.findViewById(R.id.pull_to_refresh_progress);
- mRefreshViewLastUpdated =
- (TextView) mRefreshView.findViewById(R.id.pull_to_refresh_updated_at);
- mRefreshViewImage.setMinimumHeight(50);
- mRefreshOriginalTopPadding = mRefreshView.getPaddingTop();
- mRefreshState = TAP_TO_REFRESH;
- //为listview头部增加一个view
- addHeaderView(mRefreshView);
- super.setOnScrollListener(this);
- measureView(mRefreshView);
- mRefreshViewHeight = mRefreshView.getMeasuredHeight();
- }
- @Override
- protected void onAttachedToWindow() {
- setSelection(1);
- }
- @Override
- public void setAdapter(ListAdapter adapter) {
- super.setAdapter(adapter);
- setSelection(1);
- }
- /**
- * 设置滑动监听器
- *
- */
- @Override
- public void setOnScrollListener(AbsListView.OnScrollListener l) {
- mOnScrollListener = l;
- }
- /**
- * 注册一个list需要刷新时的回调接口
- *
- */
- public void setOnRefreshListener(OnRefreshListener onRefreshListener) {
- mOnRefreshListener = onRefreshListener;
- }
- /**
- * 设置标签显示何时最后被刷新
- *
- * @param lastUpdated
- * Last updated at.
- */
- public void setLastUpdated(CharSequence lastUpdated) {
- if (lastUpdated != null) {
- mRefreshViewLastUpdated.setVisibility(View.VISIBLE);
- mRefreshViewLastUpdated.setText(lastUpdated);
- } else {
- mRefreshViewLastUpdated.setVisibility(View.GONE);
- }
- }
- // 实现该方法处理触摸
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- final int y = (int) event.getY();
- mBounceHack = false;
- switch (event.getAction()) {
- case MotionEvent.ACTION_UP:
- if (!isVerticalScrollBarEnabled()) {
- setVerticalScrollBarEnabled(true);
- }
- if (getFirstVisiblePosition() == 0 && mRefreshState != REFRESHING) {
- // 拖动距离达到刷新需要
- if ((mRefreshView.getBottom() >= mRefreshViewHeight
- || mRefreshView.getTop() >= 0)
- && mRefreshState == RELEASE_TO_REFRESH) {
- // 把状态设置为正在刷新
- mRefreshState = REFRESHING;
- // 准备刷新
- prepareForRefresh();
- // 刷新
- onRefresh();
- } else if (mRefreshView.getBottom() < mRefreshViewHeight
- || mRefreshView.getTop() <= 0) {
- // 中止刷新
- resetHeader();
- setSelection(1);
- }
- }
- break;
- case MotionEvent.ACTION_DOWN:
- // 获得按下y轴位置
- mLastMotionY = y;
- break;
- case MotionEvent.ACTION_MOVE:
- // 计算边距
- applyHeaderPadding(event);
- break;
- }
- return super.onTouchEvent(event);
- }
- // 获得header的边距
- private void applyHeaderPadding(MotionEvent ev) {
- int pointerCount = ev.getHistorySize();
- for (int p = 0; p < pointerCount; p++) {
- if (mRefreshState == RELEASE_TO_REFRESH) {
- if (isVerticalFadingEdgeEnabled()) {
- setVerticalScrollBarEnabled(false);
- }
- int historicalY = (int) ev.getHistoricalY(p);
- // 计算申请的边距,除以1.7使得拉动效果更好
- int topPadding = (int) (((historicalY - mLastMotionY)
- - mRefreshViewHeight) / 1.7);
- mRefreshView.setPadding(
- mRefreshView.getPaddingLeft(),
- topPadding,
- mRefreshView.getPaddingRight(),
- mRefreshView.getPaddingBottom());
- }
- }
- }
- /**
- * 将head的边距重置为初始的数值
- */
- private void resetHeaderPadding() {
- mRefreshView.setPadding(
- mRefreshView.getPaddingLeft(),
- mRefreshOriginalTopPadding,
- mRefreshView.getPaddingRight(),
- mRefreshView.getPaddingBottom());
- }
- /**
- * 重置header为之前的状态
- */
- private void resetHeader() {
- if (mRefreshState != TAP_TO_REFRESH) {
- mRefreshState = TAP_TO_REFRESH;
- resetHeaderPadding();
- // 将刷新图标换成箭头
- mRefreshViewImage.setImageResource(R.drawable.ic_pulltorefresh_arrow);
- // 清除动画
- mRefreshViewImage.clearAnimation();
- // 隐藏图标和进度条
- mRefreshViewImage.setVisibility(View.GONE);
- mRefreshViewProgress.setVisibility(View.GONE);
- }
- }
- // 估算headview的width和height
- private void measureView(View child) {
- ViewGroup.LayoutParams p = child.getLayoutParams();
- if (p == null) {
- p = new ViewGroup.LayoutParams(
- ViewGroup.LayoutParams.FILL_PARENT,
- ViewGroup.LayoutParams.WRAP_CONTENT);
- }
- int childWidthSpec = ViewGroup.getChildMeasureSpec(0,
- 0 + 0, p.width);
- int lpHeight = p.height;
- int childHeightSpec;
- if (lpHeight > 0) {
- childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
- } else {
- childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
- }
- child.measure(childWidthSpec, childHeightSpec);
- }
- @Override
- public void onScroll(AbsListView view, int firstVisibleItem,
- int visibleItemCount, int totalItemCount) {
- // 在refreshview完全可见时,设置文字为松开刷新,同时翻转箭头
- if (mCurrentScrollState == SCROLL_STATE_TOUCH_SCROLL
- && mRefreshState != REFRESHING) {
- if (firstVisibleItem == 0) {
- mRefreshViewImage.setVisibility(View.VISIBLE);
- if ((mRefreshView.getBottom() >= mRefreshViewHeight + 20
- || mRefreshView.getTop() >= 0)
- && mRefreshState != RELEASE_TO_REFRESH) {
- mRefreshViewText.setText("松开加载...");
- mRefreshViewImage.clearAnimation();
- mRefreshViewImage.startAnimation(mFlipAnimation);
- mRefreshState = RELEASE_TO_REFRESH;
- } else if (mRefreshView.getBottom() < mRefreshViewHeight + 20
- && mRefreshState != PULL_TO_REFRESH) {
- mRefreshViewText.setText("下拉刷新...");
- if (mRefreshState != TAP_TO_REFRESH) {
- mRefreshViewImage.clearAnimation();
- mRefreshViewImage.startAnimation(mReverseFlipAnimation);
- }
- mRefreshState = PULL_TO_REFRESH;
- }
- } else {
- mRefreshViewImage.setVisibility(View.GONE);
- resetHeader();
- }
- } else if (mCurrentScrollState == SCROLL_STATE_FLING
- && firstVisibleItem == 0
- && mRefreshState != REFRESHING) {
- setSelection(1);
- mBounceHack = true;
- } else if (mBounceHack && mCurrentScrollState == SCROLL_STATE_FLING) {
- setSelection(1);
- }
- if (mOnScrollListener != null) {
- mOnScrollListener.onScroll(view, firstVisibleItem,
- visibleItemCount, totalItemCount);
- }
- }
- @Override
- public void onScrollStateChanged(AbsListView view, int scrollState) {
- mCurrentScrollState = scrollState;
- if (mCurrentScrollState == SCROLL_STATE_IDLE) {
- mBounceHack = false;
- }
- if (mOnScrollListener != null) {
- mOnScrollListener.onScrollStateChanged(view, scrollState);
- }
- }
- public void prepareForRefresh() {
- resetHeaderPadding();// 恢复header的边距
- mRefreshViewImage.setVisibility(View.GONE);
- // 注意加上,否则仍然显示之前的图片
- mRefreshViewImage.setImageDrawable(null);
- mRefreshViewProgress.setVisibility(View.VISIBLE);
- // 设置文字
- mRefreshViewText.setText("加载中...");
- mRefreshState = REFRESHING;
- }
- public void onRefresh() {
- if (mOnRefreshListener != null) {
- mOnRefreshListener.onRefresh();
- }
- }
- /**
- * 重置listview为普通的listview,该方法设置最后更新时间
- *
- * @param lastUpdated
- * Last updated at.
- */
- public void onRefreshComplete(CharSequence lastUpdated) {
- setLastUpdated(lastUpdated);
- onRefreshComplete();
- }
- /**
- * 重置listview为普通的listview,不设置最后更新时间
- */
- public void onRefreshComplete() {
- resetHeader();
- // 如果refreshview在加载结束后可见,下滑到下一个条目
- if (mRefreshView.getBottom() > 0) {
- invalidateViews();
- setSelection(1);
- }
- }
- /**
- * 刷新监听器接口
- */
- public interface OnRefreshListener {
- /**
- * list需要被刷新时调用
- */
- public void onRefresh();
- }
- }
相信我注释已经写的比较详细了,主要注意 onTouchEvent和onScroll方法,在这里面计算头部边距,从而通过用户的手势实现“下拉刷新”到“松开加载”以及“加载”三个状态的切 换。其中还有一系列和header有关的方法,用来设置header的显示以及取得header的边距。于此同时,代码留出了接口以供调用。
那么现在写一个测试Activity来试验下效果:
- package com.notice.pullrefresh;
- import java.util.Arrays;
- import java.util.LinkedList;
- import android.app.ListActivity;
- import android.os.AsyncTask;
- import android.os.Bundle;
- import android.widget.ArrayAdapter;
- import com.notice.pullrefresh.PullToRefreshListView.OnRefreshListener;
- public class PullrefreshActivity extends ListActivity {
- private LinkedList<String> mListItems;
- ArrayAdapter<String> adapter;
- /** Called when the activity is first created. */
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.pull_to_refresh);
- // list需要刷新时调用
- ((PullToRefreshListView) getListView())
- .setOnRefreshListener(new OnRefreshListener() {
- @Override
- public void onRefresh() {
- // 在这执行后台工作
- new GetDataTask().execute();
- }
- });
- mListItems = new LinkedList<String>();
- mListItems.addAll(Arrays.asList(mStrings));
- adapter = new ArrayAdapter<String>(this,
- android.R.layout.simple_list_item_1, mListItems);
- setListAdapter(adapter);
- }
- private class GetDataTask extends AsyncTask<Void, Void, String[]> {
- @Override
- protected String[] doInBackground(Void... params) {
- // 在这里可以做一些后台工作
- try {
- Thread.sleep(2000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- return mStrings;
- }
- @Override
- protected void onPostExecute(String[] result) {
- // 下拉后增加的内容
- mListItems.addFirst("Added after refresh...");
- // 刷新完成调用该方法复位
- ((PullToRefreshListView) getListView()).onRefreshComplete();
- super.onPostExecute(result);
- }
- }
- private String[] mStrings = { "normal data1", "normal data2",
- "nomal data3", "normal data4", "norma data5", "normal data6" };
- }
代码通过asyncTask实现一个异步操作,并通过设置onRefreshListener监听器调用onRefresh方法实现下拉时刷新,并在刷新完成后调用onRefreshComplete做复位处理。