下拉刷新 是请求网络数据中经常会用的一种功能.
实现步骤如下:
1.新建项目   PullToRefreshDemo,定义下拉显示的头部布局pull_to_refresh_refresh.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"  
    android:layout_width="match_parent"
    android:id="@+id/pull_to_refresh_head"
    android:layout_height="60dip"
     >
     <LinearLayout 
         android:layout_width="200dip"
         android:layout_height="60dip"
         android:layout_centerInParent="true"
         android:orientation="horizontal"
         >
         <RelativeLayout 
             android:layout_width="0dip"
             android:layout_height="60dip"
             android:layout_weight="3"
             >
             <ImageView 
                 android:id="@+id/iv_arrow"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:layout_centerInParent="true"
                 android:src="@drawable/arrow"
                 />
             <ProgressBar 
                 android:id="@+id/pb"
                 android:layout_width="30dip"
                 android:layout_height="30dip"
                 android:layout_centerInParent="true"
                 android:visibility="gone"
                 />
         </RelativeLayout>
         <LinearLayout 
             android:layout_width="0dip"
             android:layout_height="60dip"
             android:layout_weight="12"
             android:orientation="vertical"
             >
             <TextView 
                 android:id="@+id/tv_description"
                 android:layout_width="fill_parent"
                 android:layout_height="0dip"
                 android:layout_weight="1"
                 android:gravity="center_horizontal|bottom"
                 android:text="下拉可以刷新"
                 />
             <TextView 
                 android:id="@+id/tv_update"
                 android:layout_width="fill_parent"
                 android:layout_height="0dip"
                 android:layout_weight="1"
                 android:gravity="center_horizontal|top"
                 android:text="上次更新于%1$s前"
                 />
         </LinearLayout>
     </LinearLayout>
    
</RelativeLayout>
2.新建一个RefreshView继承自LinearLayout.
public class RefreshView extends LinearLayout implements OnTouchListener {
    //下拉状态
    public static final int STATUS_PULL_TO_REFRESH=0;
    //释放立即刷新状态
    public static final int STATUS_RELEASE_TO_REFRESH=1;
    //正在刷新状态
    public static final int STATUS_REFRESHING=2;
    //刷新完成或未刷新状态
    public static final int STATUS_REFRESH_FINISH=3;
    
    //下拉时头部回滚的速度
    public static final int SCROLL_SPEED=-20;
    
    //一分钟的毫秒值,判断上次的更新时间
    public static final long ONE_MINUTE=60*1000;
    //一小时的毫秒值,用于判断上次的更新时间
    public static final long ONE_HOUR=60*ONE_MINUTE;
    //一天的毫秒值
    public static final long ONE_DAY=24*ONE_HOUR;
    //一月的毫秒值
    public static final long ONE_MONTH=30*ONE_DAY;
    //一年的毫秒值
    public static final long ONE_YEAR=12*ONE_MONTH;
    
    //上次更新时间的字符串常量,用来做SharedPreference的键值
    public static final String UPDATE_AT="update_at";
    
    //存储上次更新时间
    private SharedPreferences mShared;
    
    //下拉时显示的View
    private View header;
    
    //下拉刷新的ListView
    private ListView lv;  
    
    //刷新时显示的进度条
    private ProgressBar mProgressBar;
    
    //指示下拉和释放的箭头
    private ImageView arrow;
    
    //指示下拉和释放的文字描述
    private TextView tv_des;
    
    //上次更新时间的文字描述
    private TextView tv_update;
    
    //下拉头的布局参数
    private MarginLayoutParams headerLayoutParams;
    
    //上次更新时间的毫秒数
    private long lastUpdateTime;
    
    //为了防止不同界面的下拉刷新与上次更新时间互相有冲突,使用id来做区分
    private int mId=-1;
    
    //下拉头的高度
    private int hideHeaderHeight;
    
    
    //标志当前是什么状态
    private int currentStatus=STATUS_REFRESH_FINISH;
    
    //记录上次的状态是什么,避免进行重复操作
    private int lastStatus=currentStatus;
    
    
    //手指按下时 的屏幕纵坐标
    private float yDown;
    
    //在被判断为滚动之前用户手指可以移动的最大值
    private int touchSlop;
    
    //判断已加载过一次layout,这里的onLayout的初始化只需加载一次
    private boolean loadOnce;
    
    //当前是否可以下拉,只有ListView滚到头才允许下拉
    private boolean ableToPull;
        
    //下拉刷新的回调接口
    private PullToRefreshListener mListener;
    
    public RefreshView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mShared=PreferenceManager.getDefaultSharedPreferences(context);   
        header=LayoutInflater.from(context).inflate(R.layout.pull_to_refresh,null,true);   
        mProgressBar=(ProgressBar) header.findViewById(R.id.pb);
        arrow=(ImageView) header.findViewById(R.id.iv_arrow);
        tv_des=(TextView) header.findViewById(R.id.tv_description);
        tv_update=(TextView) header.findViewById(R.id.tv_update);
        touchSlop=ViewConfiguration.get(context).getScaledTouchSlop()*3;   //得到  至少移动的距离
        refreshUpdatedAtValue();  //更新文字描述
        setOrientation(VERTICAL);  //设置摆放方向
        addView(header, 0);     
    }
    
    //进行一些关键的初始化操作,比如:将下拉头向上偏移进行隐藏,给ListView注册touch事件
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        if(changed&&!loadOnce){   //只执行一次
            hideHeaderHeight=-header.getHeight();     //设置成负值   刚好隐藏在页面的最上方
            headerLayoutParams = (MarginLayoutParams) header.getLayoutParams(); 
            headerLayoutParams.topMargin=hideHeaderHeight;    //设置布局的topMargin
            header.setLayoutParams(headerLayoutParams);
            lv=(ListView) getChildAt(1);   //找到 listView  因为第一个Child是上拉头   所以第二个才是 ListView.
            lv.setOnTouchListener(this);
            loadOnce=true;
        }
    }
    
    //给下拉刷新控件注册一个监听器
    public void setOnRefreshListener(PullToRefreshListener mListener,int id){
        this.mListener=mListener;
        mId=id;
    }
    
    //更新下拉头中的信息
    private void updateHeaderView(){
        if(lastStatus!=currentStatus){
            if(currentStatus==STATUS_PULL_TO_REFRESH){
                tv_des.setText("下拉刷新");
                arrow.setVisibility(View.VISIBLE);
                mProgressBar.setVisibility(View.GONE);
                rotateArrow();
            }
            else if(currentStatus==STATUS_RELEASE_TO_REFRESH){
                tv_des.setText("释放刷新");
                arrow.setVisibility(View.VISIBLE);
                mProgressBar.setVisibility(View.GONE);
                rotateArrow();
            }
            else if(currentStatus==STATUS_REFRESHING){
                tv_des.setText("正在刷新中");
                mProgressBar.setVisibility(View.VISIBLE);
                arrow.clearAnimation(); //清除动画效果
                arrow.setVisibility(View.GONE);
            }
            refreshUpdatedAtValue();
        }
    }
    //根据当前的状态来旋转箭头
    private void rotateArrow(){
        float pivoX=arrow.getWidth()/2f;
        float pivoY=arrow.getHeight()/2f;
        
        float fromDegress=0f;
        float toDegress=0f;
        
        if(currentStatus==STATUS_PULL_TO_REFRESH){
            fromDegress=180f;
            toDegress=360f;
        }
        else{
            fromDegress=0f;
            toDegress=180f;
        }
        RotateAnimation animation=new RotateAnimation(fromDegress,toDegress,pivoX,pivoY);
        animation.setDuration(100);
        animation.setFillAfter(true);
        arrow.startAnimation(animation);
    }
    
    
    //根据当前listView的滚动状态来设定   ableToPull 的值
    //每次都需要在onTouch中的一个执行,这样可以判断出当前滚动的是listView,还是应该进行下拉
    private void setIsAbleToPull(MotionEvent event){
        View firstView=lv.getChildAt(0);
        if(firstView!=null){
            int firstVisiblePos=lv.getFirstVisiblePosition();   //获得listView顶头项的是该列数据的第几个
            if(firstVisiblePos==0&&firstView.getTop()==0){
                if(!ableToPull){
                    yDown=event.getRawY();
                }
                //如果首个元素的上边缘,距离父布局值为0,就说明 listView滚到了最顶部,此时允许下拉刷新
                ableToPull=true;
            }
            else{
                if(headerLayoutParams.topMargin!=hideHeaderHeight){
                    headerLayoutParams.topMargin=hideHeaderHeight;
                    header.setLayoutParams(headerLayoutParams);
                }
                ableToPull=false;
            }
        }
    }
    
    //当所有刷新的逻辑执行完成后,停止刷新, 并记录
    public void finishRefreshing(){
        currentStatus=STATUS_REFRESH_FINISH;
        mShared.edit().putLong(UPDATE_AT+mId, System.currentTimeMillis()).commit();
        new HideHeaderTask().execute();
    }
    
    
    //更新下拉头中上次更新时间的文字描述
    private void refreshUpdatedAtValue(){
        lastUpdateTime=mShared.getLong(UPDATE_AT+mId,-1);  //从配置文件中取出上次更新的时间的毫秒数
        long currentTime=System.currentTimeMillis();       //获得当前时间毫秒数
        long timePassed=currentTime-lastUpdateTime;        //中间相差的毫秒数  
        long timeIntoFormat;                               
        String updateAtValue;                             
        if(lastUpdateTime==-1){
            updateAtValue="暂未更新过";
        }
        else if(timePassed<0){
            updateAtValue="时间故障";
        }
        else if(timePassed<ONE_MINUTE){
            updateAtValue="刚刚更新";
        }
        else if(timePassed<ONE_HOUR){
            timeIntoFormat=timePassed/ONE_HOUR;
            String value=timeIntoFormat+"分钟";
            updateAtValue=String.format("上次更新于%1$s前",value);
        }
        else if(timePassed<ONE_DAY){
            timeIntoFormat=timePassed/ONE_HOUR;
            String value=timeIntoFormat+"小时";
            updateAtValue=String.format("上次更新于%1$s前",value);
        }
        else if(timePassed<ONE_MONTH){
            timeIntoFormat=timePassed/ONE_DAY;
            String value=timeIntoFormat+"天";
            updateAtValue=String.format("上次更新于%1$s前",value);
        }
        else if(timePassed<ONE_YEAR){
            timeIntoFormat=timePassed/ONE_MONTH;
            String value=timeIntoFormat+"月";
            updateAtValue=String.format("上次更新于%1$s前",value);
        }
        else{
            timeIntoFormat=timePassed/ONE_YEAR;
            String value=timeIntoFormat+"年";
            updateAtValue=String.format("上次更新于%1$s前",value);
        }
        tv_update.setText(updateAtValue);
    }
    
    //当listView被触摸时调用,其中处理了各种下拉刷新的具体逻辑
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        setIsAbleToPull(event);
        if(ableToPull){
            switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                yDown=event.getRawY();
                break;
            case MotionEvent.ACTION_MOVE:
                float yMove=event.getRawY();
                int distance=(int)(yMove-yDown);
                if(distance<=0&&headerLayoutParams.topMargin<=hideHeaderHeight){
                    return false;
                }
                if(distance<touchSlop){
                    return false;
                }
                if(currentStatus!=STATUS_REFRESHING){
                    if(headerLayoutParams.topMargin>0){
                        currentStatus=STATUS_RELEASE_TO_REFRESH;
                    }
                    else{
                        currentStatus=STATUS_PULL_TO_REFRESH;
                    }
                    headerLayoutParams.topMargin=(distance/2)+hideHeaderHeight;
                    header.setLayoutParams(headerLayoutParams);   //让  ListView可以弹动
                }
                break;
            case MotionEvent.ACTION_UP:
            default:
                if(currentStatus==STATUS_RELEASE_TO_REFRESH){
                    //松开手 如果是释放立即刷新  ,则去调用刷新的任务
                    new RefreshingTask().execute();
                }
                else if(currentStatus==STATUS_PULL_TO_REFRESH){
                    //松开手 如果是下拉状态,则去隐藏下拉头的任务
                    new HideHeaderTask().execute();
                }
                break;
            }
            
            if(currentStatus==STATUS_PULL_TO_REFRESH||currentStatus==STATUS_RELEASE_TO_REFRESH){
                updateHeaderView();
                //当前处于 下拉或释放 状态,要让listView失去焦点,否则被点击的那一项会一直处于选中状态
                lv.setPressed(false);
                lv.setFocusable(false);
                lv.setFocusableInTouchMode(false);
                lastStatus=currentStatus;
                return true;
            }
        }
        return false;
    }
    
    //正在刷新的任务
    class RefreshingTask extends AsyncTask<Void, Integer, Void>{
        @Override
        protected Void doInBackground(Void... params) {
            int topMargin=headerLayoutParams.topMargin;
            while(true){
                topMargin=topMargin+SCROLL_SPEED;
                if(topMargin<=0){
                    topMargin=0;
                    break;
                }
                publishProgress(topMargin);
                sleep(10);
            }
            currentStatus=STATUS_REFRESHING;
            publishProgress(0);
            if(mListener!=null){
                mListener.onRefresh();  //通知刷新
            }
            return null;
        }
        
        @Override
        protected void onProgressUpdate(Integer... topMargin) {
            updateHeaderView();
            headerLayoutParams.topMargin=topMargin[0];
            header.setLayoutParams(headerLayoutParams);
        }
    }
    
    //隐藏下拉头的任务
    class HideHeaderTask extends AsyncTask<Void, Integer, Integer>{
        @Override
        protected Integer doInBackground(Void... params) {
            int topMargin=headerLayoutParams.topMargin;
            while(true){
                topMargin=topMargin+SCROLL_SPEED;   //慢慢往回收缩
                if(topMargin<=hideHeaderHeight){    //判断是不是回到了原位
                    topMargin=hideHeaderHeight;
                    break;
                }
                publishProgress(topMargin);  //设置  收缩动作
                sleep(10);
            }
            return topMargin;
        }
        
        @Override
        protected void onProgressUpdate(Integer... values) {
            headerLayoutParams.topMargin=values[0];
            header.setLayoutParams(headerLayoutParams);
        }
        @Override
        protected void onPostExecute(Integer result) {
            headerLayoutParams.topMargin=result;
            header.setLayoutParams(headerLayoutParams);  
            currentStatus=STATUS_REFRESH_FINISH;
        }
        
        
    }
    
    /** 
     * 使当前线程睡眠指定的毫秒数。 
     *  
     * @param time 
     *            指定当前线程睡眠多久,以毫秒为单位 
     */  
    private void sleep(int time) {  
        try {  
            Thread.sleep(time);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    }  
    
    
    //下拉刷新的监听器
    public interface PullToRefreshListener{
        void onRefresh();
    }
}
首先,在构造函数中动态添加了pull_to_refresh这个布局作为下拉头,然后将onLayout方法中将下拉头向上偏移出了屏幕,再给ListView注册了Touch事件.
如果在ListView上进行滑动,onTouch就会执行,onTouch首先会用setIsAbleToPull方法判断ListView是否滚动到了最顶部,只有滚动到最顶部才会执行后面的代码,否则就是ListView的正常滚动,不作处理.当ListView滚动到最顶部,如果手指还在向下拖动,就会改变下拉头的偏移值,让下拉头显示出来,如果下拉的距离足够大,在松手后就会执行刷新操作,如果距离不够大,则会隐藏下拉头.

具体刷新方法操作在RefreshingTask中进行,其中在doInBackground方法中回调了PullToRefreshListener接口的onRefresh()方法.
具体使用方法如下:
3.在activity_main.xml中
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity" >
    <com.cy.pulltorefreshDemo.RefreshView 
        android:id="@+id/refresh_view"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        >
        <ListView 
            android:id="@+id/lv"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:cacheColorHint="@android:color/transparent"
            ></ListView>
    </com.cy.pulltorefreshDemo.RefreshView>
</RelativeLayout>
只要将需要刷新的ListView包含在  RefreshView中.

4.MainActivity.java
public class MainActivity extends Activity {
    RefreshView refreshView;
    ListView lv;
    ArrayAdapter<String> adapter;
    List<String> items=new ArrayList<String>();
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        items.add("A");
        items.add("B");
        refreshView=(RefreshView) findViewById(R.id.refresh_view);
        lv=(ListView) findViewById(R.id.lv);
        adapter=new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1,items);
        lv.setAdapter(adapter);
        refreshView.setOnRefreshListener(new PullToRefreshListener() {
            @Override
            public void onRefresh() {
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                items.add("222");//自动添加 到 ListView中
                refreshView.finishRefreshing();
            }
        }, 0);
    }
}
就这样,一个完整的下拉刷新.



android 自定义控件之下拉刷新源码详解的更多相关文章

  1. Android进阶笔记:Messenger源码详解

    Messenger可以理解为一个是用于发送消息的一个类用法也很多,这里主要分析一下再跨进程的情况下Messenger的实现流程与源码分析.相信结合前面两篇关于aidl解析文章能够更好的对aidl有一个 ...

  2. Activiti架构分析及源码详解

    目录 Activiti架构分析及源码详解 引言 一.Activiti设计解析-架构&领域模型 1.1 架构 1.2 领域模型 二.Activiti设计解析-PVM执行树 2.1 核心理念 2. ...

  3. RocketMQ源码详解 | Producer篇 · 其二:消息组成、发送链路

    概述 在上一节 RocketMQ源码详解 | Producer篇 · 其一:Start,然后 Send 一条消息 中,我们了解了 Producer 在发送消息的流程.这次我们再来具体下看消息的构成与其 ...

  4. 源码详解系列(七) ------ 全面讲解logback的使用和源码

    什么是logback logback 用于日志记录,可以将日志输出到控制台.文件.数据库和邮件等,相比其它所有的日志系统,logback 更快并且更小,包含了许多独特并且有用的特性. logback ...

  5. RocketMQ源码详解 | Consumer篇 · 其一:消息的 Pull 和 Push

    概述 当消息被存储后,消费者就会将其消费. 这句话简要的概述了一条消息的最总去向,也引出了本文将讨论的问题: 消息什么时候才对被消费者可见? 是在 page cache 中吗?还是在落盘后?还是像 K ...

  6. RocketMQ源码详解 | Broker篇 · 其四:事务消息、批量消息、延迟消息

    概述 在上文中,我们讨论了消费者对于消息拉取的实现,对于 RocketMQ 这个黑盒的心脏部分,我们顺着消息的发送流程已经将其剖析了大半部分.本章我们不妨乘胜追击,接着讨论各种不同的消息的原理与实现. ...

  7. RocketMQ源码详解 | Broker篇 · 其五:高可用之主从架构

    概述 对于一个消息中间件来讲,高可用功能是极其重要的,RocketMQ 当然也具有其对应的高可用方案. 在 RocketMQ 中,有主从架构和 Dledger 两种高可用方案: 第一种通过主 Brok ...

  8. Spark Streaming揭秘 Day25 StreamingContext和JobScheduler启动源码详解

    Spark Streaming揭秘 Day25 StreamingContext和JobScheduler启动源码详解 今天主要理一下StreamingContext的启动过程,其中最为重要的就是Jo ...

  9. spring事务详解(三)源码详解

    系列目录 spring事务详解(一)初探事务 spring事务详解(二)简单样例 spring事务详解(三)源码详解 spring事务详解(四)测试验证 spring事务详解(五)总结提高 一.引子 ...

随机推荐

  1. 安装juicer

    由于我第一次安装 JUICER时遇到了很多问题,现在把这些问题都记录下来,给同样第一次安装使用的同学一点借鉴. 前面已经安装了Torch3和Tracter,这都是为安装Juicer做的准备,现在安装J ...

  2. void和void*指针的一些理解

    void 和 void* 指针分别表示无类型和无类型指针. void 的作用是限制: 1,函数无返回值. 2,函数无参数. 当函数的返还值无参数的时候一定要加上 void ,因为在缺省的状态下函数的返 ...

  3. 【软件笔记】 ◆笔记·I◆ 各类冷门函数细解

    [软件笔记·I] 各类冷门函数细解 ■题外话■ 总觉得作为一个志向远大的 coder (٩(◕‿◕。)۶),我觉得单单只会做题是不够的所以我开始尝试自己编写软件!初入道的我并不知道C++其实并不太适合 ...

  4. python中enumerate函数使用

    enumerate()说明 enumerate()是python的内置函数 enumerate在字典上是枚举.列举的意思 对于一个可迭代的(iterable)/可遍历的对象(如列表.字符串),enum ...

  5. PHP 输出控制

    一.前言 说到PHP输出控制, 在很多框架里面,比如说TP,Yii和Laraval的模版引擎里面都有输出控制函数的阴影,输出控制也叫输出缓冲,说到它的作用有以下几点. 二.内容 1. 输出模版 $va ...

  6. 学习python第十六天,正则表达式

    正则表达式是一个特殊的字符序列,它能帮助你方便的检查一个字符串是否与某种模式匹配.采取动态模糊的匹配,最大的应用是爬虫. re 模块使 Python 语言拥有全部的正则表达式功能. compile 函 ...

  7. 学习python第十五天,面向对象

    Python从设计之初就已经是一门面向对象的语言,正因为如此,在Python中创建一个类和对象是很容易的. 面向对象技术简介 类(Class): 用来描述具有相同的属性和方法的对象的集合.它定义了该集 ...

  8. iOS-UICollectionViewController 介绍

    废话不多说,列几个列子 (几种情况下的做法): 情景一: 介绍:1. 在UIViewController 上加 UICollectionView (用代码 创建 UICollectionView). ...

  9. C语言字符篇(二)字符串处理函数

    字符串处理函数 1. 拷贝 strcpy 2. 追加 strcat   #include <string.h>   char *strcpy(char *dest, const char ...

  10. Codeforces146D 概率DP

    Bag of mice The dragon and the princess are arguing about what to do on the New Year's Eve. The drag ...