美文网首页Android DemoAndroid开发Android技术知识
从0开始撸一个自己的下拉刷新上拉加载的RecyclerView

从0开始撸一个自己的下拉刷新上拉加载的RecyclerView

作者: 谢长意 | 来源:发表于2018-05-04 14:55 被阅读312次

    授人以鱼不如授人以渔,虽然网上有很多这样的现成的组件,但是我们真的了解怎么去实现吗?这篇文章主要讲怎么一步一步的实现这个功能。
    本文章详细介绍怎么从0开始实现一个支持上拉刷新下拉加载的recyclerview,这个0到什么地步呢?那就从打开as新建一个项目开始。

    先看一波最后实现的效果图


    效果图

    源码地址

    前言:在打开as新建项目前,先来构思一下整个思路


    如果要实现上拉刷新下拉加载,那么就要在头部和底部添加headView和footView,如果是当作recyclerView的item添加到第一行和最后一行,那么针对一行显示两列item的就不适用了。这里我们就抛弃这个想法,换个方法实现。
    利用三个独立view(这样也支持更换不同的headView和footView)来实现,headView ,recyclerView,footView,然后将headView和footView布局到屏幕外边,然后手机拖动的时候,再根据距离来慢慢移动到布局内部。如图:


    思路实现

    现在我们考虑用那种方式来实现这个位置的移动。
    方案1:利用 父布局的 scrollTo()/ scrollBy() 来实现。
    方案2:利用子view的 setTranslationY() 来实现。
    方案3:利用子view的 offsetTopAndBottom() 来实现。
    方案4:利用子view的 layout() 来实现。
    我用经验告诉你们,只有 方案4 是最好的实现。
    方案1和方案2会有bug:当处于正在刷新或者加载状态的时候,这个时候你手指向下滑动,recyclerView却是向下滚动的。
    方案3:当处于正在刷新或者加载的时候,recyclerView会有一部分处于屏幕外边,这个时候会挡住一部分item。

    选了移动方案之后,那么我们根据什么来设置这个移动值呢?

    方案1: 新的嵌套滚动机制
    方案2:拦截触摸事件,自己计算偏移值
    方案3:拦截触摸事件,并把触摸事件托管给GestureDetector
    我在用经验告诉你们:这里三种方式都可以,难度也都差不多。
    本文我选择了方案3

    总结:本次项目实现,拦截触摸事件,并托管给GestureDetector,然后在scroll()回调方法中中重布局headView,recyclerView和footView。
    下拉:只需要更改headView的top和bottom以及recyclerView的top
    上拉:只需要更改footView的top和bottom以及recyclerView的bottom


    正式开始

    1.打开As新建一个项目


    崭新的项目

    2.新建一个view类(SwipeRecycler.java)并继承ViewGroup


    public class SwipeRecycler extends ViewGroup {
        public SwipeRecycler(Context context) {
            super(context);
        }
    
        public SwipeRecycler(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        public SwipeRecycler(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
    
        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
    
        }
    }
    

    重写拦截方法

       /**
         * 最先被拦截,在传给子view之前会调用
         * @param ev
         * @return super传递给子view 
         *         true/false 不再向下传递,并会调用{@link #onTouchEvent(MotionEvent event)}
         */
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            return super.onInterceptTouchEvent(ev);
        }
        //如果触摸事件没有被子view消耗完,会调用此方法
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            return super.onTouchEvent(event);
        }
    

    再做一些准备工作,整体代码如下

    public class SwipeRecycler extends ViewGroup {
        public SwipeRecycler(Context context) {
            super(context);
            init();
        }
        public SwipeRecycler(Context context, AttributeSet attrs) {
            super(context, attrs);
            init();
        }
        public SwipeRecycler(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            init();
        }
        //做一些初始化操作 
        private void init() {
    
        }
        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
    
        }
        /**
         * 最先被拦截,在传给子view之前会调用
         * @param ev
         * @return super传递给子view
         *         true/false 不再向下传递,并会调用{@link #onTouchEvent(MotionEvent event)}
         */
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            return super.onInterceptTouchEvent(ev);
        }
        //如果触摸事件没有被子view消耗完,会调用此方法
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            return super.onTouchEvent(event);
        }
    }
    

    3.添加recyclerView库,默认是没有的

     implementation 'com.android.support:recyclerview-v7:27.1.1'
    

    4.创建headView,footView和recyclerView


        //创建headView,注意LayoutParams参数
        private Button getHeadView(){
            ViewGroup.LayoutParams lp=new LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.WRAP_CONTENT);
            Button head=new Button(getContext());
            head.setText("下拉刷新");
            head.setLayoutParams(lp);
            return head;
        }
        //创建recyclerView,注意LayoutParams参数
        private RecyclerView getRecyclerView(){
            ViewGroup.LayoutParams lp=new LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.MATCH_PARENT);
            RecyclerView rcv=new RecyclerView(getContext());
            rcv.setLayoutParams(lp);
            return rcv;
        }
        //创建footView,注意LayoutParams参数
        private Button getFootView(){
            ViewGroup.LayoutParams lp=new LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.WRAP_CONTENT);
            Button foot=new Button(getContext());
            foot.setText("上拉加载");
            foot.setLayoutParams(lp);
            return foot;
        }
    

    init()方法中将view添加到viewGroup中

      //做一些初始化操作
        private void init() {
            //添加view
            headView = getHeadView();
            footView = getFootView();
            recyclerView = getRecyclerView();
            addView(headView);
            addView(recyclerView);
            addView(footView);
        }
    

    5.重写测量方法,不然view不会显示

       //重写测量方法,不然view不会显示
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            final int childCount=getChildCount();
            for (int i = 0; i < childCount; i++) {
                final View childView=getChildAt(i);
                measureChild(childView,widthMeasureSpec,heightMeasureSpec);
            }
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
    
    

    6.设置手势监听


    实现GestureDetector.OnGestureListener

    class SwipeRecycler extends ViewGroup implements GestureDetector.OnGestureListener
    

    重写方法

        @Override
        public boolean onDown(MotionEvent e) {
            return false;
        }
        @Override
        public void onShowPress(MotionEvent e) {
    
        }
        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            return false;
        }
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            return false;
        }
        @Override
        public void onLongPress(MotionEvent e) {
    
        }
        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            return false;
        }
    

    虽然很多,但是我们需要的只有这个

    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)

    其他的什么意思,自行百度,这里就不过多的介绍了,不能失去了重点。
    然后在init()设置监听

     //设置手势监听器监听
    gestureDetector=new GestureDetector(getContext(),this);
    

    onTouchEvent()中把触摸事件托管给手势监听。

      //如果触摸事件没有被子view消耗完,会调用此方法
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            return gestureDetector.onTouchEvent(event);
        }
    

    注意:不能在拦截的时候托管,因为这样的话,你的子view就接收不到任何触摸事件,那样的话,你的recyclerview就不能滑动了。


    7.布局子view


    headView的布局四个位置应该是:
    left:0(宽度和父view的宽度一致,左边为0)
    top:-headView.getHeight()(上方应该是headView的高度负值)
    right:getWidth()(宽度和父view的宽度一致,右边为headView或者父view的宽度)
    bottom:0(下边应该紧挨着父view的上方)

    footView的布局四个位置应该是:
    left:0(宽度和父view的宽度一致,左边为0)
    top:getHeight()(上方应该紧挨着父view的bottom)
    right:getWidth()(宽度和父view的宽度一致,右边为footView或者父view的宽度)
    bottom:getHeight()+footView.getHeight()(下边应该是父view的高度加上footview的高度)

    recyclerView的布局四个位置应该是:
    left:0(宽度和父view的宽度一致,左边为0)
    top:0(上方应该紧挨着父view的top)
    right:getWidth()(宽度和父view的宽度一致,右边为recyclerView或者父view的宽度)
    bottom:getHeight()(上方应该紧挨着父view的bottom)

    现在在layout()中开始布局

    由于布局要考虑到padding值,并且布局的时候只能拿到测量高度,实际高度拿不到。所以完整代码如下

     @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
            if (changed) {
                for (int i = 0; i < getChildCount(); i++) {
                    View childView = getChildAt(i);
                    if (childView == headView) {//开始布局headView
                        childView.layout(getPaddingLeft(), -headView.getMeasuredHeight() + getPaddingTop(), r - l - getPaddingRight(), getPaddingTop());
                    } else if (childView == footView) {//开始布局footV
                        childView.layout(getPaddingLeft(), b - t - getPaddingBottom(), r - l - getPaddingRight(),
                                b - t + footView.getMeasuredHeight() - getPaddingBottom());
                    } else if (childView == recyclerView) {//开始布局recyclerView
                        childView.layout(getPaddingLeft(), getPaddingTop(), r - l - getPaddingRight(), b - t - getPaddingBottom());
                    } else {//其他的view,目前是没有的。
                        childView.layout(0, 0, 0, 0);
                    }
                }
            } else {
                for (int i = 0; i < getChildCount(); i++) {
                    View childView = getChildAt(i);
                    childView.layout(childView.getLeft(), childView.getTop(), childView.getRight(), childView.getBottom());
                }
            }
        }
    

    目前为止,准备工作已经完毕,接下来就是处理触摸事件了。


    8.处理拦截事件


    这里发生以下情况下才产生拦截:
    1.recyclerView划到了头部,并且继续下滑
    2.recyclerView划到了底部,并且继续上拉
    3.已经发生了下拉,这个时候滑动分为继续下拉和和上划复位
    4.已经发生了上拉,这个时候滑动分为继续上拉和下拉复位

    这里设置一个变量来保存几种状态

        //0正常状态 1触发了下拉刷新 2触发了上拉加载 3正在进行下拉刷新 4正在进行上拉加载
        private int pullStatus=0;
    

    通过recyclerView.canScrollVertically()来判断是否滑动到了第一个item或者最后一个item

    //-1表示检查是否可以下拉,返回true:还没划到第一个item
    recyclerView.canScrollVertically(-1);
    //1表示检查是否可以上拉,返回true:还没划到最后一个item
     recyclerView.canScrollVertically(1);
    

    开始处理拦截事件

        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            //只有处于正常状态才有可能拦截
            //一次完整的触摸,如果产生了拦截,就不会再走此方法
            if (pullStatus == 0) {
                switch (ev.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        oldTouchY = ev.getY();
                        break;
                    case MotionEvent.ACTION_MOVE:
                        //获取偏移值,offy>0 由上向下滑
                        float offy = ev.getY() - oldTouchY;
                        oldTouchY = ev.getY();
                        //向下划,而且recyclerView已经滑动到了第一个item
                        if (offy > 0 && !recyclerView.canScrollVertically(-1)) {
                            //设置状态为触发了下拉
                            pullStatus = 1;
                            //返回true,拦截这次事件
                            return true;
                        //向上划,而且recyclerView已经滑动到了最好一个item
                        } else if (offy < 0 && !recyclerView.canScrollVertically(1)) {
                            //设置状态为触发了上拉
                            pullStatus = 2;
                            //返回true,拦截这次事件
                            return true;
                        }
                        break;
                }
            }
            return super.onInterceptTouchEvent(ev);
        }
    

    9.处理滑动


    这时候就要用到手势监听器了,而且他已经处理好了滑动,只需要这个方法就可以了。

      //用户滚动的时候 distanceY<0下拉距离否则上拉距离 
       int mixThreshold = 10;
    
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            //为了防止一次跳动太大,当只有距离小于50的时候才重新布局
            if (Math.abs(distanceY) < 50) {
                if (pullStatus == 1 || pullStatus == 3) {
                    if (distanceY < 0) {
                        //这里需要distanceY的相反值,distanceY / 2表示,手指移动100,实际布局只向下移动50
                        offsetTopOrBottom(-(distanceY / 2));
                    } else {
                        int t = recyclerView.getTop();
                        //由于复位的时候,有时候并不能检测到t是否等于0,当t<mixThreshold的时候,就认为t已经是0了。
                        if (t < mixThreshold) {//下拉刷新,这个时候又不想刷了,又上拉到了原来的地方
                            offsetTopOrBottom(-t);
                            pullStatus = 0;//设置状态为正常
                        } else {
                            offsetTopOrBottom(-(distanceY / 2));
                        }
                    }
                } else if (pullStatus == 2 || pullStatus == 4) {
                    if (distanceY > 0) {
                        offsetTopOrBottom(-(distanceY / 2));
                    } else {
                        int t = recyclerView.getBottom();
                        //由于复位的时候,有时候并不能检测到t是否等于原来的高度,当getHeight() - t<mixThreshold的时候,
                        // 就认为差值已经是0了。
                        if (getHeight() - t < mixThreshold) {//上拉加载,这个时候又不想加载了,又拖动到了原来的地方
                            offsetTopOrBottom(getHeight() - t);
                            pullStatus = 0;//设置状态为正常
                        } else {
                            offsetTopOrBottom(-(distanceY / 2));
                        }
                    }
                }
    
            }
            return false;
        }
    
       /**
         * 手指拖动的时候重新布局
         *
         * @param offY 向下或者向上的距离上次布局的距离
         */
        private void offsetTopOrBottom(float offY) {
            //当偏移量==0 ,不执行下面的操作
            if (offY == 0) {
                return;
            }
            int value = (int) offY;
            //当处于下拉状态,布局recyclerView和headView
            if (pullStatus == 1 || pullStatus == 3) {
                int oldTop = recyclerView.getTop();
                int newTop = oldTop + value;
                recyclerView.layout(recyclerView.getLeft(), newTop, recyclerView.getRight(), recyclerView.getBottom());
                headView.layout(headView.getLeft(), newTop - headView.getHeight(), headView.getRight(), newTop);
                if (newTop > headView.getHeight()) {//newTop为下拉距离,当大于headView的高度,则达到了刷新条件
                    headView.setText("松手刷新");
                } else {
                    headView.setText("下拉刷新");
                }
            } else if (pullStatus == 2 || pullStatus == 4) { //当处于上拉状态,布局recyclerView和footViewView
                int oldBottopm = recyclerView.getBottom();
                int newBottom = oldBottopm + value
                recyclerView.layout(recyclerView.getLeft(), recyclerView.getTop(), recyclerView.getRight(), newBottom);
                recyclerView.scrollBy(0, -value);
                footView.layout(headView.getLeft(), newBottom, headView.getRight(), newBottom + footView.getHeight());
                if (getHeight() - newBottom > footView.getHeight()) {//getHeight() - newBottom为上拉距离,当大于headView的高度,则达到了加载条件
                    footView.setText("松手加载");
                } else {
                    footView.setText("下拉加载");
                }
            }
        }
    
    

    写到这里先看下效果吧
    先写一个设置适配器的方法给外部调用

       public void setAdapter(RecyclerView.Adapter adapter){
            recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
            recyclerView.setAdapter(adapter);
        }
    

    然后在Acticity里面引入这个view,然后写个适配器来测试。
    测试activity:

    public class MainActivity extends AppCompatActivity {
        private SwipeRecycler swipeRecycler;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            swipeRecycler = findViewById(R.id.rcv);
            swipeRecycler.setAdapter(new MyAdapter());
        }
    
        class MyAdapter extends RecyclerView.Adapter<MyAdapter.Holder> {
            @NonNull
            @Override
            public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
                TextView tv = new TextView(parent.getContext());
                tv.setPadding(50, 50, 50, 50);
                return new Holder(tv);
            }
    
            @Override
            public void onBindViewHolder(@NonNull Holder holder, int position) {
                TextView tv = (TextView) holder.itemView;
                tv.setText(position + "");
            }
    
            @Override
            public int getItemCount() {
                return 20;
            }
    
            class Holder extends RecyclerView.ViewHolder {
                public Holder(View itemView) {
                    super(itemView);
                }
            }
        }
    }
    
    1.gif

    这个时候你松开手指,界面是不会动的,接下来就要处理手指松开。


    10.处理手指松开


    首先在onTouchEvent中监听手指松开

      @Override
        public boolean onTouchEvent(MotionEvent event) {
            if (pullStatus == 0) {
                return super.onTouchEvent(event);
            }
           //手指松开
            if (event.getAction() == MotionEvent.ACTION_UP) {
                stop();
            }
            return gestureDetector.onTouchEvent(event);
        }
    

    用动画处理手指松开后的重布局

      private void stop() {
            //处于正常状态
            if (pullStatus == 0) {
                return;
            }
            //检测recyclerView是否发生了滑动
            if (recyclerView.getTop() == 0 && recyclerView.getBottom() == getHeight()) {
                pullStatus = 0;
                return;
            }
            //用属性动画来处理复位
            int start = 0;
            int end = 0;
            if (recyclerView.getTop() == 0) {//上拉
                //上拉的距离大于footView的高度,这个时候就达到加载的条件
                if (getHeight() - recyclerView.getBottom() > footView.getHeight()) {
                    pullStatus = 4;//设置当前状态是处于加载
                }
                start = recyclerView.getBottom();
                if (pullStatus == 4) {
                    footView.setText("正在加载...");
                    //此时bottom刚好显示出footView
                    end = getHeight() - footView.getHeight();
                } else {
                    //此时恢复到初始界面
                    end = getHeight();
                }
            } else {//下拉
                if (recyclerView.getTop() > headView.getHeight()) {
                    pullStatus = 3;//设置当前状态是处于刷新
                }
                start = recyclerView.getTop();
                if (pullStatus == 3) {
                    headView.setText("正在刷新...");
                    end = headView.getHeight();
                } else {
                    end = 0;
                }
            }
            ValueAnimator anim = ValueAnimator.ofInt(start, end);
            anim.setDuration(200);
            anim.setInterpolator(new LinearInterpolator());
            final int finalEnd = end;
            anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    int vaule = (int) animation.getAnimatedValue();
                    offsetTopOrBottomBy(vaule);
                    if (vaule == finalEnd) {
                        //动画结束的时候,不是刷新或者加载状态,设置为正常状态
                        if (pullStatus == 1 || pullStatus == 2) {
                            pullStatus = 0;
                        }
                    }
                }
            });
            anim.start();
        }
    
    
        //复位的时候,重新布局
        private void offsetTopOrBottomBy(int value) {
            if (pullStatus == 1 || pullStatus == 3) {//上拉复位重布局
                recyclerView.layout(recyclerView.getLeft(), value, recyclerView.getRight(), recyclerView.getBottom());
                headView.layout(headView.getLeft(), value - headView.getHeight(), headView.getRight(), value);
            } else if (pullStatus == 2 || pullStatus == 4) {//下拉复位重布局
                recyclerView.layout(recyclerView.getLeft(), recyclerView.getTop(), recyclerView.getRight(), value);
                footView.layout(footView.getLeft(), value, footView.getRight(), value + footView.getHeight());
            }
        }
    

    这个时候再写一个对外停止正在刷新或者正在加载的接口

     public void stopRefreshOrLoadMore() {
            //如果是刷新,停止的时候设置为1,不然系统认为仍是刷新状态,不会执行其他操作
            if (pullStatus==3){
                pullStatus=1;
            }else if (pullStatus==4){
                //如果是加载,停止的时候设置为2,不然系统认为仍是加载状态,不会执行其他操作
                pullStatus=2;
            }
            stop();
        }
    

    最后:还有很多细节需要自己处理,比如设置刷新时候的监听,加载时候的监听,以及做更华丽的headView/footView等等。

    源码地址

    目前为止所有基本工作已经完成,看一波最后的效果图


    效果图

    相关文章

      网友评论

      • 程序_Yuan:问一下简书怎么复制代码成你这样?😷我每次只是粘贴文本
        谢长意:用markdown编辑器编写,百度一下,很简单的。
      • Android程序员老鸦:厉害了 楼主安卓经验几年了
        谢长意:@帅气的昵称呢啊吧 差不多两年吧
      • IT人故事会:老铁,写的很用心,你的文章我收藏了啊
        谢长意:@IT人故事会 谢谢,希望文章对你有帮助

      本文标题:从0开始撸一个自己的下拉刷新上拉加载的RecyclerView

      本文链接:https://www.haomeiwen.com/subject/yffdrftx.html