您的位置:首页 > 其它

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

2016-06-04 13:02 736 查看

一、前言


在移动开发过程中,当我们遇到信息展示的时候,往往会选择使用列表的形式。那在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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: