可以下拉刷新,上拉加载更多的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相关文章推荐
- SQL字符串处理函数大全
- 选择小波函数的一般原则和尺度的选择
- Linux 常见命令说明
- GitHub 第三方开源框架总结
- acer笔记本安装ubuntu后,无线网卡不能使用
- C中时间/环境/终端控制相关函数
- 函数指针,结构体指针
- Linux查找替换
- 《深入理解mybatis原理》 MyBatis的二级缓存的设计原理
- 天之痕——Tacke竹桑
- Redis原理介绍
- 彻底解决git中.gitignore文件失效原因及解决办法
- 软件工程个人总结
- Spinner控件
- android中listview的item可以获取到点击事件的方法
- 剑指Offer----面试题17:合并两个排序的链表
- 梳理caffe代码pooling_layer(二十)
- n位数字删除数问题
- 硬件加速机制
- 管理口令(P):[INS-30011] 输入的 ADMIN 口令不符合 Oracle 建议的标准