Android DemoAndroid开发Android技术知识

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

2018-05-04  本文已影响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等等。

源码地址

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


效果图
上一篇下一篇

猜你喜欢

热点阅读