有一俩月没写博客了,这惰性没救了 今天补救一下。
不少人在找下拉刷新、上拉加载的控件,其实网上的各种解决方案挺多的,
Github上面更是各种脑洞大开的绚丽效果,不过大多只有下拉刷新比如官方的SwipeRefreshLayout,
或者只支持ListView,再或者是集成很不方便,甚至有些会与自己重写的触摸事件冲突之类的,
今天为大家分享个我自己写的下拉上拉控件,支持ListView ScrollView TextView ImageView WebView,
虽然有点小限制但是我认为对于一般APP来讲影响不大,集成也不是很费事,
不会与自定义的OnTouch事件相冲突。

特别方便为已有项目添加上拉下拉功能,而且代码改动不大

先放上demo 下载地址

下面直接进入正题

  • 先上图 样式就是最普通的样式
  • 集成
拷贝2个xml布局文件到layout文件夹
    1. refresh_top_item.xml  头布局文件
    2. refresh_bottom_item.xml 尾布局文件
拷贝2个资源图片到 drawable-hdpi文件夹
    1. goicon_up.png
    2. go icon.png
拷贝3个源码文件到相应包
    1. PullableLayout.java
    2. HeaderView.java
    3. FooterView.java
  • 使用方法
    xml中PullableLayout直接当做 LinearLayout使用
    需要注意的是 如果子布局是 TextView ImageView LinearLayout之类的或者其他默认不可点击的控件需要设置 clickable=”true” ,可以不用实现点击事件。因为如果事件不向子控件分发的话 拦截事件是不会触发的.
<com.example.pullablelayout.widgets.PullableLayout 
        android:layout_width="match_parent" android:layout_height="match_parent"
        android:id="@+id/pullableLayout">
        <ScrollView android:layout_width="match_parent" android:layout_height="match_parent">
            <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content"
                android:orientation="vertical">

            </LinearLayout>
        </ScrollView>
    </com.example.pullablelayout.widgets.PullableLayout>

activity中设置头尾布局 添加监听事件

pullableLayout = (PullableLayout) findViewById(R.id.pullableLayout);
        //设置下拉头
        pullableLayout.setHeader(new HeaderView(MainActivity.this));
        //设置上拉尾
        pullableLayout.setFooter(new FooterView(MainActivity.this));
        //设置上拉下拉监听 MainActivity实现了 OnRefreshListener接口
        pullableLayout.setRefreshListener(MainActivity.this);

完成监听事件
OnRefreshListener要实现两个方法 下拉 onPullDown() 上拉onPullUp()

/**
     * 下拉上拉回调的接口
     */
    public interface OnRefreshListener{
        /**
         * 下拉回调
         */
        public void onPullDown();
        /**
         * 上拉回调
         */
        public void onPullUp();
    }

在对应的方法里完成异步回调 我这里仅仅是个例子

@Override
    public void onPullDown() {
        //举个例子 不要在意这些细节
        mHandler.postDelayed(new Runnable() {

            @Override
            public void run() {
                pullableLayout.stopRefresh(null);
            }
        }, 2000);
    }
    @Override
    public void onPullUp() {
        mHandler.postDelayed(new Runnable() {

            @Override
            public void run() {
                pullableLayout.stopRefresh(null);
            }
        }, 2000);
    }

好了 大功告成 集成完毕 。

  1. 基本原理
    如图下拉上拉总体分三部分 就是控制header、footer的显示
    header的topMargin设置为 headerview高度*-1
    footer的bottomMargin设置为 footerview高度*-1
    上拉下拉过程中利用scrollTo方法滚动到相应位置 显示和隐藏对应布局
    难点在于事件拦截
  2. android listview上拉下滑加载 android上拉刷新下拉加载_下拉刷新

  3. 实现
    PullableLayout < Extends LinearLayout>
    集成自LinearLayout 在Xml中完全可以当作 线性布局使用
    使用到的自定义的变量如下 每个都有注释应该很好理解
public class PullableLayout extends LinearLayout{

    public static final int STATUS_NORMAL = 0;  //正常状态
    public static final int STATUS_DOWN = 1;    //下拉状态
    public static final int STATUS_UP = 2;      //上拉状态

    public ExtraView header;                    //下拉控件
    public ExtraView footer;                    //上拉控件

    public int intercepted = 0;                 //记录单次触摸事件的状态
    public int status = STATUS_NORMAL;          //记录view当前的状态  下拉刷新中 上拉加载中
    private float downX=0,downY=0;              //手指按下时的位置
    private int startY;                         //开始上拉 下拉的Y轴位置
    public static final String TAG = "PullableLayout";
    private OnRefreshListener refreshListener;  //事件监听
  • 头尾布局类需要实现的接口
    具体实现的例子可以看源码 也可以自己实现自定义各种效果
public interface ExtraView{
        /**
         * 事先定义好的几种状态位
         */
        public final static int PULL_To_REFRESH = 0;
        public final static int RELEASE_To_REFRESH = 1;
        public final static int REFRESHING = 2;
        public final static int DONE = 3;
        /**
         * 获取布局的高度 如果不想隐藏返回0即可
         * @return 布局的高度 
         */
        public int getLayoutHeight();
        /**
         * 获取布局view
         * @return 
         */
        public View getView();
        /**
         * 滑动事件的回调
         * @param length 计算后的下拉的距离 为了有迟滞感 其实是返回的是实际下拉距离的一半
         */
        public void onPull(int length);
        /**
         * 
         * @param obj 传递给 header view的参数 即 stopRefresh(Object obj)的参数
         */
        public void onFinish(Object obj);
        /**
         * 松手后开始刷新、加载的回调
         */
        public void onRefresh();
        /**
         * 是否可以刷新
         * @return 如果返回false 则不回调 OnRefreshListener
         */
        public boolean canRefresh();
    }
  • 设置头尾布局
/**
     * 设置下拉的布局  通过布局的 margin达到隐藏view的目的
     * @param view
     */
    public void setHeader(ExtraView view){
        if(view==null){
            return;
        }
        header = view;
        LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT,header.getLayoutHeight());
        lp.topMargin = -header.getLayoutHeight();
        addView(header.getView(), 0, lp);
    }
    /**
     * 设置上拉布局 通过布局的 margin达到隐藏view的目的
     * @param view
     */
    public void setFooter(ExtraView view){
        if(view==null){
            return;
        }
        footer = view;
        LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT,footer.getLayoutHeight());
        lp.bottomMargin = -footer.getLayoutHeight();
        addView(footer.getView(), getChildCount(), lp);     
    }
  • 事件拦截
//滑动事件拦截 具体处理在 onTouchEvent中
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        //如果没有设置上拉下拉直接返回
        if(header==null && footer==null){
            return super.onInterceptTouchEvent(ev);
        }

        float x = ev.getX();
        float y = ev.getY();
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            downX = x;
            downY = y;
        }else if(ev.getAction() == MotionEvent.ACTION_MOVE){
            //水平滑动 或者 事件已拦截 则不做额外处理
            if( Math.abs(x - downX) > Math.abs(y-downY)*2){ 
                return super.onInterceptTouchEvent(ev);
            }
            if(intercepted>0){
                return true;
            }
            if(y-downY>0){  //下拉
                if(canPullDown()){
                    intercepted = STATUS_DOWN;                  
                    startY = (int)y;
                    return true;
                }

            }else{      //上拉
                if(canPullUp()){
                    intercepted = STATUS_UP;
                    startY = (int)y;
                    return true;
                }

            }
        }else if(ev.getAction()==MotionEvent.ACTION_UP){
            intercepted = 0;
        }
        return super.onInterceptTouchEvent(ev);
    }
  • 可上拉下拉时机的判断 这个是重点 具体解释见注释
/**
     * 判断是否响应拦截下拉事件
     * 不同的子控件判断方式不同需要分别讨论
     * 需要特别判断的 一般是 AbsListView子类、ScrollView、WebView
     * 这里没具体判断当子控件是webview时候的情况 如有需要请自行添加
     * @return
     */
    private boolean canPullDown(){
        if(header==null) {
            return false;
        }
        View childView  = getChildAt(1);
        if (childView instanceof AbsListView) { //当子控件为 AbsListView的子类时

            if(((AbsListView) childView).getChildCount()<1){
                return true;
            }
            //判断显示的第一项的左上角相对于视图左上角的垂直偏移
            int top = ((AbsListView) childView).getChildAt(0).getTop();
            //布局的上padding
            int pad = ((AbsListView) childView).getListPaddingTop();

            if ((Math.abs(top - pad)) < 3
                    && ((AbsListView) childView).getFirstVisiblePosition() == 0) {
                return true;
            } else {
                return false;
            }

        }else if(childView instanceof ScrollView){ //当子控件为 ScrollView的子类时
             if (((ScrollView) childView).getScrollY() == 0) {
                 return true;
             } else {
                 return false;
             }
        }else{
            return true;
        }
    }
    /**
     * 判断是否响应拦截上拉事件
     * 同{@link #canPullDown()}
     * @return
     */
    private boolean canPullUp(){
        if(footer==null) {
            return false;
        }

        int p = 0; //要判断类型的子控件在布局中的位置 没有上拉为0 否则为1
        if(header!=null){
            p = 1;
        }
        View childView  = getChildAt(p);
        if (childView instanceof AbsListView) {
            AbsListView absListView = (AbsListView) childView;
            if(absListView.getChildCount()<1){
                return true;
            }
            //列表最后一项的右下角的Y坐标                    
            int bottom = absListView.getChildAt(absListView.getChildCount() -1).getBottom();
            //列表项显示的底部Y坐标
            int pad = absListView.getBottom() - absListView.getListPaddingBottom();
            //bottom位置需要在pad之上 (屏幕上的Y坐标 从上到下依次增大) 
            if(pad-bottom > -1 && absListView.getLastVisiblePosition() == absListView.getAdapter().getCount()-1){       
                return true;
            }else{
                return false;
            }
        }else if(childView instanceof ScrollView){
            ScrollView scrollView = (ScrollView) childView;
            //这里比较的是 视图滚动到底部时候的右上角的Y坐标
            if (scrollView.getScrollY() >= (scrollView.getChildAt(0).getHeight() - scrollView.getMeasuredHeight())){    
                return true;
            } else {
                return false;
            }
        }else{
            return true;
        }
    }
  • onTouchEvent的实现
@Override
    public boolean onTouchEvent(MotionEvent event) {
        if(event.getAction()==MotionEvent.ACTION_UP){       //手指弹起时
            //下拉
            if(intercepted == STATUS_DOWN){
                //判断是否可以响应下拉
                if(header.canRefresh() && status == STATUS_NORMAL){
                    //当前状态更改为下拉刷新中
                    status = STATUS_DOWN;
                    header.onRefresh();
                    //当有多余的滑动距离时弹回 此时不回调 onPull
                    setHeader(header.getLayoutHeight(),true);

                    if(refreshListener!=null ){
                        refreshListener.onPullDown();
                    }

                }else{
                    //不响应下拉状态时直接复位 
                    setHeader(0,true);
                }

            }else if(intercepted == STATUS_UP){  //上拉
                //同下拉
                if(footer.canRefresh() && status == STATUS_NORMAL){
                    status = STATUS_UP;
                    footer.onRefresh();
                    if(refreshListener!=null ){
                        refreshListener.onPullUp();
                    }
                    setFooter(footer.getLayoutHeight(),true);
                }else{
                    setFooter(0,true);
                }

            }
            //一次滑动操作结束 复位intercepted
            intercepted = STATUS_NORMAL;
        }else if(event.getAction()==MotionEvent.ACTION_MOVE){ //手指在屏幕上滑动时
            int y = (int) event.getY();
            //根据滑动距离设置不同的显示状态 同时排除越界
            if(intercepted == STATUS_DOWN){
                int top = (y-startY)/2;
                if(top>0){
                    setHeader(top,false);
                }
            }else if(intercepted == STATUS_UP){
                int bottom = (startY - y)/2;
                if(bottom>0){
                    setFooter(bottom,false);
                }
            }
        }
        return super.onTouchEvent(event);
    }

    /**
     * 设置下拉时的响应
     * @param marginTop 下拉的距离
     * @param intercept 是否拦截header的响应  true则拦截不调用header.onPull()
     * 当上/下拉松手复位footer刷新的时候 intercept = true;
     */
    private void setHeader(int marginTop,boolean intercept){
        if(header!=null && !intercept){
            header.onPull(marginTop);
        }
        //在这里可以设置不同滑动的响应 
        scrollTo(0, -marginTop);
    }
    /**
     * 设置上拉时的响应
     * @param marginBottom 上拉距离
     * @param intercept 是否拦截footer的响应 true拦截 不调用 footer.onPull() 
     * 当上/下拉松手复位footer加载的时候 intercept = true;
     */
    private void setFooter(int marginBottom,boolean intercept){
        if(footer!=null && !intercept){
            footer.onPull(marginBottom);
        }
        scrollTo(0, marginBottom);
    }