一、前言

二、将要实现的效果


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


        2.动图展示


Android recyclerview 聊天页面下拉加载聊天记录 recyclerview 上拉加载更多_初始化

Android recyclerview 聊天页面下拉加载聊天记录 recyclerview 上拉加载更多_自定义_02

Android recyclerview 聊天页面下拉加载聊天记录 recyclerview 上拉加载更多_自定义_02

Android recyclerview 聊天页面下拉加载聊天记录 recyclerview 上拉加载更多_自定义_04


Android recyclerview 聊天页面下拉加载聊天记录 recyclerview 上拉加载更多_初始化_05

三、实现思路

        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.到达的顶端之后我们还需要计算拖拽的距离,同时显示出下拉的动画,具体计算的方法我们在下面去做详细解释。
   

四、核心代码以及注释 


/**
     * 只有当滑动距离小于某个值的时候,才会将事件向下传递
     *
     * @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很有可能不会显示不会滑动(感兴趣可以试试)。


@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);
    }



/**
     * 设置加载栏 松开立即刷新
     */
    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);
    }



        判断拖拽边界的方法


/**
     * 是否滑到顶部
     *
     * @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);
    }



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


<?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的不同之外,其他的和上一种方法一样。

六、源码