首页 > 代码库 > android 自定义控件之下拉刷新源码详解

android 自定义控件之下拉刷新源码详解

下拉刷新 是请求网络数据中经常会用的一种功能.
实现步骤如下:
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=http://www.mamicode.com/"暂未更新过";
        }
        else if(timePassed<0){
            updateAtValue=http://www.mamicode.com/"时间故障";
        }
        else if(timePassed<ONE_MINUTE){
            updateAtValue=http://www.mamicode.com/"刚刚更新";
        }
        else if(timePassed<ONE_HOUR){
            timeIntoFormat=timePassed/ONE_HOUR;
            String value=http://www.mamicode.com/timeIntoFormat+"分钟";
            updateAtValue=http://www.mamicode.com/String.format("上次更新于%1$s前",value);
        }
        else if(timePassed<ONE_DAY){
            timeIntoFormat=timePassed/ONE_HOUR;
            String value=http://www.mamicode.com/timeIntoFormat+"小时";
            updateAtValue=http://www.mamicode.com/String.format("上次更新于%1$s前",value);
        }
        else if(timePassed<ONE_MONTH){
            timeIntoFormat=timePassed/ONE_DAY;
            String value=http://www.mamicode.com/timeIntoFormat+"天";
            updateAtValue=http://www.mamicode.com/String.format("上次更新于%1$s前",value);
        }
        else if(timePassed<ONE_YEAR){
            timeIntoFormat=timePassed/ONE_MONTH;
            String value=http://www.mamicode.com/timeIntoFormat+"月";
            updateAtValue=http://www.mamicode.com/String.format("上次更新于%1$s前",value);
        }
        else{
            timeIntoFormat=timePassed/ONE_YEAR;
            String value=http://www.mamicode.com/timeIntoFormat+"年";
            updateAtValue=http://www.mamicode.com/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);
    }
 
}
就这样,一个完整的下拉刷新.

 

 
 



来自为知笔记(Wiz)



android 自定义控件之下拉刷新源码详解