RecyclerView侧滑删除可以通过ItemTouchHelper来实现,但侧滑菜单栏没有原生的实现方式,我就尝试重写RecyclerView的onInterceptEvent和onTouchEvent方法来实现侧滑菜单,下面来讲下我的实现思路。文章底部有源码,已封装可直接使用。
先来简单分析下触碰事件分发,如下图所示,我们可以把RecyclerView看作ViewGroup,把其itemView看作View(itemView应为ViewGroup,但此处只用考虑itemView及其子View是否消费事件)。
下面展示下单单水平滑动事件分发(定RecyclerView的onTouchEvent()会消耗事件)。
itemView有点击事件时:
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;
当itemView无点击事件时:
ACTION_DOWN:ViewGroup.dispatchTouchEvent→ViewGroup.onInterceptTouchEvent→View.dispatchTouchEvent→View.onTouchEvent→ViewGroup.onTouchEvent;
ACTION_MOVE:
ViewGroup.dispatchTouchEvent→ViewGroup.onTouchEvent;
ACTION_UP:ViewGroup.dispatchTouchEvent→ViewGroup.onInterceptTouchEvent→View.dispatchTouchEvent→View.onTouchEvent;
综上分析,只要ACTION_DOWN事件到达RecyclerView,该事件必走onInterceptTouchEvent()方法,所以在该方法中可做一些信息准备工作。应在RecyclerView的onInterceptTouchEvent()方法和onTouchEvent()方法中都做水平滑动判断拦截,一旦拦截则后续事件不会再往下分发,后续事件一到该控件的dispatchTouchEvent()方法就会进入到onTouchEvent()方法。
RecyclerView是水平滑动可根据多方面判断:
松手时使菜单栏平缓滑动到指定区,这需使用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) 通过提供的起始点、要行驶的距离和滚动的时间来开始滚动. |
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
int x = (int) e.getX();
int y = (int) e.getY();
addVelocityEvent(e);
switch (e.getAction()){
case MotionEvent.ACTION_DOWN:
......
//获取点击区域所在的itemView
mMoveView = (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点击事件,关闭未关闭的菜单栏。
@Override
public 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跟随手指滑动
//这里重新判断是避免itemView中不拦截ACTION_DOWN事件,则后续ACTION_MOVE并不会走onInterceptTouchEvent()方法
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成了末次滑动的itemView
mLastView = 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跟随手指滑动,若不是则进行水平滑动判断,这里重新判断是避免itemView中不拦截ACTION_DOWN事件,则后续ACTION_MOVE并不会走onInterceptTouch-Event()方法,所以此处需加判断;在ACTION_UP时,若处于水平滑动状态,之前的Scroller动画还没结束,则让其终止并直接到达终点值,因松手后当前这个itemView也马上要使用Scroller进行动画了,避免冲突,后续工作就是对末次itemView进行赋值,并先根据速度判断开关菜单栏,速度条件不满足则对itemView的菜单栏显露宽度进行判断,如果不是水平滑动时还有菜单栏开着,在最后放手时关闭菜单栏。
源码地址:支持侧滑菜单栏的RecyclerView
使用规范:LayoutManager为LinearLayoutManager,且为竖直滑动,RecyclerView的itemView中需有两个子View,第二个子View即为菜单栏。
如有问题欢迎指出。