可以下拉刷新,上拉加载更多的RecyclerView

一、前言

         在移动开发过程中,当我们遇到信息展示的时候,往往会选择使用列表的形式。那在Android开发中,我们则会选用ListView来实现这种需求。但是随着Google的不断更新,它为我们提供了新的选择--RecyclerView。由于RecyclerView扩展性比较强,它的一些功能需要使用者自己动手去实现,举个例子:ListView中的headerView与footerView就是它所不具备的。伴随着数据量的增大,也会出现一些需要分页显示数据的情况(上拉加载更多),ListView有很多开源的框架去支持它,但是RecyclerView则没有那么方便,虽然官方提供了SwipeRefreshLayout,但是它有时也满足不了设计师的天马星空的想象。那怎么办?哼哼,是时候来一波自定义了。

二、将要实现的效果

        1.文字描述

        RecyclerView的下拉刷新,上拉加载更多

        2.动图展示


三、实现思路

        1.需要实现的列表具有RecyclerView的所有属性,所以继承RecyclerView
        2.参考SwipeRefreshLayout,我们重写RecyclerView的拦截监听onInterceptTouchEvent,以及触摸监听onTouchEvent
        3.同时我们需要根据手势以及RecyclerView的状态调整ViewGroup的样式,比如位置,以及刷新UI的显示隐藏等,我们先使用最简单粗暴的方法,View.setTop();View.setBottom;View.setLeft();View.setRight()。
        4.为了解决刷新重复刷新的问题,我们使用boolean isRefresh来判断是否处于刷新状态,若是处于刷新状态,则拦截所有touchEvent,同样的原理,当我们执行加载更多的时候也会添加设置一个标志来拦截事件
        5.判断时候已经到达顶端的函数我们使用SwipeRefreshLayout中的方法ViewCompat.canScrollVertically去做判断
        6.到达的顶端之后我们还需要计算拖拽的距离,同时显示出下拉的动画,具体计算的方法我们在下面去做详细解释。
        7.除此之外我们还需要一个回调,当下拉到最大距离并且用户收起手指时,我们应该回调一个刷新的监听,这里使用接口(interface)去实现

四、核心代码以及注释 

        1.重写onInterceptTouchEvent方法

    /**
     * 只有当滑动距离小于某个值的时候,才会将事件向下传递
     *
     * @param e
     * @return 返回false向下传递,返回true自己处理
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent e) {
        if (isReFreshIng) {
            return true;
        }
        boolean superTouch = super.onInterceptTouchEvent(e);
        boolean thisTouch = true;//true 拦截
        final int action = MotionEventCompat.getActionMasked(e);
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                if (mRecyclerView!=null){
                    mInitialHigh = mRecyclerView.getHeight();//初始化recyclerView高度
                    mInitialWidth = mRecyclerView.getWidth();
                }
                mActivePointerId = MotionEventCompat.getPointerId(e, 0);
                if (getOrientation == LINEAR_VERTICAL) {
                    mInitialMotionY = getMotionEventY(e, mActivePointerId);
                }
                if (getOrientation == GRID_HORIZONTAL) {
                    mInitialMotionX = getMotionEventX(e, mActivePointerId);
                }
                mIsBeingDragged = false;//recyclerView是否处在拖拽状态中
                thisTouch = false;
                break;
            case MotionEvent.ACTION_MOVE:
                final int pointerIndex = MotionEventCompat.findPointerIndex(e, mActivePointerId);//手机接触屏幕的点的位置
                if (pointerIndex < 0) {
                    //不可用的触点
                    Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");
                    return false;
                }
                if (getOrientation == LINEAR_VERTICAL) {
                    final float y = MotionEventCompat.getY(e, pointerIndex);//触点Y坐标值
                    final float overscrollTop = (y - mInitialMotionY);//超过这个值,将执行刷新
                    if (overscrollTop * overscrollTop < 1) {//该条件判断的是手势移动是否大于1个最低计量单位,使用乘法考虑的是为了正负方向都适配
                        //向下传递 传递到子
                        thisTouch = false;
                    } else {
                        //自己处理
                        thisTouch = true;
                    }
                }
                if (getOrientation == GRID_HORIZONTAL) {
                    final float x = MotionEventCompat.getX(e, pointerIndex);
                    final float overscrollLeft = (x - mInitialMotionX);
                    if (overscrollLeft * overscrollLeft < 1) {
                        thisTouch = false;
                    } else {
                        thisTouch = true;
                    }
                }
                break;
            case MotionEventCompat.ACTION_POINTER_UP:
                onSecondaryPointerUp(e);
                break;

            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mIsBeingDragged = false;
                mActivePointerId = INVALID_POINTER;
                break;
        }
        return thisTouch && superTouch;
    }</span>

        当响应点击事件的时候RecyclerView已经绘制完成所以.getHeight()可以获取到recyclerView的高度,我们使用该方法记录recyclerView的高,待以后使用。
        getOrientation == LINEAR_VERTICAL
       上面这个条件判断的是RecyclerView 的LayoutManager以满足recyclerView特定的需求:当为LinearLayoutManager的时候支持下拉刷新上拉加载,当为GridLayoutManager的时候支持左划刷新右划加载更多。
可以看到该方法的返回值由thisTouch与superTouch同时决定,因为我们的列表一定要实现父类RecyclerView的拦截内容。如果没有实现super.onInterceptTouchEvent(e),那你的RecyclerView很有可能不会显示不会滑动(感兴趣可以试试)。

        2.重写onTouchEvent方法

    @Override
    public boolean onTouchEvent(MotionEvent e) {
        if (isReFreshIng) {
            //如果正在执行刷新,则不响应任何touch事件
            return false;
        }
        boolean superTouch = super.onTouchEvent(e);//需要完全继承recyclerView父类的TouchEvent
        boolean thisTouch = true;//根据需要新添加的touch事件处理返回值
        final int action = MotionEventCompat.getActionMasked(e);
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mActivePointerId = MotionEventCompat.getPointerId(e, 0);//获取到活跃的触摸点id
                if (getOrientation == LINEAR_VERTICAL) {
                    mInitialMotionX = getMotionEventX(e, mActivePointerId);
                    mInitialMotionY = getMotionEventY(e, mActivePointerId);//初始化第一个触摸点
                    mInitialTargetY = mRecyclerView.getTop() + mRecyclerView.getPaddingTop();//初始化recyclerView的初始y坐标
                    mIsBeingDragged = false;//初始化拖拽状态
                }
                if (getOrientation == GRID_HORIZONTAL) {
                    mInitialMotionX = getMotionEventX(e, mActivePointerId);
                    mInitialTargetX = mRecyclerView.getLeft() + mRecyclerView.getPaddingLeft();
                    mIsBeingDragged = false;
                }
                break;
            case MotionEvent.ACTION_MOVE: {
                final int pointerIndex = MotionEventCompat.findPointerIndex(e, mActivePointerId);//手机接触屏幕的点的位置
                if (pointerIndex < 0) {
                    //不可用的触点
                    Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");
                    return false;
                }
                if (getOrientation == LINEAR_VERTICAL) {
                    if (!ifCouldPullDown()) {
                        //到顶
                        mIsBeingDragged = true;
                    }

                    final float y = MotionEventCompat.getY(e, pointerIndex);//触点Y坐标值
                    final float x = MotionEventCompat.getX(e, pointerIndex);
                    final float overscrollLeft = (x - mInitialMotionX) * DRAG_RATE;
                    final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;//超过这个值,将执行刷新
                    if (canParentScroll && (overscrollLeft * overscrollLeft > overscrollTop * overscrollTop)) {
                        return false;
                    }
                    if (mIsBeingDragged && !isReFreshIng) {
                        float originalDragPercent = overscrollTop / mTotalDragDistance;//下拉的距离站总距离的百分比
                        if (originalDragPercent < 0) {
                            return false;
                        }
                        float dragPercent = Math.min(1f, Math.abs(originalDragPercent));//拖拽百分比
                        if (originalDragPercent >= 0) {
                            if (overscrollTop < mTotalDragDistance) {
                                //按比例放大动画效果
                                setPullDown((int) overscrollTop);
                            } else {
                                //动画效果达到最大不再改变
                                setLoosen();
                            }
                        }
                    }
                }
                if (getOrientation == GRID_HORIZONTAL) {
                    if (!ifCouldPullRight()) {
                        //到最左边
                        mIsBeingDragged = true;
                    }
                    if (!ifCouldPullLeft()) {
                        mIsBeingDragged = true;
                    }
                    final float x = MotionEventCompat.getX(e, pointerIndex);
                    final float overscrollRight = (x - mInitialMotionX) * DRAG_RATE;
                    if (mIsBeingDragged && !isReFreshIng) {
                        float originalDragPercent = overscrollRight / mTotalDragDistance;//右滑的距离站总距离的百分比
                        if (originalDragPercent < 0) {
                            return false;
                        }
                        if (originalDragPercent >= 0) {
                            if (overscrollRight < mTotalDragDistance) {
                                //按比例放大动画效果
                            } else {
                                //动画效果达到最大不再改变
                            }
                        }
                    }
                }

                break;
            }
            case MotionEventCompat.ACTION_POINTER_DOWN: {
                final int index = MotionEventCompat.getActionIndex(e);
                mActivePointerId = MotionEventCompat.getPointerId(e, index);
            }
            case MotionEventCompat.ACTION_POINTER_UP:
                onSecondaryPointerUp(e);
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL: {
                if (mActivePointerId == INVALID_POINTER) {
                    if (action == MotionEvent.ACTION_UP) {
                        Log.e(LOG_TAG, "Got ACTION_UP event but don't have an active pointer id.");
                    }
                    return false;
                }
                final int pointerIndex = MotionEventCompat.findPointerIndex(e, mActivePointerId);
                if (getOrientation == LINEAR_VERTICAL) {
                    final float y = MotionEventCompat.getY(e, pointerIndex);
                    final float x = MotionEventCompat.getX(e, pointerIndex);
                    final float overscrollLeft = (x - mInitialMotionX) * DRAG_RATE;
                    final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;//划过的距离
                    if (canParentScroll && (overscrollLeft * overscrollLeft > overscrollTop * overscrollTop)) {
                        return false;
                    }
                    if (mIsBeingDragged) {
                        if (overscrollTop > mTotalDragDistance && !isReFreshIng) {
                            //refresh划过的距离大于约定距离执行刷新(未处于刷新状态)
                            if (onRefreshListener != null) {
                                onRefreshListener.onRefresh();
                            } else {
                                Log.e("recyclerView", "onRefreshListener is null");
                            }

                        } else {
                            //cancel refresh
                            if (!isReFreshIng) {
                                setViewBack();
                            }
                        }
                    }
                    if (!isReFreshIng) {
                        if (!ifCouldPullUp()) {
                            if (overscrollTop < 0 && (overscrollTop * overscrollTop > mTotalDragDistance * mTotalDragDistance)) {
                                if (onLoadMoreListener != null) {
                                    onLoadMoreListener.onLoadMore();
                                } else {
                                    Log.e("recyclerView", "onLoadMoreListener is null");
                                }
                            }
                        }
                    }
                    mIsBeingDragged = false;
                    mActivePointerId = INVALID_POINTER;
                    return false;
                }
                if (getOrientation == GRID_HORIZONTAL) {
                    final float x = MotionEventCompat.getX(e, pointerIndex);
                    final float overscrollLeft = (x - mInitialMotionX) * DRAG_RATE;
                    if (mIsBeingDragged) {
                        if (overscrollLeft > mTotalDragDistance && !isReFreshIng) {
                            if (onRefreshListener != null) {
                                onRefreshListener.onRefresh();
                            } else {
                                Log.e("recyclerView", "onRefreshListener is null");
                            }

                        } else {
                            if (!isReFreshIng) {
                                //初始化所有view位置
                            }
                        }
                    }
                    if (mIsBeingDragged && !isReFreshIng) {
                        if (!ifCouldPullLeft()) {
                            if (overscrollLeft < 0 && (overscrollLeft * overscrollLeft > mTotalDragDistance * mTotalDragDistance)) {
                                if (onLoadMoreListener != null) {
                                    onLoadMoreListener.onLoadMore();
                                } else {
                                    Log.e("recyclerView", "onLoadMoreListener is null");
                                }
                            }
                        }
                    }
                    mIsBeingDragged = false;
                    mActivePointerId = INVALID_POINTER;
                    return false;
                }
            }
            thisTouch = true;
        }
        return thisTouch && superTouch;
    }

        在onTouchEvent中我们将判断下拉手势,计算下拉距离,同时显示下拉刷新布局。这些处理都将在MotionEvent.ACTION_MOVE 中进行。
        计算下拉距离有本段代码完成

if (getOrientation == LINEAR_VERTICAL) {
	if (!ifCouldPullDown()) {
		</span>//到顶
		</span>mIsBeingDragged = true;
	}

	final float y = MotionEventCompat.getY(e, pointerIndex);//触点Y坐标值
	final float x = MotionEventCompat.getX(e, pointerIndex);
	final float overscrollLeft = (x - mInitialMotionX) * DRAG_RATE;
	final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;//超过这个值,将执行刷新
	if (canParentScroll && (overscrollLeft * overscrollLeft >
                                                 overscrollTop * overscrollTop)) {
		//这个条件可以判断,手势的滑动方向
		return false;
	}
	if (mIsBeingDragged && !isReFreshIng) {
		float originalDragPercent = overscrollTop / mTotalDragDistance;//下拉的距离站总距离的百分比
			if (originalDragPercent < 0) {
                            return false;
                        }
                        float dragPercent = Math.min(1f, Math.abs(originalDragPercent));//拖拽百分比
                        if (originalDragPercent >= 0) {
                            if (overscrollTop < mTotalDragDistance) {
                                //按比例放大动画效果
                                setPullDown((int) overscrollTop);
                            } else {
                                //动画效果达到最大不再改变
                                setLoosen();
                            }
                        }
                    }
        }

        加载下拉刷新布局并向下滑出由setPullDown完成,该函数通过setTop,setBottom改变recyclerView以及下拉刷新布局的位置。

    /**
     * 设置加载栏 向下拖拽
     */
    private void setPullDown(int overscrollTop) {
        if (loadView==null||loosenView==null||refreshView==null||mRecyclerView==null){
            return;
        }
        loadView.setTop(mInitialTargetY - loadView.getHeight() + overscrollTop);
        loadView.setBottom(mInitialTargetY + overscrollTop);
        loosenView.setTop(mInitialTargetY - loosenView.getHeight() + overscrollTop);
        loosenView.setBottom(mInitialTargetY + overscrollTop);
        refreshView.setTop(mInitialTargetY - refreshView.getHeight() + overscrollTop);
        refreshView.setBottom(mInitialTargetY + overscrollTop);
        mRecyclerView.setTop(mInitialTargetY + overscrollTop);
        mRecyclerView.setBottom(mInitialTargetY + mInitialHigh + overscrollTop);
        loadView.setVisibility(VISIBLE);
        loosenView.setVisibility(INVISIBLE);
        refreshView.setVisibility(INVISIBLE);
    }

        当下拉超过一定距离的时候则不再改变布局样式,该功能由setLoosen方法完成

    /**
     * 设置加载栏 松开立即刷新
     */
    private void setLoosen() {
        if (loadView==null||loosenView==null||refreshView==null||mRecyclerView==null){
            return;
        }
        loadView.setTop(mInitialTargetY - loadView.getHeight() + (int) mTotalDragDistance);
        loadView.setBottom(mInitialTargetY + (int) mTotalDragDistance);
        loosenView.setTop(mInitialTargetY - loosenView.getHeight() + (int) mTotalDragDistance);
        loosenView.setBottom(mInitialTargetY + (int) mTotalDragDistance);
        refreshView.setTop(mInitialTargetY - refreshView.getHeight() + (int) mTotalDragDistance);
        refreshView.setBottom(mInitialTargetY + (int) mTotalDragDistance);
        mRecyclerView.setTop(mInitialTargetY + (int) mTotalDragDistance);
        mRecyclerView.setBottom(mInitialTargetY + (int) mTotalDragDistance + mInitialHigh);
        loadView.setVisibility(INVISIBLE);
        loosenView.setVisibility(VISIBLE);
        refreshView.setVisibility(INVISIBLE);
    }

        3.一些比较重要的小方法

        判断拖拽边界的方法
    /**
     * 是否滑到顶部
     *
     * @return true 没有到顶;false 到达顶部
     */
    private boolean ifCouldPullDown() {
        if (android.os.Build.VERSION.SDK_INT < 14) {
            return ViewCompat.canScrollVertically(mRecyclerView, -1) || mRecyclerView.getScrollY() > 0;
        } else {
            return ViewCompat.canScrollVertically(mRecyclerView, -1);
        }
    }

    /**
     * 是否滑到底部
     *
     * @return true 没有到底;false 到达底部
     */
    private boolean ifCouldPullUp() {
        if (android.os.Build.VERSION.SDK_INT < 14) {
            return ViewCompat.canScrollVertically(mRecyclerView, 1) || mRecyclerView.getScrollY() > 0;
        } else {
            return ViewCompat.canScrollVertically(mRecyclerView, 1);
        }
    }

    /**
     * 是否可以右滑
     *
     * @return true 可以,false 不可以
     */
    private boolean ifCouldPullRight() {
        if (Build.VERSION.SDK_INT < 14) {
            return ViewCompat.canScrollHorizontally(mRecyclerView, -1) || mRecyclerView.getScrollX() > 0;
        } else {
            return ViewCompat.canScrollHorizontally(mRecyclerView, -1);
        }
    }

    /**
     * 是否可以左滑
     *
     * @return true 可以,false 不可以
     */
    private boolean ifCouldPullLeft() {
        if (Build.VERSION.SDK_INT < 14) {
            return ViewCompat.canScrollHorizontally(mRecyclerView, 1) || mRecyclerView.getScrollX() > 0;
        } else {
            return ViewCompat.canScrollHorizontally(mRecyclerView, 1);
        }
    }

        获取触摸点坐标值的方法
    /**
     * 该方法获取触点的y坐标值
     *
     * @param ev
     * @param activePointerId
     * @return
     */
    private float getMotionEventY(MotionEvent ev, int activePointerId) {
        final int index = MotionEventCompat.findPointerIndex(ev, activePointerId);
        if (index < 0) {
            return -1;
        }
        return MotionEventCompat.getY(ev, index);
    }

    /**
     * 该方法获取触点的x坐标值
     *
     * @param ev
     * @param activePointerId
     * @return
     */
    private float getMotionEventX(MotionEvent ev, int activePointerId) {
        final int index = MotionEventCompat.findPointerIndex(ev, activePointerId);
        if (index < 0) {
            return -1;
        }
        return MotionEventCompat.getX(ev, index);
    }

        4.下拉布局的实现

        将下拉布局分成三部分,下拉部分,边界部分,刷新部分,分别在三种拖拽状态中替换三者下拉时显示下拉部分,拖拽到最大时显示边界部分,松开刷新时显示刷新部分,下面是布局文件
        layout_pull_operate_view.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <LinearLayout
        android:id="@+id/ll_pull_fresh"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:orientation="horizontal"
        android:padding="10dp"
        android:visibility="gone">

        <ImageView
            android:id="@+id/img_refresh_tag"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@mipmap/icon_pull_down_arrow" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="下拉刷新..."
            android:textColor="#666666"
            android:textSize="16sp" />
    </LinearLayout>

    <LinearLayout
        android:id="@+id/ll_pull_fresh_l"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:orientation="horizontal"
        android:padding="10dp"
        android:visibility="gone">

        <ImageView
            android:id="@+id/img_refresh_tag_l"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@mipmap/icon_pull_up_arrow" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="松开刷新..."
            android:textColor="#666666"
            android:textSize="16sp" />
    </LinearLayout>

    <LinearLayout
        android:id="@+id/ll_pull_loading"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:orientation="horizontal"
        android:padding="10dp"
        android:visibility="gone">

        <ImageView
            android:id="@+id/img_refresh_loading"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@mipmap/icon_pull_refresh" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="正在更新..."
            android:textColor="#666666"
            android:textSize="16sp" />
    </LinearLayout>
</FrameLayout>

五、使用以及注意事项

        1.在Activity中使用

        在Activity使用时需要把原来的<.RecyclerView/>替换成图中的代码。

把recyclerView替换成图二内的<FrameLayout/>中的全部内容:recyclerView的id自行命名

但是下面的layout_pull_operate_view不要做任何改动

        在java中的代码:

        首先,该PullToOperateRecyclerView只支持LinearLayoutManager的竖向下拉刷新与上拉加载,以及GridLayoutManager的横向的 右滑刷新左滑加载

        初始化:其中Adapter的使用和RecyclerView完全一样,这里不再赘述

        PullToOperateRecyclerView可以设置两个回调,刷新回调OnRefreshListener,加载更多回调OnLoadMoreListener。

        在调用刷新的时候,务必要在回调接口里面使用 .setRefresh()方法,不然无法触发刷新动画

同时(在使用GridLayoutManager的时候无需调用),在完成数据的刷新之后一定要再调用.setViewBack()方法,不然下拉之后recyclerView就无法回到初始位置(在使用GridLayoutManager的时候无需调用)

        2.在Fragment中使用

        布局方法与上面完全一样

        使用方法略有不同,在为recyclerView设置Adapter之前一定要先调用.setRootView(view)

        其中,该view是指fragment的rootView

        除了在setRootView的不同之外,其他的和上一种方法一样。

六、源码

        https://git.oschina.net/mr-zhang/TestUtils.git

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值