Android RecyclerView —— 实现侧滑菜单

    RecyclerView侧滑删除可以通过ItemTouchHelper来实现,但侧滑菜单栏没有原生的实现方式,我就尝试重写RecyclerView的onInterceptEvent和onTouchEvent方法来实现侧滑菜单,下面来讲下我的实现思路。文章底部有源码,已封装可直接使用。

一、实现效果图

Android RecyclerView —— 实现侧滑菜单_第1张图片

二、实现目标

  1. 快速左滑或者将itemView侧滑至菜单栏显示过半则打开菜单栏;
  2. 快速右滑或者将itemView侧滑至菜单栏显示过未半则关闭菜单栏;
  3. 点击菜单栏按钮或点击其他itemView,关闭菜单栏;
  4. 竖直滑动RecyclerView,关闭菜单栏;
  5. 打开其他itemView的菜单栏,关闭之前itemView的菜单栏;
  6. 松手后的菜单栏滑动平缓
  7. 不影响原先RecyclerView的功能

三、思路分析

  • 理清触碰事件分发

    先来简单分析下触碰事件分发,如下图所示,我们可以把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;

Android RecyclerView —— 实现侧滑菜单_第2张图片

    综上分析,只要ACTION_DOWN事件到达RecyclerView,该事件必走onInterceptTouchEvent()方法,所以在该方法中可做一些信息准备工作。应在RecyclerView的onInterceptTouchEvent()方法和onTouchEvent()方法中都做水平滑动判断拦截,一旦拦截则后续事件不会再往下分发,后续事件一到该控件的dispatchTouchEvent()方法就会进入到onTouchEvent()方法。

  • 判断好RecyclerView是竖直滑动还是侧滑

    RecyclerView是水平滑动可根据多方面判断:

  1. 水平滑动距离大于竖直滑动距离,且大于系统最小滑动距离(根据事件中的坐标计算判断)
  2. 水平速度大于竖直滑动速度,且大于规定最小滑动速度(使用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

getFinalY()

返回滚动的Y轴结束位置.

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即为菜单栏。

    如有问题欢迎指出。

 

你可能感兴趣的:(Android,RecyclerView侧滑,菜单栏)