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

来源:互联网 发布:生鲜配送软件 编辑:程序博客网 时间:2024/05/11 20:07

上下拉实现刷新和加载更多的ListView,如下:

[java] view plaincopyprint?
  1. package com.sin.android.ui;  
  2.   
  3. import android.content.Context;  
  4. import android.util.AttributeSet;  
  5. import android.view.Gravity;  
  6. import android.view.MotionEvent;  
  7. import android.view.View;  
  8. import android.widget.AbsListView;  
  9. import android.widget.AbsListView.OnScrollListener;  
  10. import android.widget.LinearLayout;  
  11. import android.widget.ListView;  
  12. import android.widget.ProgressBar;  
  13. import android.widget.TextView;  
  14.   
  15.   
  16. /** 
  17.  * 动态刷新和加载数据ListView 
  18.  * @author RobinTang 
  19.  * 
  20.  */  
  21. public class DynamicListView extends ListView implements OnScrollListener {  
  22.   
  23.     /** 
  24.      * 监听器 
  25.      * 监听控件的刷新或者加载更多事件 
  26.      * 所有的条目事件都会有一个偏移量,也就是position应该减1才是你适配器中的条目 
  27.      * @author RobinTang 
  28.      * 
  29.      */  
  30.     public interface DynamicListViewListener {  
  31.         /** 
  32.          *  
  33.          * @param dynamicListView 
  34.          * @param isRefresh 为true的时候代表的是刷新,为false的时候代表的是加载更多 
  35.          * @return true:刷新或者加载更多动作完成,刷新或者加载更多的动画自动消失 false:刷新或者加载更多为完成,需要在数据加载完成之后去调用控件的doneRefresh()或者doneMore()方法  
  36.          */  
  37.         public boolean onRefreshOrMore(DynamicListView dynamicListView, boolean isRefresh);  
  38.     }  
  39.       
  40.   
  41.     /** 
  42.      * 状态控件(StatusView,列表头上和底端的)的状态枚举 
  43.      * @author RobinTang 
  44.      * 
  45.      */  
  46.     enum RefreshStatus {  
  47.         none, normal, willrefresh, refreshing  
  48.     }  
  49.       
  50.     /** 
  51.      * 状态控件 
  52.      * @author RobinTang 
  53.      * 
  54.      */  
  55.     class StatusView extends LinearLayout {  
  56.         public int height;  
  57.         public int width;  
  58.         private ProgressBar progressBar = null;  
  59.         private TextView textView = null;  
  60.         private RefreshStatus refreshStatus = RefreshStatus.none;  
  61.         private String normalString = "下拉刷新";  
  62.         private String willrefreshString = "松开刷新";  
  63.         private String refreshingString = "正在刷新";  
  64.   
  65.         public StatusView(Context context, AttributeSet attrs) {  
  66.             super(context, attrs);  
  67.             initThis(context);  
  68.         }  
  69.   
  70.         public StatusView(Context context) {  
  71.             super(context);  
  72.             initThis(context);  
  73.         }  
  74.   
  75.         private void initThis(Context context) {  
  76.             this.setOrientation(LinearLayout.HORIZONTAL);  
  77.             this.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.CENTER_VERTICAL);  
  78.   
  79.             progressBar = new ProgressBar(context);  
  80.             progressBar.setLayoutParams(new LinearLayout.LayoutParams(3030));  
  81.             textView = new TextView(context);  
  82.             textView.setPadding(5000);  
  83.   
  84.             this.addView(progressBar);  
  85.             this.addView(textView);  
  86.   
  87.             int w = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);  
  88.             int h = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);  
  89.             this.measure(w, h);  
  90.   
  91.             height = this.getMeasuredHeight();  
  92.             width = this.getMeasuredWidth();  
  93.   
  94.             this.setRefreshStatus(RefreshStatus.normal);  
  95.         }  
  96.   
  97.         public RefreshStatus getRefreshStatus() {  
  98.             return refreshStatus;  
  99.         }  
  100.   
  101.         public void setRefreshStatus(RefreshStatus refreshStatus) {  
  102.             if (this.refreshStatus != refreshStatus) {  
  103.                 this.refreshStatus = refreshStatus;  
  104.                 if(refreshStatus == RefreshStatus.refreshing){  
  105.                     this.progressBar.setVisibility(View.VISIBLE);  
  106.                 }  
  107.                 else{  
  108.                     this.progressBar.setVisibility(View.GONE);  
  109.                 }  
  110.                 refreshStatusString();  
  111.                 this.invalidate();  
  112.             }  
  113.         }  
  114.   
  115.         private void refreshStatusString() {  
  116.             switch (refreshStatus) {  
  117.             case normal:  
  118.                 textView.setText(normalString);  
  119.                 progressBar.setProgress(0);  
  120.                 break;  
  121.             case willrefresh:  
  122.                 textView.setText(willrefreshString);  
  123.                 break;  
  124.             case refreshing:  
  125.                 textView.setText(refreshingString);  
  126.                 break;  
  127.             default:  
  128.                 break;  
  129.             }  
  130.         }  
  131.           
  132.         /** 
  133.          * 设置状态字符串 
  134.          * @param normalString  平时的字符串 
  135.          * @param willrefreshString 松开后刷新(或加载)的字符串 
  136.          * @param refreshingString  正在刷新(或加载)的字符串 
  137.          */  
  138.         public void setStatusStrings(String normalString, String willrefreshString, String refreshingString){  
  139.             this.normalString = normalString;  
  140.             this.willrefreshString = willrefreshString;  
  141.             this.refreshingString = refreshingString;  
  142.             this.refreshStatusString();  
  143.         }  
  144.     }  
  145.   
  146.     private StatusView refreshView;  
  147.     private StatusView moreView;  
  148.     private int itemFlag = -1;  
  149.     private boolean isRecorded = false;  
  150.     private int downY = -1;  
  151.     private final float minTimesToRefresh = 1.5f;  
  152.     private final static int ITEM_FLAG_FIRST = 1;  
  153.     private final static int ITEM_FLAG_NONE = 0;  
  154.     private final static int ITEM_FLAG_LAST = -1;  
  155.       
  156.     // 两个监听器  
  157.     private DynamicListViewListener onRefreshListener;  
  158.     private DynamicListViewListener onMoreListener;  
  159.     // 滚动到低端的时候是否自动加载更多  
  160.     private boolean doMoreWhenBottom = false;  
  161.       
  162.   
  163.     public DynamicListView(Context context, AttributeSet attrs, int defStyle) {  
  164.         super(context, attrs, defStyle);  
  165.         initThis(context);  
  166.     }  
  167.   
  168.     public DynamicListView(Context context, AttributeSet attrs) {  
  169.         super(context, attrs);  
  170.         initThis(context);  
  171.     }  
  172.   
  173.     public DynamicListView(Context context) {  
  174.         super(context);  
  175.         initThis(context);  
  176.     }  
  177.   
  178.     private void initThis(Context context) {  
  179.         refreshView = new StatusView(context);  
  180.         moreView = new StatusView(context);  
  181.         refreshView.setStatusStrings("继续下拉刷新数据...""松开之后刷新数据...""正在刷新数据...");  
  182.         moreView.setStatusStrings("继续上拉加载数据...""松开之后加载数据...""正在加载数据...");  
  183.         this.addHeaderView(refreshView, nullfalse);  
  184.         this.addFooterView(moreView, nullfalse);  
  185.         this.setOnScrollListener(this);  
  186.         doneRefresh();  
  187.         doneMore();  
  188.     }  
  189.   
  190.       
  191.     // 监听器操作  
  192.     public DynamicListViewListener getOnRefreshListener() {  
  193.         return onRefreshListener;  
  194.     }  
  195.   
  196.     public void setOnRefreshListener(DynamicListViewListener onRefreshListener) {  
  197.         this.onRefreshListener = onRefreshListener;  
  198.     }  
  199.   
  200.     public DynamicListViewListener getOnMoreListener() {  
  201.         return onMoreListener;  
  202.     }  
  203.   
  204.     public void setOnMoreListener(DynamicListViewListener onMoreListener) {  
  205.         this.onMoreListener = onMoreListener;  
  206.     }  
  207.   
  208.     // 设置  
  209.     public boolean isDoMoreWhenBottom() {  
  210.         return doMoreWhenBottom;  
  211.     }  
  212.   
  213.     public void setDoMoreWhenBottom(boolean doMoreWhenBottom) {  
  214.         this.doMoreWhenBottom = doMoreWhenBottom;  
  215.     }  
  216.   
  217.     @Override  
  218.     public void onScroll(AbsListView l, int t, int oldl, int count) {  
  219.         // log("%d %d %d", t, oldl, count);  
  220.         if (t == 0)  
  221.             itemFlag = ITEM_FLAG_FIRST;  
  222.         else if ((t + oldl) == count){  
  223.             itemFlag = ITEM_FLAG_LAST;  
  224.             if(doMoreWhenBottom && onMoreListener != null && moreView.getRefreshStatus() != RefreshStatus.refreshing){  
  225.                     doMore();  
  226.             }  
  227.         }  
  228.         else {  
  229.             itemFlag = ITEM_FLAG_NONE;  
  230. //          isRecorded = false;  
  231.         }  
  232.     }  
  233.   
  234.     @Override  
  235.     public void onScrollStateChanged(AbsListView arg0, int arg1) {  
  236.   
  237.     }  
  238.   
  239.     @Override  
  240.     public boolean onTouchEvent(MotionEvent ev) {  
  241.         switch (ev.getAction()) {  
  242.         case MotionEvent.ACTION_DOWN:  
  243.             if (isRecorded == false && (itemFlag == ITEM_FLAG_FIRST && onRefreshListener != null && refreshView.getRefreshStatus() == RefreshStatus.normal || itemFlag == ITEM_FLAG_LAST && onMoreListener != null && moreView.getRefreshStatus() == RefreshStatus.normal)) {  
  244.                 downY = (int) ev.getY(0);  
  245.                 isRecorded = true;  
  246. //              log("按下,记录:%d flag:%d", downY, itemFlag);  
  247.             }  
  248.             break;  
  249.         case MotionEvent.ACTION_UP: {  
  250.             isRecorded = false;  
  251.             if (onRefreshListener != null && refreshView.getRefreshStatus() == RefreshStatus.willrefresh) {  
  252.                 doRefresh();  
  253.             } else if (refreshView.getRefreshStatus() == RefreshStatus.normal) {  
  254.                 refreshView.setPadding(0, -1 * refreshView.height, 00);  
  255.             }  
  256.   
  257.             if (onMoreListener != null && moreView.getRefreshStatus() == RefreshStatus.willrefresh) {  
  258.                 doMore();  
  259.             } else if (moreView.getRefreshStatus() == RefreshStatus.normal) {  
  260.                 moreView.setPadding(000, -1 * moreView.height);  
  261.             }  
  262.             break;  
  263.         }  
  264.         case MotionEvent.ACTION_MOVE: {  
  265.             if (isRecorded == false && (itemFlag == ITEM_FLAG_FIRST && onRefreshListener != null && refreshView.getRefreshStatus() == RefreshStatus.normal ||   
  266.                     itemFlag == ITEM_FLAG_LAST && onMoreListener != null && moreView.getRefreshStatus() == RefreshStatus.normal)) {  
  267.                 downY = (int) ev.getY(0);  
  268.                 isRecorded = true;  
  269. //              log("按下,记录:%d flag:%d", downY, itemFlag);  
  270.             } else if (isRecorded) {  
  271.                 int nowY = (int) ev.getY(0);  
  272.                 int offset = nowY - downY;  
  273.                 if (offset > 0 && itemFlag == ITEM_FLAG_FIRST) {  
  274.                     // 下拉  
  275.                     setSelection(0);  
  276.                     if (offset >= (minTimesToRefresh * refreshView.height)) {  
  277.                         refreshView.setRefreshStatus(RefreshStatus.willrefresh);  
  278.                     } else {  
  279.                         refreshView.setRefreshStatus(RefreshStatus.normal);  
  280.                     }  
  281.   
  282.                     refreshView.setPadding(0, -1 * (refreshView.height - offset), 00);  
  283.                 } else if(itemFlag == ITEM_FLAG_LAST){  
  284.                     // 上拉  
  285.                     setSelection(this.getCount());  
  286.                     if (offset <= -1 * (minTimesToRefresh * moreView.height)) {  
  287.                         moreView.setRefreshStatus(RefreshStatus.willrefresh);  
  288.                     } else {  
  289.                         moreView.setRefreshStatus(RefreshStatus.normal);  
  290.                     }  
  291.                     moreView.setPadding(000, -1 * (moreView.height + offset));  
  292.                 }  
  293. //              log("位移:%d", offset);  
  294.             }  
  295.             break;  
  296.         }  
  297.         default:  
  298.             break;  
  299.         }  
  300.         return super.onTouchEvent(ev);  
  301.     }  
  302.   
  303.     /** 
  304.      * 开始刷新 
  305.      */  
  306.     private void doRefresh(){  
  307. //      log("开始刷新");  
  308.         refreshView.setRefreshStatus(RefreshStatus.refreshing);  
  309.         refreshView.setPadding(0000);  
  310.         if(onRefreshListener.onRefreshOrMore(thistrue))  
  311.             doneRefresh();  
  312.     }  
  313.       
  314.     /** 
  315.      * 开始加载更多 
  316.      */  
  317.     private void doMore(){  
  318. //      log("加载更多");  
  319.         moreView.setRefreshStatus(RefreshStatus.refreshing);  
  320.         moreView.setPadding(0000);  
  321.         if(onMoreListener.onRefreshOrMore(thisfalse))  
  322.             doneMore();  
  323.     }  
  324.       
  325.     /** 
  326.      * 刷新完成之后调用,用于取消刷新的动画 
  327.      */  
  328.     public void doneRefresh() {  
  329. //      log("刷新完成!");  
  330.         refreshView.setRefreshStatus(RefreshStatus.normal);  
  331.         refreshView.setPadding(0, -1 * refreshView.height, 00);  
  332.     }  
  333.       
  334.     /** 
  335.      * 加载更多完成之后调用,用于取消加载更多的动画 
  336.      */  
  337.     public void doneMore() {  
  338. //      log("加载完成!");  
  339.         moreView.setRefreshStatus(RefreshStatus.normal);  
  340.         moreView.setPadding(000, -1 * moreView.height);  
  341.     }  
  342.   
  343.     /** 
  344.      * 获取刷新的状态 
  345.      * @return  一般 将要刷新 刷新完成 
  346.      */  
  347.     public RefreshStatus getRefreshStatus(){  
  348.         return refreshView.getRefreshStatus();  
  349.     }  
  350.     /** 
  351.      * 获取加载更多的状态 
  352.      * @return  一般 将要加载 加载完成 
  353.      */  
  354.     public RefreshStatus getMoreStatus(){  
  355.         return moreView.getRefreshStatus();  
  356.     }  
  357.       
  358. //  private void log(Object obj) {  
  359. //      log("%s", obj.toString());  
  360. //  }  
  361. //  
  362. //  private void log(String format, Object... args) {  
  363. //      Log.i("DynamicListView", String.format(format, args));  
  364. //  }  
  365. }  

使用例子:

[java] view plaincopyprint?
  1. package com.sin.android.ui;  
  2.   
  3. import java.util.ArrayList;  
  4. import java.util.Date;  
  5.   
  6. import android.annotation.SuppressLint;  
  7. import android.app.Activity;  
  8. import android.os.Bundle;  
  9. import android.os.Handler;  
  10. import android.os.Message;  
  11. import android.util.Log;  
  12. import android.view.View;  
  13. import android.widget.AdapterView;  
  14. import android.widget.AdapterView.OnItemClickListener;  
  15. import android.widget.ArrayAdapter;  
  16. import android.widget.Toast;  
  17.   
  18. import com.sin.android.ui.DynamicListView.DynamicListViewListener;  
  19.   
  20. @SuppressLint("HandlerLeak")  
  21. public class MainActivity extends Activity implements DynamicListViewListener {  
  22.     DynamicListView listView;  
  23.     ArrayList<String> data;  
  24.     ArrayAdapter<String> adapter;  
  25.       
  26.     // 用于刷新控件状态  
  27.     Handler handler = new Handler() {  
  28.         @Override  
  29.         public void handleMessage(Message msg) {  
  30.             if (msg.what == 0) {  
  31.                 adapter.notifyDataSetChanged();  
  32.                 listView.doneRefresh();  
  33.                 Toast.makeText(MainActivity.this"新加载"+msg.arg1+"条数据!", Toast.LENGTH_LONG).show();  
  34.             } else if (msg.what == 1) {  
  35.                 adapter.notifyDataSetChanged();  
  36.                 listView.doneMore();  
  37.             } else {  
  38.                 super.handleMessage(msg);  
  39.             }  
  40.         }  
  41.     };  
  42.   
  43.     @Override  
  44.     protected void onCreate(Bundle savedInstanceState) {  
  45.         super.onCreate(savedInstanceState);  
  46.         listView = new DynamicListView(this);  
  47.         setContentView(listView);  
  48.   
  49.         data = new ArrayList<String>();  
  50.         for (int i = 1; i < 10; ++i) {  
  51.             data.add("原始数据" + i);  
  52.         }  
  53.         adapter = new ArrayAdapter<String>(this, android.R.layout.simple_expandable_list_item_1, data);  
  54.         listView.setAdapter(adapter);  
  55.         listView.setOnItemClickListener(new OnItemClickListener() {  
  56.             @Override  
  57.             public void onItemClick(AdapterView<?> arg0, View arg1, int arg2, long arg3) {  
  58.                 Log.i("",data.get(arg2-1));  
  59.             }  
  60.         });  
  61.         listView.setDoMoreWhenBottom(false);    // 滚动到低端的时候不自己加载更多  
  62.         listView.setOnRefreshListener(this);  
  63.         listView.setOnMoreListener(this);  
  64.     }  
  65.   
  66.     @Override  
  67.     public boolean onRefreshOrMore(DynamicListView dynamicListView, boolean isRefresh) {  
  68.         if (isRefresh) {  
  69.             new Thread(new Runnable() {  
  70.                 @Override  
  71.                 public void run() {  
  72.                     // 刷新  
  73.                     ArrayList<String> temp = new ArrayList<String>();  
  74.                     for (int i = 0; i < 3; ++i) {  
  75.                         temp.add(0new Date().toLocaleString());  
  76.                         try {  
  77.                             Thread.sleep(1000);  
  78.                         } catch (InterruptedException e) {  
  79.                             e.printStackTrace();  
  80.                         }  
  81.                     }  
  82.                     synchronized (data) {  
  83.                         data.addAll(0, temp);  
  84.                     }  
  85.                       
  86.                     Message message = new Message();  
  87.                     message.what = 0;  
  88.                     message.arg1 = temp.size();  
  89.                     handler.sendMessage(message);  
  90.                 }  
  91.             }).start();  
  92.         } else {  
  93.             new Thread(new Runnable() {  
  94.                 @Override  
  95.                 public void run() {  
  96.                     // 加载更多  
  97.                     ArrayList<String> temp = new ArrayList<String>();  
  98.                     for (int i = 0; i < 3; ++i) {  
  99.                         temp.add(new Date().toLocaleString());  
  100.                         try {  
  101.                             Thread.sleep(1000);  
  102.                         } catch (InterruptedException e) {  
  103.                             e.printStackTrace();  
  104.                         }  
  105.                     }  
  106.                     synchronized (data) {  
  107.                         data.addAll(temp);  
  108.                     }  
  109.                     handler.sendEmptyMessage(1);  
  110.                 }  
  111.             }).start();  
  112.         }  
  113.         return false;  
  114.     }  
  115. }  


截图:










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

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

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

                            

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

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

这个类是整个下拉刷新功能中最重要的一个类,注释已经写得比较详细了,我再简单解释一下。首先在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作为程序主界面的布局,加入如下代码:
[html] view plaincopy
  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.   
  7.     <com.example.pulltorefreshtest.RefreshableView  
  8.         android:id="@+id/refreshable_view"  
  9.         android:layout_width="fill_parent"  
  10.         android:layout_height="fill_parent" >  
  11.   
  12.         <ListView  
  13.             android:id="@+id/list_view"  
  14.             android:layout_width="fill_parent"  
  15.             android:layout_height="fill_parent" >  
  16.         </ListView>  
  17.     </com.example.pulltorefreshtest.RefreshableView>  
  18.   
  19. </RelativeLayout>  
可以看到,我们在自定义的RefreshableView中加入了一个ListView,这就意味着给这个ListView加入了下拉刷新的功能,就是这么简单!
然后我们再来看一下程序的主Activity,打开或新建MainActivity,加入如下代码:
[java] view plaincopy
  1. public class MainActivity extends Activity {  
  2.   
  3.     RefreshableView refreshableView;  
  4.     ListView listView;  
  5.     ArrayAdapter<String> adapter;  
  6.     String[] items = { "A""B""C""D""E""F""G""H""I""J""K""L" };  
  7.   
  8.     @Override  
  9.     protected void onCreate(Bundle savedInstanceState) {  
  10.         super.onCreate(savedInstanceState);  
  11.         requestWindowFeature(Window.FEATURE_NO_TITLE);  
  12.         setContentView(R.layout.activity_main);  
  13.         refreshableView = (RefreshableView) findViewById(R.id.refreshable_view);  
  14.         listView = (ListView) findViewById(R.id.list_view);  
  15.         adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, items);  
  16.         listView.setAdapter(adapter);  
  17.         refreshableView.setOnRefreshListener(new PullToRefreshListener() {  
  18.             @Override  
  19.             public void onRefresh() {  
  20.                 try {  
  21.                     Thread.sleep(3000);  
  22.                 } catch (InterruptedException e) {  
  23.                     e.printStackTrace();  
  24.                 }  
  25.                 refreshableView.finishRefreshing();  
  26.             }  
  27.         }, 0);  
  28.     }  
  29.   
  30. }  

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

                                                 
效果看起来还是非常不错的。我们最后再来总结一下,在项目中引入ListView下拉刷新功能只需三步:

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

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

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

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

好了,今天的讲解到此结束,有疑问的朋友请在下面留言。

源码下载,请点击这里






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() {@Overridepublic 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[]> {@Overrideprotected String[] doInBackground(Void... params) {// 在这里可以做一些后台工作try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}return mStrings;}@Overrideprotected 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做复位处理。

android UI进阶之实现listview的分页加载

上篇博文和大家分享了下拉刷新,这是一个用户体验非常好的操作方式。新浪微薄就是使用这种方式的典型。

还有个问题,当用户从网络上读取微薄的时候,如果一下子全部加载用户未读的微薄这将耗费比较长的时间,造成不好的用户体验,同时一屏的内容也不足以显示如此多的内容。这时候,我们就需要用到另一个功能,那就是listview的分页了。通过分页分次加载数据,用户看多少就去加载多少。

通常这也分为两种方式,一种是设置一个按钮,用户点击即加载。另一种是当用户滑动到底部时自动加载。今天我就和大家分享一下这个功能的实现。

首先,写一个xml文件,moredata.xml,该文件即定义了放在listview底部的视图:

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:orientation="vertical" >  <Button          android:id="@+id/bt_load"          android:layout_width="fill_parent"          android:layout_height="wrap_content"        android:text="加载更多数据" />   <ProgressBar      android:id="@+id/pg"      android:layout_width="wrap_content"      android:layout_height="wrap_content"      android:layout_gravity="center_horizontal"      android:visibility="gone"      /></LinearLayout> 

可以看到是一个按钮和一个进度条。因为只做一个演示,这里简单处理,通过设置控件的visibility,未加载时显示按钮,加载时就显示进度条。

写一个item.xml,大家应该很熟悉了。用来定义listview的每个item的视图。

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:orientation="vertical" >        <TextView        android:id="@+id/tv_title"        android:textSize="20sp"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_marginTop="5dp"        />    <TextView        android:textSize="12sp"        android:id="@+id/tv_content"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_marginTop="5dp"        /></LinearLayout>

main.xml就不贴了,整个主界面就一个listview。

直接先看下Activity的代码,在里面实现分页效果。

package com.notice.moredate;import java.util.ArrayList;import java.util.HashMap;import android.app.Activity;import android.os.Bundle;import android.os.Handler;import android.view.View;import android.view.View.OnClickListener;import android.widget.AbsListView;import android.widget.AbsListView.OnScrollListener;import android.widget.Button;import android.widget.ListView;import android.widget.ProgressBar;import android.widget.SimpleAdapter;import android.widget.Toast;public class MoreDateListActivity extends Activity implements OnScrollListener {        // ListView的Adapter    private SimpleAdapter mSimpleAdapter;    private ListView lv;    private Button bt;    private ProgressBar pg;    private ArrayList<HashMap<String,String>> list;    // ListView底部View    private View moreView;    private Handler handler;    // 设置一个最大的数据条数,超过即不再加载    private int MaxDateNum;    // 最后可见条目的索引    private int lastVisibleIndex;        /** Called when the activity is first created. */    @Override    public void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.main);                        MaxDateNum = 22; // 设置最大数据条数        lv = (ListView) findViewById(R.id.lv);        // 实例化底部布局        moreView = getLayoutInflater().inflate(R.layout.moredate, null);        bt = (Button) moreView.findViewById(R.id.bt_load);        pg = (ProgressBar) moreView.findViewById(R.id.pg);        handler = new Handler();        // 用map来装载数据,初始化10条数据        list = new ArrayList<HashMap<String,String>>();        for (int i = 0; i < 10; i++) {            HashMap<String, String> map = new HashMap<String, String>();            map.put("ItemTitle", "第" + i + "行标题");            map.put("ItemText", "第" + i + "行内容");            list.add(map);        }        // 实例化SimpleAdapter        mSimpleAdapter = new SimpleAdapter(this, list, R.layout.item,                new String[] { "ItemTitle", "ItemText" },                new int[] { R.id.tv_title, R.id.tv_content });        // 加上底部View,注意要放在setAdapter方法前        lv.addFooterView(moreView);        lv.setAdapter(mSimpleAdapter);        // 绑定监听器        lv.setOnScrollListener(this);        bt.setOnClickListener(new OnClickListener() {            @Override            public void onClick(View v) {                pg.setVisibility(View.VISIBLE);// 将进度条可见                bt.setVisibility(View.GONE);// 按钮不可见                handler.postDelayed(new Runnable() {                    @Override                    public void run() {                        loadMoreDate();// 加载更多数据                        bt.setVisibility(View.VISIBLE);                        pg.setVisibility(View.GONE);                        mSimpleAdapter.notifyDataSetChanged();// 通知listView刷新数据                    }                }, 2000);            }        });    }    private void loadMoreDate() {        int count = mSimpleAdapter.getCount();        if (count + 5 < MaxDateNum) {            // 每次加载5条            for (int i = count; i < count + 5; i++) {                HashMap<String, String> map = new HashMap<String, String>();                map.put("ItemTitle", "新增第" + i + "行标题");                map.put("ItemText", "新增第" + i + "行内容");                list.add(map);            }        } else {            // 数据已经不足5条            for (int i = count; i < MaxDateNum; i++) {                HashMap<String, String> map = new HashMap<String, String>();                map.put("ItemTitle", "新增第" + i + "行标题");                map.put("ItemText", "新增第" + i + "行内容");                list.add(map);            }        }    }    @Override    public void onScroll(AbsListView view, int firstVisibleItem,            int visibleItemCount, int totalItemCount) {        // 计算最后可见条目的索引        lastVisibleIndex = firstVisibleItem + visibleItemCount - 1;        // 所有的条目已经和最大条数相等,则移除底部的View        if (totalItemCount == MaxDateNum + 1) {            lv.removeFooterView(moreView);            Toast.makeText(this, "数据全部加载完成,没有更多数据!", Toast.LENGTH_LONG).show();        }    }    @Override    public void onScrollStateChanged(AbsListView view, int scrollState) {        // 滑到底部后自动加载,判断listview已经停止滚动并且最后可视的条目等于adapter的条目        if (scrollState == OnScrollListener.SCROLL_STATE_IDLE                && lastVisibleIndex == mSimpleAdapter.getCount()) {            // 当滑到底部时自动加载            // pg.setVisibility(View.VISIBLE);            // bt.setVisibility(View.GONE);            // handler.postDelayed(new Runnable() {            //            // @Override            // public void run() {            // loadMoreDate();            // bt.setVisibility(View.VISIBLE);            // pg.setVisibility(View.GONE);            // mSimpleAdapter.notifyDataSetChanged();            // }            //            // }, 2000);        }    }    } 

通过注释,大家应该很容易理解了。这里做下简单的解析。首先要注意的是,addFootView方法一定要在setAdapter方法之前,否则会无效。addFootView方法为listview底部加入一个视图,在本例中就是那个Button加progressbar的视图。当用户点击按钮时,调用loadmoreDate方法,为listview绑定更多的数据,通过adapter的notifyDataSetChanged方法通知listview刷新,显示刚加入的数据。

这里用handler异步延迟2秒操作,模仿加载过程。同时listview绑定了onScrollListener监听器,并且实现了onScroll和onScrollStateChanged方法。在后者方法中,我们通过判断listview已经停止滚动并且最后可视的条目等于adapter的条目,可以知道用户已经滑动到底部并且自动加载,代码中将这部分代码注释掉了,大家可以自己试下。

代码中还加入了一个MaxDateNum变量,用来记录最大的数据数量。也就是说网络或者其他地方一共的数据。通过onScroll方法判断用户加载完这些数据后,移除listview底部视图,不让继续加载。同时在loadmoreDate方法中也对最大数据量做相应的操作来判断加载数量。(默认加载5条,不足5条时加载剩余的)。

看下效果图:

就写这么多了,总的来说还是很简单的,但是确实非常有用的一个效果。

android UI进阶之实现listview中checkbox的多选与记录

今天继续和大家分享涉及到listview的内容。在很多时候,我们会用到listview和checkbox配合来提供给用户一些选择操作。比如在一个清单页面,我们需要记录用户勾选了哪些条目。这个的实现并不太难,但是有很多朋友来问我如何实现,他们有遇到各种各样的问题,这里就一并写出来和大家一起分享。

ListView的操作就一定会涉及到item和Adapter,我们还是先来实现这部分内容。

首先,写个item的xml布局,里面放置一个TextView和一个CheckBox。要注意的时候,这里我设置了CheckBox没有焦点,这样的话,无法单独点击checkbox,而是在点击listview的条目后,Checkbox会响应操作。

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="fill_parent"    android:layout_height="fill_parent"    android:orientation="horizontal" >    <TextView        android:id="@+id/item_tv"        android:layout_width="0dp"        android:layout_height="wrap_content"        android:layout_weight="1"        android:gravity="center_vertical"         />    <CheckBox        android:id="@+id/item_cb"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:clickable="false"        android:focusable="false"        android:focusableInTouchMode="false"         android:gravity="center_vertical"        /></LinearLayout> 

下面就写一个Adapter类,我们依然继承BaseAdapter类。这里我们使用一个HashMap<Integer,boolean>的键值来记录checkbox在对应位置的选中状况,这是本例的实现的基础。

package com.notice.listcheck;import java.util.ArrayList;import java.util.HashMap;import android.content.Context;import android.view.LayoutInflater;import android.view.View;import android.view.ViewGroup;import android.widget.BaseAdapter;import android.widget.CheckBox;import android.widget.TextView;public class MyAdapter extends BaseAdapter{// 填充数据的listprivate ArrayList<String> list;// 用来控制CheckBox的选中状况private static HashMap<Integer,Boolean> isSelected;// 上下文private Context context;// 用来导入布局private LayoutInflater inflater = null;// 构造器public MyAdapter(ArrayList<String> list, Context context) {this.context = context;this.list = list;inflater = LayoutInflater.from(context);isSelected = new HashMap<Integer, Boolean>();// 初始化数据initDate();}// 初始化isSelected的数据private void initDate(){for(int i=0; i<list.size();i++) {getIsSelected().put(i,false);}}@Overridepublic int getCount() {return list.size();}@Overridepublic Object getItem(int position) {return list.get(position);}@Overridepublic long getItemId(int position) {return position;}@Overridepublic View getView(int position, View convertView, ViewGroup parent) {ViewHolder holder = null;if (convertView == null) {// 获得ViewHolder对象holder = new ViewHolder();// 导入布局并赋值给convertviewconvertView = inflater.inflate(R.layout.listviewitem, null);holder.tv = (TextView) convertView.findViewById(R.id.item_tv);holder.cb = (CheckBox) convertView.findViewById(R.id.item_cb);// 为view设置标签convertView.setTag(holder);} else {// 取出holderholder = (ViewHolder) convertView.getTag();}// 设置list中TextView的显示holder.tv.setText(list.get(position));// 根据isSelected来设置checkbox的选中状况holder.cb.setChecked(getIsSelected().get(position));return convertView;}public static HashMap<Integer,Boolean> getIsSelected() {return isSelected;}public static void setIsSelected(HashMap<Integer,Boolean> isSelected) {MyAdapter.isSelected = isSelected;}} 

注释已经写的非常详尽了,通过

holder.cb.setChecked(getIsSelected().get(position)); 这行代码我们实现了设置CheckBox的选中状况。

那么我们只需要在点击事件中,控制isSelected的键值即可控制对应位置checkbox的选中了。

在Activity中我们除了放置一个ListView外,还放置了三个按钮,分别实现全选,取消和反选。

看下Activity类的代码:

package com.notice.listcheck;import java.util.ArrayList;import android.app.Activity;import android.os.Bundle;import android.view.View;import android.view.View.OnClickListener;import android.widget.AdapterView;import android.widget.AdapterView.OnItemClickListener;import android.widget.Button;import android.widget.ListView;import android.widget.TextView;public class Ex_checkboxActivity extends Activity {        private ListView lv;    private MyAdapter mAdapter;    private ArrayList<String> list;    private Button bt_selectall;    private Button bt_cancel;    private Button bt_deselectall;    private int checkNum; // 记录选中的条目数量    private TextView tv_show;// 用于显示选中的条目数量        /** Called when the activity is first created. */    @Override    public void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.main);        /* 实例化各个控件 */        lv = (ListView) findViewById(R.id.lv);        bt_selectall = (Button) findViewById(R.id.bt_selectall);        bt_cancel = (Button) findViewById(R.id.bt_cancelselectall);        bt_deselectall = (Button) findViewById(R.id.bt_deselectall);        tv_show = (TextView) findViewById(R.id.tv);        list = new ArrayList<String>();        // 为Adapter准备数据        initDate();        // 实例化自定义的MyAdapter        mAdapter = new MyAdapter(list, this);        // 绑定Adapter        lv.setAdapter(mAdapter);        // 全选按钮的回调接口        bt_selectall.setOnClickListener(new OnClickListener() {            @Override            public void onClick(View v) {                // 遍历list的长度,将MyAdapter中的map值全部设为true                for (int i = 0; i < list.size(); i++) {                    MyAdapter.getIsSelected().put(i, true);                }                // 数量设为list的长度                checkNum = list.size();                // 刷新listview和TextView的显示                dataChanged();            }        });        // 取消按钮的回调接口        bt_cancel.setOnClickListener(new OnClickListener() {            @Override            public void onClick(View v) {                // 遍历list的长度,将已选的按钮设为未选                for (int i = 0; i < list.size(); i++) {                    if (MyAdapter.getIsSelected().get(i)) {                        MyAdapter.getIsSelected().put(i, false);                        checkNum--;// 数量减1                    }                }                // 刷新listview和TextView的显示                dataChanged();            }        });        // 反选按钮的回调接口        bt_deselectall.setOnClickListener(new OnClickListener() {            @Override            public void onClick(View v) {                // 遍历list的长度,将已选的设为未选,未选的设为已选                for (int i = 0; i < list.size(); i++) {                    if (MyAdapter.getIsSelected().get(i)) {                        MyAdapter.getIsSelected().put(i, false);                        checkNum--;                    } else {                        MyAdapter.getIsSelected().put(i, true);                        checkNum++;                    }                }                // 刷新listview和TextView的显示                dataChanged();            }        });                // 绑定listView的监听器        lv.setOnItemClickListener(new OnItemClickListener() {            @Override            public void onItemClick(AdapterView<?> arg0, View arg1, int arg2,                    long arg3) {                // 取得ViewHolder对象,这样就省去了通过层层的findViewById去实例化我们需要的cb实例的步骤          ViewHolder holder = (ViewHolder) arg1.getTag();                // 改变CheckBox的状态                holder.cb.toggle();                // 将CheckBox的选中状况记录下来                MyAdapter.getIsSelected().put(arg2, holder.cb.isChecked());                 // 调整选定条目                if (holder.cb.isChecked() == true) {                    checkNum++;                } else {                    checkNum--;                }                // 用TextView显示                tv_show.setText("已选中"+checkNum+"项");                            }        });    }    // 初始化数据    private void initDate() {        for (int i = 0; i < 15; i++) {            list.add("data" + "   " + i);        }    }    // 刷新listview和TextView的显示    private void dataChanged() {        // 通知listView刷新        mAdapter.notifyDataSetChanged();        // TextView显示最新的选中数目        tv_show.setText("已选中" + checkNum + "项");    }    } 

代码中在item的点击事件中,直接调用

holder.cb.toggle();

先改变CheckBox的状态,然后将值存进map记录下来

MyAdapter.getIsSelected().put(arg2, holder.cb.isChecked());而其他几个Button的点击事件,都是通过遍历list的长度来设置isSelected的值,进而通知listview根据已经变化的adapter刷新,来实现Checkbox的对应选中状态。因为对listview的处理中我们仍然使用了ViewHolder来优化ListView的效率(通过findViewById层层查找是比较耗时的,这里不了解的朋友可以看我另一篇博客android应用开发全程实录-你有多熟悉listview?,全面解析listview的)。

最后,来看下运行效果:

    

好了,就写到这里。相信大家都能明白了。这里要说下一个问题,有很多朋友留言或者发邮件要博客中的一些源码。我在这里声明下,我不会去发任何我觉得已经在博客里介绍的非常清楚的实例的源码,有些实例我已经把所有代码都贴出来了,还是有人要源码。。。我希望看我博客的朋友都能真正理解这个实例,能学到更多的知识,最好能有自己的改进然后再和大家一起分享。很多朋友现在已经习惯了拿别人的源码,功能类似的就直接搬到自己项目里,这是非常不好的习惯。动动手,多写写,你会学到更多。

1 0