Your likes and attention are the biggest motivation for me to keep writing. I am looking for a job opportunity in test development. Please contact me on wechat: GYx764884989

preface

Recently in the solution to the problem of RecyclerView sliding conflict, encountered the use of OnItemTouchLister can not solve the problem of the scene, this article will combine the actual case, focus on the following problems:

  1. RecyclerViewBrief analysis of event distribution execution process
  2. addOnItemTouchListenerWhy can’t it solve the problem?
  3. The final solution for this scenario

The business requirements

In a video call interface, place a speaker list, which supports horizontal sliding, called the small window list, and the window in the background is called the big window. When the user wants to switch an item in the small window list to the big window, he can touch the item he wants to switch with his finger and slide upward. The selected small window can be switched to the large window position, and the upward slide needs to support the vertical up and oblique up direction.

Original solution

The solution

The original solution was to set the OnTouchListener method for the Item View to determine whether dy (Y offset) is greater than a certain threshold in the ACTION_MOVE event in its onTouch() method.

Problems encountered

The problem is that when the item slides up, the DY of the ACTION_MOVE event that the item view receives is always very small, even if you’re sure you’ve slid a lot

Problem location & doubt

  • The problem is positioned as the nested sliding conflict between RecyclerView and Item occurs in horizontal sliding
  • It is suspected that RecyclerView consumed part of sliding events, resulting in the sliding distance received by item View being very small.

Try new solutions

By browsing the source code found that RecyclerView internal provides OnItemTouchListener, introduced as follows:

    /**
     * An OnItemTouchListener allows the application to intercept touch events in progress at the
     * view hierarchy level of the RecyclerView before those touch events are considered for
     * RecyclerView's own scrolling behavior.
     *
     * <p>This can be useful for applications that wish to implement various forms of gestural
     * manipulation of item views within the RecyclerView. OnItemTouchListeners may intercept
     * a touch interaction already in progress even if the RecyclerView is already handling that
     * gesture stream itself for the purposes of scrolling.</p>
     *
     * @see SimpleOnItemTouchListener
     */
    public static interface OnItemTouchListener{
        /**
         * Silently observe and/or take over touch events sent to the RecyclerView
         * before they are handled by either the RecyclerView itself or its child views.
         *
         * <p>The onInterceptTouchEvent methods of each attached OnItemTouchListener will be run
         * in the order in which each listener was added, before any other touch processing
         * by the RecyclerView itself or child views occurs.</p>
         *
         * @param e MotionEvent describing the touch event. All coordinates are in
         *          the RecyclerView's coordinate system.
         * @return true if this OnItemTouchListener wishes to begin intercepting touch events, false
         *         to continue with the current behavior and continue observing future events in
         *         the gesture.
         */
        public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e); . }Copy the code

OnItemTouchListener has two main functions:

  1. Give developers the right to customize event distribution algorithms before RecyclerView consumes events.
  2. When RecyclerView has been in the event consumption process, you can use this class to intercept the event sequence that RecylerView is processing.

Add OnItemTouchListener to RecyclerView and change it into an onInterceptTouchEvent(RecyclerView RV, MotionEvent E). Return false in onInterceptTouchEvent() if the Y offset is greater than a certain threshold that indicates the current user wants to trigger a window replacement. We expect RecyclerView to consume no events at all. Make the event sink into item View of RecyclerView, then item can normally get MOVE event, part of the code is as follows:

    
    /** * The ordinate offset threshold */
    private final int Y_AXIS_MOVE_THRESHOLD = 15;
    private int downY = 0;

    @Override
    public boolean onInterceptTouchEvent(MotionEvent e) {

        if (e.getAction() == MotionEvent.ACTION_DOWN) {
            downY = (int) e.getRawY();

        } else if (e.getAction() == MotionEvent.ACTION_MOVE) {
            int realtimeY = (int) e.getRawY();
            int dy = Math.abs(downY - realtimeY);

            if (dy > Y_AXIS_MOVE_THRESHOLD) {
                return false; }}return true;
    }
Copy the code

But in fact, it is impossible to realize the requirements in this way, because according to our current implementation scheme, RecyclerView is expected to completely release the MOVE event and sink the event into item View to process when DY is greater than the threshold value. According to the event distribution rules, The onInterceptTouchEvent() of RecyclerView returns false, and the onTouchEvent() of the item View will be called. And then realize the window replacement, we come to the source code analysis why this scheme can not be achieved.

RecyclerView event distribution code analysis

    @Override
    public boolean onInterceptTouchEvent(MotionEvent e) {
        if (mLayoutFrozen) {
            // When layout is frozen, RV does not intercept the motion event.
            // A child view e.g. a button may still get the click.
            return false;
        }
        if (dispatchOnItemTouchIntercept(e)) {
            cancelTouch();
            return true;
        }

        if (mLayout == null) {
            return false;
        }

        final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
        final boolean canScrollVertically = mLayout.canScrollVertically();

        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(e);

        final int action = MotionEventCompat.getActionMasked(e);
        final int actionIndex = MotionEventCompat.getActionIndex(e);

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                if (mIgnoreMotionEventTillDown) {
                    mIgnoreMotionEventTillDown = false;
                }
                mScrollPointerId = e.getPointerId(0);
                mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5 f);
                mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5 f);

                if (mScrollState == SCROLL_STATE_SETTLING) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                    setScrollState(SCROLL_STATE_DRAGGING);
                }

                // Clear the nested offsets
                mNestedOffsets[0] = mNestedOffsets[1] = 0;

                int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
                if (canScrollHorizontally) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
                }
                if (canScrollVertically) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
                }
                startNestedScroll(nestedScrollAxis);
                break;

            case MotionEventCompat.ACTION_POINTER_DOWN:
                mScrollPointerId = e.getPointerId(actionIndex);
                mInitialTouchX = mLastTouchX = (int) (e.getX(actionIndex) + 0.5 f);
                mInitialTouchY = mLastTouchY = (int) (e.getY(actionIndex) + 0.5 f);
                break;

            case MotionEvent.ACTION_MOVE: {
                final int index = e.findPointerIndex(mScrollPointerId);
                if (index < 0) {
                    Log.e(TAG, "Error processing scroll; pointer index for id " +
                            mScrollPointerId + " not found. Did any MotionEvents get skipped?");
                    return false;
                }

                final int x = (int) (e.getX(index) + 0.5 f);
                final int y = (int) (e.getY(index) + 0.5 f);
                if(mScrollState ! = SCROLL_STATE_DRAGGING) {final int dx = x - mInitialTouchX;
                    final int dy = y - mInitialTouchY;
                    boolean startScroll = false;
                    if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
                        mLastTouchX = mInitialTouchX + mTouchSlop * (dx < 0 ? -1 : 1);
                        startScroll = true;
                    }
                    if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
                        mLastTouchY = mInitialTouchY + mTouchSlop * (dy < 0 ? -1 : 1);
                        startScroll = true;
                    }
                    if(startScroll) { setScrollState(SCROLL_STATE_DRAGGING); }}}break;

            case MotionEventCompat.ACTION_POINTER_UP: {
                onPointerUp(e);
            } break;

            case MotionEvent.ACTION_UP: {
                mVelocityTracker.clear();
                stopNestedScroll();
            } break;

            caseMotionEvent.ACTION_CANCEL: { cancelTouch(); }}return mScrollState == SCROLL_STATE_DRAGGING;
    }
Copy the code

Analysis of the

  1. mLayoutFrozenUsed to identify whether the RecyclerView disabled layout process and scroll ability, RecyclerView provides a way to set itsetLayoutFrozen(boolean frozen)If mLayoutFrozen is marked true, RecyclreView can change as follows:
  • All Layout requests to RecyclerView will be deferred until mLayoutFrozen is set to false again
  • The child View will not be refreshed
  • RecyclerView also won’t respond to sliding requests, that is, it won’t respondsmoothScrollBy(int, int).scrollBy(int, int).scrollToPosition(int).smoothScrollToPosition(int)
  • Do not respond to Touch Events and GenericMotionEvents
  1. If RecyclerView is setOnItemTouchListener, then call before RecyclerView itself slidesdispatchOnItemTouchIntercept(MotionEvent e)For distribution, the code is as follows:
    private boolean dispatchOnItemTouchIntercept(MotionEvent e) {
        final int action = e.getAction();
        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_DOWN) {
            mActiveOnItemTouchListener = null;
        }

        final int listenerCount = mOnItemTouchListeners.size();
        for (int i = 0; i < listenerCount; i++) {
            final OnItemTouchListener listener = mOnItemTouchListeners.get(i);
            if (listener.onInterceptTouchEvent(this, e) && action ! = MotionEvent.ACTION_CANCEL) { mActiveOnItemTouchListener = listener;return true; }}return false;
    }
Copy the code

A. mActiveOnItemTouchListener is OnItemTouchListener type of object, if received ACTION_CANCEL or ACTION_DOWN events, Set the callback to NULL to remove the effect of the previous sequence of events on the current sequence. When will we receive the ACTION_CANCEL event? If the parent View returns true from onInterceptTouchEvent(), the child View will receive the ACTION_CANCEL event when it is consuming ACTION_MOVE events. And the ACTION_CANCEL event cannot be intercepted by the parent View.

B. Iterate over all registered OnItemTouchListener. If the current event is not ACTION_CANCEL, Call OnItemTouchListener onInterceptTouchEvent() and return true, indicating that RecyclerView intercepts the event sequence according to the event distribution rules. Events are distributed to RecyclerView’s onTouchEvent(). If the sliding condition is met, RecyclerView will consume it to make itself slide.

addOnItemTouchListenerWhy can’t it solve the problem?

So that gives us the answer, why does the scheme in OnItemTouchListener fail or fail,

  1. iflistener.onInterceptTouchEvent(this, e)Return true, then RecyclerViewonInterceptTouchEvent()Will return true, the event changed to RecyclerViewonTouchEvent()By consumption.
  2. iflistener.onInterceptTouchEvent(this, e)Return false, then RecyclerView will continue to process this group of MOVE events, and eventually events will change to RecyclerViewonTouchEvent()By consumption.

Final solution

The end result is the same as onInterceptTouchEvent using OnItemTouchListener. The difference is that this time we create a RecyclerView subclass. Rewrite RecyclerView onInterceptTouchEvent, the specific code is as follows:


/** * Custom RecyclerView, block its horizontal movement in some scenarios */
public class InterceptHScrollRecyclerView extends RecyclerView {
    private final String TAG = InterceptHScrollRecyclerView.class.getSimpleName();
    /** * The ordinate offset threshold exceeds this */
    private final int Y_AXIS_MOVE_THRESHOLD = 15;

    public InterceptHScrollRecyclerView(Context context) {
        super(context);
    }

    public InterceptHScrollRecyclerView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public InterceptHScrollRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    int downY = 0;

    @Override
    public boolean onInterceptTouchEvent(MotionEvent e) {


        if (e.getAction() == MotionEvent.ACTION_DOWN) {
            downY = (int) e.getRawY();

        } else if (e.getAction() == MotionEvent.ACTION_MOVE) {
            int realtimeY = (int) e.getRawY();
            int dy = Math.abs(downY - realtimeY);


            if (dy > Y_AXIS_MOVE_THRESHOLD) {
                return false; }}return super.onInterceptTouchEvent(e); }}Copy the code

The reason why this solution works is because if you use inheritance, this code is like inserting a code before RecyclerView executes the event distribution process, which has a bit of AOP feel. If you return false, you can completely avoid RecyclerView taking over the event. To do that, notice this last line of code,

    return super.onInterceptTouchEvent(e);
Copy the code

You cannot return true directly, because if you do not intercept, the return value is an internal RecyclerView option.


Persistence is not easy, your praise is the biggest power of my writing!