您现在的位置是:首页 >其他 >SwipeRecyclerView开源库源码分析之(一)触摸事件处理分析网站首页其他

SwipeRecyclerView开源库源码分析之(一)触摸事件处理分析

xiayuexingkong 2024-06-17 10:19:33
简介SwipeRecyclerView开源库源码分析之(一)触摸事件处理分析

1 仓库信息

https://github.com/yanzhenjie/SwipeRecyclerView

2 布局层级分析

定义了一个继承RecyclerView的子类SwipeRecyclerView。在该类中重写了onInterceptTouchEvent方法。其中ItemView为SwipeMenuLayout继承FrameLayout。itemView的布局如下:

<?xml version="1.0" encoding="utf-8"?>
<com.yanzhenjie.recyclerview.SwipeMenuLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:contentViewId="@+id/swipe_content"
    app:leftViewId="@+id/swipe_left"
    app:rightViewId="@+id/swipe_right">

    <com.yanzhenjie.recyclerview.SwipeMenuView
        android:id="@+id/swipe_left"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"/>

    <FrameLayout
        android:id="@+id/swipe_content"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

    <com.yanzhenjie.recyclerview.SwipeMenuView
        android:id="@+id/swipe_right"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"/>

</com.yanzhenjie.recyclerview.SwipeMenuLayout> 

AdapterWrapper的onCreateViewHolder中,将child添加到swipe_content view中。

    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View contentView = mHeaderViews.get(viewType);
        if (contentView != null) {
            return new ViewHolder(contentView);
        }

        contentView = mFootViews.get(viewType);
        if (contentView != null) {
            return new ViewHolder(contentView);
        }

        final RecyclerView.ViewHolder viewHolder = mAdapter.onCreateViewHolder(parent, viewType);
        ...
        contentView = mInflater.inflate(R.layout.x_recycler_view_item, parent, false);
        ViewGroup viewGroup = contentView.findViewById(R.id.swipe_content);
        viewGroup.addView(viewHolder.itemView);

        try {
            Field itemView = getSupperClass(viewHolder.getClass()).getDeclaredField("itemView");
            if (!itemView.isAccessible()) itemView.setAccessible(true);
            itemView.set(viewHolder, contentView);
        } catch (Exception ignored) {
        }
        return viewHolder;
    }

swipe_content view的child的布局如下:

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="?selectableItemBackground"
    android:gravity="center_vertical"
    android:minHeight="@dimen/dp_70"
    android:padding="@dimen/dp_10">

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        tools:text="Testing"/>

</LinearLayout>

3 事件处理

假设在列表中手指向左下方划动,划动的轨迹是一个理想的线段,且该线段与x轴的夹角< 45度,则线段上任意两点的横向位移dx > dy。通过ViewConfiguration分别得到引起滚动的最小偏移量touchSlop(假如是24px)、使View Fling的maxmumFlingVelocity(24000 pixels per second)和minimumFlingVelocity(150 pexels per second)。

SwipeRecyclerView.java
    public boolean onInterceptTouchEvent(MotionEvent e) {
        boolean isIntercepted = super.onInterceptTouchEvent(e);
        if (allowSwipeDelete || mSwipeMenuCreator == null) {
            return isIntercepted;
        } else {
            if (e.getPointerCount() > 1) return true;
            int action = e.getAction();
            // 1 获取MotinEvent对应的坐标(x, y)。该坐标是相对自身左上角顶点的坐标。
            int x = (int)e.getX();
            int y = (int)e.getY();
            // 2 根据坐标(x, y)确定对应的position、ViewHolder、ItemView。
            int touchPosition = getChildAdapterPosition(findChildViewUnder(x, y));
            ViewHolder touchVH = findViewHolderForAdapterPosition(touchPosition);
            SwipeMenuLayout touchView = null;
            if (touchVH != null) {
                View itemView = getSwipeMenuView(touchVH.itemView);
                if (itemView instanceof SwipeMenuLayout) {
                    touchView = (SwipeMenuLayout)itemView;
                }
            }

            boolean touchMenuEnable = mSwipeItemMenuEnable && !mDisableSwipeItemMenuList.contains(touchPosition);
            if (touchView != null) {
                touchView.setSwipeEnable(touchMenuEnable);
            }
            if (!touchMenuEnable) return isIntercepted;

            switch (action) {
                case MotionEvent.ACTION_DOWN: {
                    // 3 Down事件赋值第一个触点的坐标(mDownX, mDownY)。
                    mDownX = x;
                    mDownY = y;

                    isIntercepted = false;
                    // 4 如果上一次发生的触摸事件,且上一次的postion和这一次的position不相等;且上一次position的
                    // itemView打开了菜单,那么关闭这个菜单,然后清除上一次触摸事件的记录。且对应的方法的返回值为true
                    // 拦截该DOWN事件。
                    if (touchPosition != mOldTouchedPosition
                            && mOldSwipedLayout != null
                            && mOldSwipedLayout.isMenuOpen()) {
                        mOldSwipedLayout.smoothCloseMenu();
                        isIntercepted = true;
                    }

                    // 5 如果这次触摸事件是第一次,或者上一次触摸事件处理中没有打开菜单,那么记录这次触摸事件。
                    // 通过记录mOldTouchPosition和mOldSwipedLayout即可。并且方法的返回值为false,即不拦截Down
                    //事件 将事件传递给childView SwipeMenuLayout。
                    if (isIntercepted) {
                        mOldSwipedLayout = null;
                        mOldTouchedPosition = INVALID_POSITION;
                    } else if (touchView != null) {
                        mOldSwipedLayout = touchView;
                        mOldTouchedPosition = touchPosition;
                    }
                    break;
                }
                // They are sensitive to retain sliding and inertia.
                case MotionEvent.ACTION_MOVE: {
                    // 6 调用handleUnDown方法,参数isIntercepted为false。如果横向的位移和纵向的位移小于
                    // 可引起滚动的偏移量,则方法的返回值为false,在最初的Move事件横向和纵向的位移都是小于
                    //touchSlop的。
                    isIntercepted = handleUnDown(x, y, isIntercepted);
                    if (mOldSwipedLayout == null) break;
                    ViewParent viewParent = getParent();
                    if (viewParent == null) break;

                    // 10 记录这次MotionEvent相对上次MotionEvent的横向位移。如果横向发生了位移则
                    // 对ViewParent设置Disallow_intercept的Flag。申请parentView不要拦截以后的事件。
                    int disX = mDownX - x;
                    // 向左滑,显示右侧菜单,或者关闭左侧菜单。
                    boolean showRightCloseLeft = disX > 0 &&
                        (mOldSwipedLayout.hasRightMenu() || mOldSwipedLayout.isLeftCompleteOpen());
                    // 向右滑,显示左侧菜单,或者关闭右侧菜单。
                    boolean showLeftCloseRight = disX < 0 &&
                        (mOldSwipedLayout.hasLeftMenu() || mOldSwipedLayout.isRightCompleteOpen());
                    Log.d(TAG, "onInterceptTouchEvent: wyj showRightCloseLeft:" + showRightCloseLeft
                    + " showLeftCloseRight:" + showLeftCloseRight);
                    viewParent.requestDisallowInterceptTouchEvent(showRightCloseLeft || showLeftCloseRight);
                }
                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_CANCEL: {
                    isIntercepted = handleUnDown(x, y, isIntercepted);
                    break;
                }
            }
        }
        Log.d(TAG, "onInterceptTouchEvent: wyj isIntercepted:" + isIntercepted);
        return isIntercepted;
    }

    private boolean handleUnDown(int x, int y, boolean defaultValue) {
        Log.d(TAG, "handleUnDown: wyj defaultValue:" + defaultValue);
        // 7 计算这次MotionEvent相对上次MotionEvent的横向和纵向的位移。
        int disX = mDownX - x;
        int disY = mDownY - y;

        Log.d(TAG, "handleUnDown: wyj disX:" + disX + " disY:" + disY + " mScaleTouchSlop:" + mScaleTouchSlop);
        // 8 如果横向位移大于可引起滚动的偏移量,并且横向的位移大于纵向的位移,则返回false。
        // swipe
        if (Math.abs(disX) > mScaleTouchSlop && Math.abs(disX) > Math.abs(disY)) {
            Log.d(TAG, "handleUnDown: wyj > scaleTouchSlop and return false");
            return false;
        }
        // 9 如果纵向的位移小于可引起滚动的偏移量,并且横向的位移也小于可引起滚动的偏移量,则方法返回false。
        // click
        if (Math.abs(disY) < mScaleTouchSlop && Math.abs(disX) < mScaleTouchSlop) {
            Log.d(TAG, "handleUnDown: wyj < scaleTouchSlop and return false");
            return false;
        }
        Log.d(TAG, "handleUnDown: wyj return defaultValue:" + defaultValue);
        return defaultValue;
    }
SwipeMenuLayout.java
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean isIntercepted = super.onInterceptTouchEvent(ev);
        if (!isSwipeEnable()) {
            return isIntercepted;
        }

        int action = ev.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN: {
                // 1 Down事件记录第一个触点的坐标(mDownX, mDownY),方法的返回值为false,即不拦截
                // 将Down事件传递给childView(id为swipe_content的FrameLayout)。默认情况下
                // swipe_content的view是会处理Down事件。
                mDownX = mLastX = (int)ev.getX();
                mDownY = (int)ev.getY();
                Log.d(TAG, "onInterceptTouchEvent: wyj down result false.");
                return false;
            }
            case MotionEvent.ACTION_MOVE: {
                // 2 记录这次MotionEvent相对于上一次MotionEvent的横向和纵向的位移。
                // 如果横向的位移大于可引起滚动的偏移量touchSlop,并且横向的位移大会纵向的位移,则方法的返回值为
                // true,表明拦截该Move事件,但是由于mFirstTouchTarget != null,仍会调用
                // childView(swipe_content)的dispatchTouchEvent方法,但是会将mFirstTouchTarget置为null。
                // 由于parentView SwipeRecyclerView的mFirstTouchTarget != null,则下一个Move事件仍会传到
                // SwipeMenuLayout中,由于mFirstTouchTarget = null,则会调用onTouchEvent方法。
                int disX = (int)(ev.getX() - mDownX);
                int disY = (int)(ev.getY() - mDownY);
                Log.d(TAG, "onInterceptTouchEvent: wyj disX:" + disX + " disY:" + disY
                 + " mScaledTouchSlop:" + mScaledTouchSlop);
                boolean result = Math.abs(disX) > mScaledTouchSlop && Math.abs(disX) > Math.abs(disY);
                Log.d(TAG, "onInterceptTouchEvent: wyj move result:" + result);
                return result;
            }
            case MotionEvent.ACTION_UP: {
                boolean isClick = mSwipeCurrentHorizontal != null &&
                    mSwipeCurrentHorizontal.isClickOnContentView(getWidth(), ev.getX());
                if (isMenuOpen() && isClick) {
                    smoothCloseMenu();
                    Log.d(TAG, "onInterceptTouchEvent: wyj up result true");
                    return true;
                }
                Log.d(TAG, "onInterceptTouchEvent: wyj up result false");
                return false;
            }
            case MotionEvent.ACTION_CANCEL: {
                if (!mScroller.isFinished()) mScroller.abortAnimation();
                Log.d(TAG, "onInterceptTouchEvent: wyj cancel result false");
                return false;
            }
        }
        return isIntercepted;
    }

   public boolean onTouchEvent(MotionEvent ev) {
        if (!isSwipeEnable()) {
            return super.onTouchEvent(ev);
        }

        if (mVelocityTracker == null) mVelocityTracker = VelocityTracker.obtain();
        mVelocityTracker.addMovement(ev);
        int dx;
        int dy;
        int action = ev.getAction();
        Log.d(TAG, "onTouchEvent: wyj ev.getAction:" + action);
        switch (action) {
            case MotionEvent.ACTION_DOWN: {
                mLastX = (int)ev.getX();
                mLastY = (int)ev.getY();
                Log.d(TAG, "onTouchEvent: wyj down");
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                // 3 这次MotionEvent事件onTouchEvent方法被调用说明了上次MotionEvent Move事件
                // 被SwipeMenuLayout拦截且mFirstTouchTarget == null,
                // 如果mDragging为false,即没有横向滚动(调用scrollBy方法)则mLastX是down事件对应的触点
                // 坐标x,mLastY 为0。如果mDagging为true,即之前发生了滚动(调用了scrollBy)则
                // mLastX、mLstY为上次MotionEvent的x、y坐标。
                // 随着 dix 的值越来越大直到超过纵向的位移,并且超过了touchSlop则视为发起滚动的信号,
                // mDragging记为true,且调用scollBy(dix, 0)。mLastX,mLastY重新赋值。调用scrollBy
                // 对调用scollTo方法和computeScoll方法,会慢慢拉开隐藏的菜单,如果dx > 0 会拉开右边的菜单。
                // 接下来的的move事件也是这样的处理,会使得scrollX越来越大。
                int disX = (int)(mLastX - ev.getX());
                int disY = (int)(mLastY - ev.getY());
                Log.d(TAG, "onTouchEvent: wyj move");
                Log.d(TAG, "onTouchEvent: wyj disX:" + disX + " disY:" + disY + " mScaledTouchSlop:" + mScaledTouchSlop);
                if (!mDragging && Math.abs(disX) > mScaledTouchSlop && Math.abs(disX) > Math.abs(disY)) {
                    mDragging = true;
                }
                if (mDragging) {
                    if (mSwipeCurrentHorizontal == null || shouldResetSwipe) {
                        if (disX < 0) {
                            if (mSwipeLeftHorizontal != null) {
                                mSwipeCurrentHorizontal = mSwipeLeftHorizontal;
                            } else {
                                mSwipeCurrentHorizontal = mSwipeRightHorizontal;
                            }
                        } else {
                            if (mSwipeRightHorizontal != null) {
                                mSwipeCurrentHorizontal = mSwipeRightHorizontal;
                            } else {
                                mSwipeCurrentHorizontal = mSwipeLeftHorizontal;
                            }
                        }
                    }
                    Log.d(TAG, "onTouchEvent: wyj scrollBy");
                    scrollBy(disX, 0);
                    mLastX = (int)ev.getX();
                    mLastY = (int)ev.getY();
                    shouldResetSwipe = false;
                }

                break;
            }
            case MotionEvent.ACTION_UP: {
                // 4 松手,通过VelocityTracker 获取当前横向的速度,假如是左滑,那么是< 0 的,
                // 如果速率 > minimumFlingVelocity,则完全打开菜单。通过OverScroller#startScroll打开。
                // 调用scroll执行动画,会调用computeScoll方法。
                
                dx = (int)(mDownX - ev.getX());
                dy = (int)(mDownY - ev.getY());
                mDragging = false;
                mVelocityTracker.computeCurrentVelocity(1000, mScaledMaximumFlingVelocity);
                int velocityX = (int)mVelocityTracker.getXVelocity();
                int velocity = Math.abs(velocityX);
                if (velocity > mScaledMinimumFlingVelocity) {
                    if (mSwipeCurrentHorizontal != null) {
                        int duration = getSwipeDuration(ev, velocity);
                        if (mSwipeCurrentHorizontal instanceof RightHorizontal) {
                            if (velocityX < 0) {
                                // 5 velocityX < 0 ,左划,则打开菜单;否则右划,关闭菜单。
                                //
                                smoothOpenMenu(duration);
                            } else {
                                smoothCloseMenu(duration);
                            }
                        } else {
                            if (velocityX > 0) {
                                smoothOpenMenu(duration);
                            } else {
                                smoothCloseMenu(duration);
                            }
                        }
                        ViewCompat.postInvalidateOnAnimation(this);
                    }
                } else {
                    judgeOpenClose(dx, dy);
                }
                mVelocityTracker.clear();
                mVelocityTracker.recycle();
                mVelocityTracker = null;
                if (Math.abs(mDownX - ev.getX()) > mScaledTouchSlop ||
                    Math.abs(mDownY - ev.getY()) > mScaledTouchSlop || isLeftMenuOpen() || isRightMenuOpen()) {
                    ev.setAction(MotionEvent.ACTION_CANCEL);
                    super.onTouchEvent(ev);
                    return true;
                }
                Log.d(TAG, "onTouchEvent: wyj up");
                break;
            }
            case MotionEvent.ACTION_CANCEL: {
                mDragging = false;
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                } else {
                    dx = (int)(mDownX - ev.getX());
                    dy = (int)(mDownY - ev.getY());
                    judgeOpenClose(dx, dy);
                }
                Log.d(TAG, "onTouchEvent: wyj cancel");
                break;
            }
        }
        boolean result = super.onTouchEvent(ev);
        Log.d(TAG, "onTouchEvent: wyj result:" + result);
        return result;
    }

    @Override
    public void scrollTo(int x, int y) {
        Log.d(TAG, "scrollTo: wyj x:" + x + " y:" + y);
        if (mSwipeCurrentHorizontal == null) {
            super.scrollTo(x, y);
        } else {
            Horizontal.Checker checker = mSwipeCurrentHorizontal.checkXY(x, y);
            shouldResetSwipe = checker.shouldResetSwipe;
            if (checker.x != getScrollX()) {
                super.scrollTo(checker.x, checker.y);
            }
        }
    }

    public void computeScroll() {
        // 6 判断是否执行了scroll的动画。在调用OverScroll#startScroll方法的时候,且动画未完成,则返回
        // false。
        boolean isOffset = mScroller.computeScrollOffset();
        int currX = mScroller.getCurrX();
        Log.d(TAG, "computeScroll: wyj isOffset:" + isOffset + " currX:" + currX);
        if (isOffset && mSwipeCurrentHorizontal != null) {
            if (mSwipeCurrentHorizontal instanceof RightHorizontal) {
                scrollTo(Math.abs(mScroller.getCurrX()), 0);
                invalidate();
            } else {
                scrollTo(-Math.abs(mScroller.getCurrX()), 0);
                invalidate();
            }
        }
    }

总结:

​ SwipeRecyclerView重写onInterceptTouchEvent方法,SwipeMenuLayout重写onInterceptTouchEvent、onTouchEvent、scrollTo、computeScroll方法。

(1) SwipeRecyclerView不拦截Down事件,将Down事件传递给SwipeMenuLayout,记录第一个触点的坐标(mDownX, mDownY)。

(2) SwipeMenuLayout不拦截Down事件,记录第一个触点的坐标(mDownX, mDownY)。并初始化mLastX = mDownX。

(3) SwipeRecyclerView处理Move事件;计算这次触点和第一个触点的位移,如果有横向的位移,则获取parentView并设置disallow_intercept_flag;如果横向的位移dx < touchSlop && dy < touchSlop,则将事件传递给SwipeMenuLaout;如果dx > touchSlop && dx > dy,将事件传递给SwipeMenuLayout;否则onInterceptTouchEvent方法的返回值取super.onInterceptTouchEvent方法的返回值。

(4) SwipeMenuLayout的onInterceptTouchEvent处理Move事件:计算这次触点和第一触点的横向和纵向的位移,如果dx > touchSlop && dx > dy,则方法的返回值为true,拦截这个事件;否则方法的返回值取super.onInterceptTouchEvent返回值。

(5) 如果上一次Move事件被SwipeMenuLayout拦截,则接下来的Move事件不会传递给child view,SwipeMenuLayout的onTouchEvent方法被调用。

(6) SwipeMenuLayout的onTouchEvent方法处理Move事件。将mLastX - ev.getX记为dx,将mLastY - ev.getY记为dy,由于mLastX = mDownX,mLastY第一次为0,则有可能dx的绝对值小于dy的绝对值,只有当dx的绝对值 > touchSlop且,dx的绝对值大于dy,才判定为发起拖动,意图打开菜单。为什么不在onInterceptTouchEvent的处理Down事件逻辑中不设置mLastY = mDownY,推断是这里不希望将打开菜单做的敏感,希望当划动轨迹与x轴的夹角比较小,或者划动轨迹的线段足够长才触发打开菜单,这里是体验上的优化。如果满足这两个场景,则判定为发起拖动意图打开菜单。字段mDraging = true,重置mLastX,mLastY,并调用scrollBy(dx, 0)。

(7) View的scrollBy,如果方法的dx > 0,则将View中的内容向左划,否则向右划。调用scrollBy之后,会调用scrollTo,computeScroll方法。

(8) 如果发起了滚动,在接下来的Move事件中,也会调用scrollBy,导致getScrollX越来越大,其绝对值逐渐接近menuView的宽度。

(9)SwipeMenuLayout的onTouchEvent方法处理UP事件。根据VelocityTracker获取当前横向的速度,如果左划,则velocityX < 0,如果右划,则velocity > 0,取横向速率和minimunFlingVelocity比较,如果横向速率大于minumumFlingVelocity,则打开菜单,通过OverScroller#startScoll执行滚动动画打开菜单。否则调用judgeOpenClose。

(10) SwipeMenuLayout处理Up事件的judgeOpenClose方法。如果当前横向滚动的偏移量scollX的绝对值 < 菜单宽度的1/2,则隐藏菜单,通过OverScoller#startScroll隐藏菜单。如果当前横向滚动的偏移量不小于菜单宽度的1/2,判断如果菜单时开的状态则关,如果菜单时关的状态则开。

(11) OverScroller#startScroll方法执行滚动动画,会触发View的computeScoll方法。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。