RecyclerView event distribution principle, practical analysis, Android interview questions

Keywords: Android Design Pattern Interview

Recently, when solving the problem of RecyclerView sliding conflict, we encountered a scenario where the problem cannot be solved by using OnItemTouchLister. This article will focus on the following problems in combination with actual cases:

  1. Brief analysis of RecyclerView event distribution execution process

  2. Why can't adding OnItemTouchListener solve the problem?

  3. The final solution in this scenario

Business requirements

In a video call interface, place a list of speakers. This list supports horizontal sliding, which is called the small window list. 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 and slide upward to switch the selected small window to the big window, Moreover, upward sliding needs to support vertical upward and oblique upward directions.

Original solution

Solution

The original solution was to set the OnTouchListener method for item view and the action in its onTouch() method_ Judge whether dy (Y coordinate offset) is greater than a threshold in the move event.

Problems encountered

The problem encountered is the action received by the item view when the item slides obliquely upward_ The dy of the move event is always very small, even when you are sure you have slid a lot

Problem orientation & doubt

  • The problem is that there is a nested sliding conflict between RecyclerView and item when sliding horizontally

  • It is suspected that RecyclerView consumed some sliding events, resulting in the small sliding distance received by item view.

Try new solutions

Through browsing the source code, it is found that the RecyclerView internally provides OnItemTouchListener, which is described 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);



        ...

    } 

OnItemTouchListener has two main functions:

  1. Before RecyclerView consumes events, developers are given the right to customize the event distribution algorithm.

  2. When the RecyclerView is already consuming events, this class can intercept the event sequence being processed by the RecyclerView.

It seems that the problem mentioned in this article can be solved. The idea is to add OnItemTouchListener to RecyclerView. When it calls oninterceptotouchevent (RecyclerView RV, motionevent E), judge that if the offset of Y axis is greater than a certain threshold, indicating that the current user wants to trigger window replacement, then return false in oninterceptotouchevent(), We expect that RecyclerView will not consume events at all, so that the events sink into the item view of RecyclerView, and the item can normally obtain the MOVE event. Some codes are as follows:

 /**

     * 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;

    } 

However, in fact, this cannot meet the requirements, because according to our current implementation scheme, it is expected that when dy is greater than the threshold, RecyclerView can completely let go of the MOVE event and sink the event into the item view for processing. According to the event distribution rules, the onintercepttouchevent() return of RecyclerView is required to be false, Then onTouchEvent() of the sub View, item view, will be called. To realize window replacement, let's analyze why this scheme can't be realized through the source code.

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.5f);

                mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);



                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.5f);

                mInitialTouchY = mLastTouchY = (int) (e.getY(actionIndex) + 0.5f);

                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.5f);

                final int y = (int) (e.getY(index) + 0.5f);

                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;



            case MotionEvent.ACTION_CANCEL: {

                cancelTouch();

            }

        }

        return mScrollState == SCROLL_STATE_DRAGGING;

    } 

analysis

  1. mLayoutFrozen is used to identify whether the RecyclerView disables the layout process and the scroll capability. RecyclerView provides the method setLayoutFrozen(boolean frozen) to set it. If mLayoutFrozen is identified as true, the recyclreview will change as follows:
  • All Layout requests to RecyclerView will be delayed until mLayoutFrozen is set to false again

  • The child View will not be refreshed

  • RecyclerView will not respond to the request for sliding, that is, it will not respond to smoothScrollBy(int, int), scrollBy(int, int), scrollToPosition(int), smoothScrollToPosition(int)

  • Do not respond to touch events and GenericMotionEvents

  1. If RecyclerView sets OnItemTouchListener, it will call dispatchOnItemTouchIntercept(MotionEvent e) to distribute before RecyclerView slid itself. 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;
#### **[attachment] relevant structure and data**

![](https://img-blog.csdnimg.cn/img_convert/121e07c8cc1aa5f2cd14e430e6bf5c77.png)

![](https://img-blog.csdnimg.cn/img_convert/706e390728dec4d2b1049d57dc3d1de8.png)

##### **[CodeChina open source project: Android learning notes summary + mobile architecture Video + big factory interview real questions + project actual combat source code]( https://codechina.csdn.net/m0_60958482/android_p7)**

ispatchOnItemTouchIntercept(MotionEvent e) {

        final int action = e.getAction();

        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_DOWN) {

            mActiveOnItemTouchListener = null;
#### **[attachment] relevant structure and data**

[External chain picture transfer...(img-E2N3La7J-1630584189557)]

[External chain picture transfer...(img-4fYqpe7f-1630584189558)]

##### **[CodeChina open source project: Android learning notes summary + mobile architecture Video + big factory interview real questions + project actual combat source code]( https://codechina.csdn.net/m0_60958482/android_p7)**

> **Previous period Android Advanced architecture information, source code, notes, videos. senior UI,Performance optimization, architect courses NDK,Hybrid development( ReactNative+Weex)Wechat applet Flutter Comprehensive Android Advanced practice technology, and there are technical cattle in the group to discuss, communicate and solve problems.**

Posted by jeaker on Thu, 02 Sep 2021 21:29:51 -0700