下拉刷新 是请求网络数据中经常会用的一种功能.
实现步骤如下:
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. 如何提高mysql的安全性?

    1.如果 MySQL 客户端和服务器端的连接需要跨越并通过不可信任的网络,那么需要使用 ssh 隧道来加密该连接的通信.2.使用 set password 语句来修改用户的密码,先“mysql -u ...

  2. 使用jdk进行数据迁移(sqlite迁移mysql)

    直接粘贴代码 注意:rewriteBatchedStatements=true(加快连接速度) package com.wbg; import org.omg.Messaging.SYNC_WITH_ ...

  3. [NVIDIA编程教程]OpenACC: Directives for GPUs

    NVIDIA已经在过去五年里大力发展CUDA技术,我们估计CUDA开发人员超过15万,很多重要的科学应用正在CUDA的帮助下完成.但是我们仍然有一个很长的路要走,以帮助每个人从GPU计算中享受到好处. ...

  4. cudpp库的编译和使用

    项目主页 http://cudpp.github.io/ 根据这个网址的提示进行 https://github.com/cudpp/cudpp/wiki/BuildingCUDPPwithCMake ...

  5. YSlow的安装与说明文档

    yslow官网 http://yslow.org/ 很明显起这个名字是说why slow 为什么这么慢,理所当然是为当前网页进行检测 借百度的 什么是YSlow? YSlow是yahoo发布的一款基于 ...

  6. javascript原生API总结

    一.查找: getElementById() 方法返回带有指定 ID 的元素(唯一): getElementsByTagName() 返回包含带有指定标签名称的所有元素的节点列表(集合/节点数组). ...

  7. DTcms网站伪静态逻辑

    我们之前写伪静态就是web.config里面配置好.-->配置伪静态(URL重写),DTcms网站写的伪静态跟之前的不一样,他是静态页面和代码现实了分离.http://demo.dtcms.ne ...

  8. C#中类的声明

    一.C#中类的声明 在C#中必须先声明类,然后才能在程序中使用. 类的声明格式如下: [类的属性] [访问修饰符] class 类名称 [: 父类名]{    [成员修饰符] 类的成员变量或者成员函数 ...

  9. Django 单元测试

    mock 测试 mock 是辅助单元测试的模块,用于测试不方便调用的别人的接口.举个简单的例子,比如说,我们测试django 写的微信登录接口,正常流程下,我们需要前端拉起授权窗口,获取jscode或 ...

  10. ABAP 调用远程rfc

    ABAP 调用rfc DESTINATION附加项后面接的是远程目标名称,该目标在事务SM59中设定,其中包含连接和登录远程系统所需的全部参数信息.如果调用的就是本机的RFC目标,则DESTINATI ...