一、前言
二、将要实现的效果
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.到达的顶端之后我们还需要计算拖拽的距离,同时显示出下拉的动画,具体计算的方法我们在下面去做详细解释。
四、核心代码以及注释
/**
* 只有当滑动距离小于某个值的时候,才会将事件向下传递
*
* @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的不同之外,其他的和上一种方法一样。
六、源码