RecyclerView侧滑删除可以通过ItemTouchHelper来实现,但侧滑菜单栏没有原生的实现方式,我就尝试重写RecyclerView的onInterceptEvent和onTouchEvent方法来实现侧滑菜单,下面来讲下我的实现思路。文章底部有源码,已封装可直接使用。
一、实现效果图
二、实现目标
- 快速左滑或者将itemView侧滑至菜单栏显示过半则打开菜单栏;
- 快速右滑或者将itemView侧滑至菜单栏显示过未半则关闭菜单栏;
- 点击菜单栏按钮或点击其他itemView,关闭菜单栏;
- 竖直滑动RecyclerView,关闭菜单栏;
- 打开其他itemView的菜单栏,关闭之前itemView的菜单栏;
- 松手后的菜单栏滑动平缓
- 不影响原先RecyclerView的功能
三、思路分析
-
理清触碰事件分发
先来简单分析下触碰事件分发,我们可以把RecyclerView看作ViewGroup,把其itemView看作View(itemView应为ViewGroup,但此处只用考虑itemView及其子View是否消费事件)。
假设ViewGroup不做拦截操作,View.onTouchEvent返回true:
ACTION_DOWN:ViewGroup.dispatchTouchEvent→ViewGroup.onInterceptTouchEvent→View.dispatchTouchEvent→View.onTouchEvent;
ACTION_MOVE:ViewGroup.dispatchTouchEvent→ViewGroup.onInterceptTouchEvent→View.dispatchTouchEvent→View.onTouchEvent;
ACTION_UP:ViewGroup.dispatchTouchEvent→ViewGroup.onInterceptTouchEvent→View.dispatchTouchEvent→View.onTouchEvent;
假设ViewGroup在onInterceptTouchEvent中对ACTION_MOVE事件做拦截操作,且View.onTouchEvent返回true:
ACTION_DOWN:ViewGroup.dispatchTouchEvent→ViewGroup.onInterceptTouchEvent→View.dispatchTouchEvent→View.onTouchEvent;
ACTION_MOVE:ViewGroup.dispatchTouchEvent→ViewGroup.onInterceptTouchEvent→ViewGroup.onTouchEvent;
再次ACTION_MOVE:ViewGroup.dispatchTouchEvent→ViewGroup.onTouchEvent;
ACTION_UP:ViewGroup.dispatchTouchEvent→ViewGroup.onTouchEvent;
详细的触碰事件体系分发可查看:事件分发机制
综上分析,只要ACTION_DOWN事件到达RecyclerView,该事件必走onInterceptTouchEvent()方法,所以在该方法中可做一些信息准备工作。应在RecyclerView的onInterceptTouchEvent()方法做水平滑动判断拦截,一旦拦截则后续事件不会再往下分发,后续事件一到RecyclerView的dispatchTouchEvent()方法就会进入到onTouchEvent()方法。
-
判断好RecyclerView是竖直滑动还是侧滑
RecyclerView是水平滑动可根据多方面判断:
- 水平滑动距离大于竖直滑动距离,且大于系统最小滑动距离(根据事件中的坐标计算判断)
- 水平速度大于竖直滑动速度,且大于规定最小滑动速度(使用VelocityTracker计算判断)
-
松开手时实现平缓滑动
松手时使菜单栏平缓滑动到指定区,这需使用Scroller的startScroll()方法,重写RecyclerView的computeScroll()方法计算并通过itemview.scrollTo(x,y)方法移动itemView的位置,一直刷新直到itemView到达终点位置。
public methods:
void | abortAnimation() 停止动画. |
boolean | computeScrollOffset() 计算出新的位置,如果返回true,则动画尚未完成. |
final int | getCurrX() 返回滚动中的当前X偏移量. |
final int | getCurrY() 返回滚动中的当前Y偏移量. |
final int | getFinalX() 返回滚动的X轴结束位置. |
final int |
|
fnal boolean | isFinished() 返回Scroller是否已完成滚动. |
void | startScroll(int startX, int startY, int dx, int dy, int duration) 通过提供的起始点、要行驶的距离和滚动的时间来开始滚动. |
四、代码实现
@Overridepublic boolean onInterceptTouchEvent(MotionEvent e) {int x = (int) e.getX();int y = (int) e.getY();addVelocityEvent(e);switch (e.getAction()){case MotionEvent.ACTION_DOWN:......//获取点击区域所在的itemViewmMoveView = (ViewGroup) findChildViewUnder(x, y);//在点击区域以外的itemView开着菜单,则关闭菜单if (mLastView != null && mLastView != mMoveView && mLastView.getScrollX() != 0){closeMenu();}//获取itemView中菜单的宽度(规定itemView中为两个子View)if (mMoveView != null && mMoveView.getChildCount() == 2){mMenuWidth = mMoveView.getChildAt(1).getWidth();}else {mMenuWidth = -1;}break;case MotionEvent.ACTION_MOVE:mVelocity.computeCurrentVelocity(1000);int velocityX = (int) Math.abs(mVelocity.getXVelocity());int velocityY = (int) Math.abs(mVelocity.getYVelocity());int moveX = Math.abs(x - mFirstX);int moveY = Math.abs(y - mFirstY);//满足如下条件其一则判定为水平滑动://1、水平速度大于竖直速度,且水平速度大于最小速度//2、水平位移大于竖直位移,且大于最小移动距离//必需条件:itemView菜单栏宽度大于0,且recyclerView处于静止状态(即并不在竖直滑动和拖拽)boolean isHorizontalMove = (Math.abs(velocityX) >= MINIMUM_VELOCITY && velocityX > velocityY || moveX > moveY && moveX > mTouchSlop) && mMenuWidth > 0 && getScrollState() == 0;if (isHorizontalMove){//设置其已处于水平滑动状态,并拦截事件mMoving = true;return true;}break;case MotionEvent.ACTION_UP:case MotionEvent.ACTION_CANCEL:releaseVelocity();//itemView以及其子view触发触碰事件(点击、长按等),菜单未关闭则直接关闭if (mLastView != null && mLastView.getScrollX() != 0){mLastView.scrollTo(0,0);}break;default:break;}return super.onInterceptTouchEvent(e);}
首先在ACTION_DOWN的时候用RecyclerView的findChildViewUnder()方法获得所点区域的itemView,mLastView为末次水平滑动的itemView,若这次点击区域不再是末次操作的itemView,且末次的itemView菜单栏还打开着则关闭它,并获取当前操作的itemView的菜单栏宽度;在ACTION_MOVE时,利用VelocityTracker计算速度和坐标点位移差进行判断是否为水平滑动,满足条件则进行拦截走onTouchEvent()方法;ACTION_UP即响应itemView点击事件,关闭未关闭的菜单栏。
@Overridepublic boolean onTouchEvent(MotionEvent e) {int x = (int) e.getX();int y = (int) e.getY();addVelocityEvent(e);switch (e.getAction()){case MotionEvent.ACTION_DOWN:break;case MotionEvent.ACTION_MOVE://若已处于水平滑动状态,则随手指滑动,否则进行条件判断if (mMoving){int dx = mLastX - x;//让itemView在规定区域随手指移动if (mMoveView.getScrollX() + dx >= 0 && mMoveView.getScrollX() + dx <= mMenuWidth) {mMoveView.scrollBy(dx, 0);}mLastX = x;return true;}else {......//根据水平滑动条件判断,是否让itemView跟随手指滑动boolean isHorizontalMove = (Math.abs(velocityX) >= MINIMUM_VELOCITY && velocityX > velocityY|| moveX > moveY && moveX > mTouchSlop) && mMenuWidth > 0 && getScrollState() == 0;if (isHorizontalMove) {int dx = mLastX - x;//让itemView在规定区域随手指移动if (mMoveView.getScrollX() + dx >= 0 && mMoveView.getScrollX() + dx <= mMenuWidth) {mMoveView.scrollBy(dx, 0);}mLastX = x;//设置正处于水平滑动状态mMoving = true;return true;}}break;case MotionEvent.ACTION_UP:case MotionEvent.ACTION_CANCEL:if (mMoving) {//先前没结束的动画终止,并直接到终点if (!mScroller.isFinished()){mScroller.abortAnimation();mLastView.scrollTo(mScroller.getFinalX(),0);}mMoving = false;//已放手,即现滑动的itemView成了末次滑动的itemViewmLastView = mMoveView;mVelocity.computeCurrentVelocity(1000);int scrollX = mLastView.getScrollX();//若速度大于正方向最小速度,则关闭菜单栏;若速度小于反方向最小速度,则打开菜单栏//若速度没到判断条件,则对菜单显示的宽度进行判断打开/关闭菜单if (mVelocity.getXVelocity() >= MINIMUM_VELOCITY){mScroller.startScroll(scrollX, 0, -scrollX, 0, Math.abs(scrollX));}else if (mVelocity.getXVelocity() <= -MINIMUM_VELOCITY){int dx = mMenuWidth - scrollX;mScroller.startScroll(scrollX, 0, dx, 0, Math.abs(dx));} else if (scrollX > mMenuWidth / 2) {int dx = mMenuWidth - scrollX;mScroller.startScroll(scrollX, 0, dx, 0, Math.abs(dx));} else {mScroller.startScroll(scrollX, 0, -scrollX, 0, Math.abs(scrollX));}invalidate();} else if (mLastView != null && mLastView.getScrollX() != 0){//若不是水平滑动状态,菜单栏开着则关闭closeMenu();}releaseVelocity();break;default:break;}return super.onTouchEvent(e);}
因为ACTION_DOWN事件必会走onInterceptonTouchEvent()方法,所以已在其方法中做好全部处理,该方法中就不必再对ACTION_DOWN事件做处理;在ACTION_MOVE时,先进行判断是否已处于水平滑动状态,若是则直接让itemView跟随手指滑动,若不是则进行水平滑动判断;在ACTION_UP时,若处于水平滑动状态,之前的Scroller动画还没结束,则让其终止并直接到达终点值,因松手后当前这个itemView也马上要使用Scroller进行动画了,避免冲突,后续工作就是对末次itemView进行赋值,并先根据速度判断开关菜单栏,速度条件不满足则对itemView的菜单栏显露宽度进行判断,如果不是水平滑动时还有菜单栏开着,在最后放手时关闭菜单栏。
五、源码地址
源码地址:支持侧滑菜单栏的RecyclerView
使用规范:LayoutManager为LinearLayoutManager,且为竖直滑动,RecyclerView的itemView中需有两个子View,第二个子View即为菜单栏。
如有问题欢迎指出。