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

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

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

 

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

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

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

上一篇:android 线程


下一篇:Android学习整理之Activity生命周期篇