Advanced UI growth path, in-depth understanding of Android 8.0 View touch event distribution mechanism

Keywords: Android Programmer view

preface

In the previous article, we introduced the basic knowledge of View and the implementation of View sliding. This article will bring you a core knowledge point of View, event distribution mechanism. Event distribution mechanism is not only a core knowledge point, but also a difficulty in Android. Let's analyze the transmission of events from the perspective of source code and finally how to solve sliding conflicts.

Event distribution mechanism

Delivery rules for click events

Before introducing the event delivery rules, we should first understand that the object to be analyzed is MotionEvent. We used MotionEvent when we introduced sliding in the last article. In fact, the so-called click event distribution is the distribution process of MotionEvent events. The distribution process of click events is completed by three very important methods, as follows:

1. dispatchTouchEvent(MotionEvent ev)

Used for event distribution. If the event can be passed to the current View, this method must be called. The returned result is affected by the onTouchEvent method of the current View and the dispatchTouchEvent method of the subordinate View, indicating whether to consume the current event.

2. onInterceptTouchEvent(MotionEvent ev)

The above internal method call is used to determine whether to intercept an event. If the current View intercepts an event, this method will not be called again in the same event sequence, and the return result indicates whether to intercept the current event.

3. onTouchEvent(MotionEvent ev)

In the first method, the call is used to handle clicking events and return results to indicate whether to consume the event. If it is not consumed, the current View will not be able to receive the event again.

Next, I draw a diagram to illustrate the relationship between the above three methods

It can also be explained by a pseudo code, as follows:

fun dispatchTouchEvent(MotionEvent ev):Boolean{
  var consume = false
  //Whether the parent class intercepts
  if(onInterceptTouchEvent(ev)){
    //If intercepted, it will execute its own onTouchEvent method
    consume = onTouchEvent(ev)
  }else{
    //If the event is not intercepted in the parent class, it will continue to be distributed to the child class
    consume = child.dispatchTouchEvent(ev)
  }
  reture consume
}

The above figure has the same meaning as the pseudo code, especially the pseudo code has shown the relationship between them in place. Through the pseudo code above, we can roughly understand a transmission rule of click events. Corresponding to a root ViewGroup, after a click event is generated, it will be transmitted to it first, and then its dispatchTouchEvent will be called, If the onInterceptTouchEvent method of the ViewGroup returns true, it means that it wants to intercept the current event, and then the event will be handed over to the ViewGroup for processing, that is, its onTouchEvent method will be called; If the onInterceptTouchEvent of the ViewGroup returns false, it means that it does not intercept the current event. At this time, the current event will be passed to its child elements, and then the dispatchTouchEvent method of the child elements will be called. This will be repeated until the event is finally processed.

The calling rules when a View needs to process events are as follows:

fun  dispatchTouchEvent(MotionEvent event): boolean{
  //1. If the current View is set to onTouchListener
if(onTouchListener != null){
  //2. Then its own onTouch will be called. If false is returned, its own onTouchEvent will be called
  if(!onTouchListener.onTouch(v: View?, event: MotionEvent?)){
       //3. onTouch returns false, onTouchEvent is called, and the internal onClick event will be called
    if(!onTouchEvent(event)){
        //4. If onClickListener is also set, onClick will also be called
       onClickListener.onClick()
    }
  }
 }
}

The logic summary of the above pseudo code is that if the current View has onTouchListener set, its own onTouch will be executed. If the return value of onTouch is false, its own onTouchEvent will be called. If onTouchEvent returns false, onClick will execute. The priority is onTouch > onTouchEvent > onClick.

When a click event is generated, its delivery process follows the following sequence: Activity - > Window - > View, that is, the event is always delivered to the Activity first, then to the Window, and finally to the top-level View. After receiving the event, the top-level View will distribute the event according to the event distribution mechanism. Consider a case where a View's onTouchEvent returns false, then its parent container's onTouchEvent will be called, and so on. If all elements do not handle this event, the event will be finally passed to the Activity for processing, that is, the onTouchEvent method of the Activity will be called. Let's use a code example to demonstrate this scenario. The code is as follows:

  1. Override Activity dispatchTouchEvent distribution and onTouchEvent event handling

    class MainActivity : AppCompatActivity() { 
            override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
          if (ev.action == MotionEvent.ACTION_DOWN)
            println("The event distribution mechanism starts distribution ----> Activity  dispatchTouchEvent")
            return super.dispatchTouchEvent(ev)
        }
    
    
        override fun onTouchEvent(event: MotionEvent?): Boolean {
          if (event.action == MotionEvent.ACTION_DOWN)
            println("Event distribution mechanism processing ----> Activity onTouchEvent implement")
            return super.onTouchEvent(event)
        }
    }  
  2. Override root ViewGroup dispatchTouchEvent distribution and onTouchEvent event handling

    public class GustomLIn(context: Context?, attrs: AttributeSet?) : LinearLayout(context, attrs) {
    
        override fun onTouchEvent(event: MotionEvent?): Boolean {
           if (event.action == MotionEvent.ACTION_DOWN)
            println("Event distribution mechanism processing ----> Parent container LinearLayout onTouchEvent")
            return false
        }
    
        override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
           if (ev.action == MotionEvent.ACTION_DOWN)
            println("The event distribution mechanism starts distribution ----> Parent container  dispatchTouchEvent")
            return super.dispatchTouchEvent(ev)
        }
    
        override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
          if (ev.action == MotionEvent.ACTION_DOWN)
            println("The event distribution mechanism starts distribution ----> Whether the parent container intercepts  onInterceptTouchEvent")
            return super.onInterceptTouchEvent(ev)
        }
    }    
  3. Overriding child View dispatchTouchEvent distribution and onTouchEvent event handling

    public class Button(context: Context?, attrs: AttributeSet?) : AppCompatButton(context, attrs) {
        override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
          if (event.action == MotionEvent.ACTION_DOWN)
            println("The event distribution mechanism starts distribution ----> son View  dispatchTouchEvent")
            return super.dispatchTouchEvent(event)
        }
    
        override fun onTouchEvent(event: MotionEvent?): Boolean {
          if (event.action == MotionEvent.ACTION_DOWN)
            println("Event distribution mechanism processing ----> son View onTouchEvent")
            return false
        }
    
    } 

Output:

System.out: The event distribution mechanism starts distribution ----> Activity       dispatchTouchEvent
System.out: The event distribution mechanism starts distribution ----> Parent container          dispatchTouchEvent
System.out: The event distribution mechanism starts distribution ----> Whether the parent container intercepts    onInterceptTouchEvent
System.out: The event distribution mechanism starts distribution ----> son View          dispatchTouchEvent
System.out: The event distribution mechanism starts processing ----> son View         onTouchEvent
System.out: The event distribution mechanism starts processing ----> Parent container         LinearLayout onTouchEvent
System.out: The event distribution mechanism starts processing ----> Activity         onTouchEvent implement

The conclusion is completely consistent with the previous description, which means that if the child View and parent ViewGroup do not handle events, they are finally handed over to the onTouchEvent method of the Activity. It can also be seen from the above results that event delivery is from the outside to the inside, that is, events are always delivered to the parent element first, and then distributed by the parent element to the child View.

Event distribution source code analysis

In the previous section, we analyzed the event distribution mechanism of View. This section will further analyze it from the perspective of source code.

  1. Distribution process of click events by Activity

    Click events are represented by MotionEvent. When a click operation occurs, the event is first transmitted to the current Activity, and the event is distributed by the Activity's dispatchTouchEvent. The specific work is completed by the window inside the Activity. Window will pass events to decorview. Decorview is generally the underlying container of the current interface, that is, the parent container set by setContentView. It inherits from FrameLayout and can be obtained in Activity through getWindow().getDecorView(). Since events are distributed first by Activity, we will directly look at its dispatchTouchEvent method, The code is as follows:

    //Activity.java
        public boolean dispatchTouchEvent(MotionEvent ev) {
            /**
             * The first press triggers ACTION_DOWN event
             */
            if (ev.getAction() == MotionEvent.ACTION_DOWN) {
                onUserInteraction();
            }
            /**
             * Get the current Window and call the superDispatchTouchEvent method
             */
            if (getWindow().superDispatchTouchEvent(ev)) {
                return true;
            }
            /**
             * If all views are not processed, they will eventually be executed into the Activity onTouchEvent method.
             */
            return onTouchEvent(ev);
        }

    Through the above code, we know that the first execution is ACTION\_DOWN presses the event to execute the onUserInteraction empty method, and then calls the getWindow() superDispatchTouchEvent method. The getWindow here is actually its only subclass PhoneWindow. We see its specific call implementation. The code is as follows:

    //PhoneWindow.java
    public class PhoneWindow extends Window implements MenuBuilder.Callback {
      ...
            private DecorView mDecor;    
        @Override
        public boolean superDispatchTouchEvent(MotionEvent event) {
            return mDecor.superDispatchTouchEvent(event);
        }
      ...
    }

    In the superDispatchTouchEvent function of PhoneWindow, it is handed over to DecorView for processing. What is DecorView?

    //DecorView.java
    
    public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
      ...
            DecorView(Context context, int featureId, PhoneWindow window,
                WindowManager.LayoutParams params) {
            super(context);
       ...
         
             @Override
        public final View getDecorView() {
            if (mDecor == null || mForceDecorInstall) {
                installDecor();
            }
            return mDecor;
        }
        }
      ...
        
    }

    We see that DecorView is actually an inherited FrameLayout. We know that we can get the View object in XML through getWindow().getDecorView().findViewById() in Activity. When is DecorView instantiated? And when is PhoneWindow instantiated? Because these are not the main contents of our explanation today, you can see what I said before Activity startup source code analysis is described in this article In fact, they are instantiated when the Activity is started. Well, that's all for the DecorView instantiation. At present, the event is passed to DecorView. Let's look at its internal source code implementation. The code is as follows:

      //  DecorView.java
    public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
        public boolean superDispatchTouchEvent(MotionEvent event) {
            return super.dispatchTouchEvent(event);
        }

    We see that the parent class dispatchTouchEvent method is called internally, so it is finally handed over to the top-level View of ViewGroup to handle the distribution.

  2. Distribution process of click events by top-level View

    In the previous section, we learned about an event delivery process. Here we will review it roughly. First, after clicking the event to reach the top-level ViewGroup, it will call its own dispatchTouchEvent method. Then, if its own interception method onInterceptTouchEvent returns true, the event will not continue to be distributed to subclasses. If its own monintouchlistener listening is set, onTouch will be called, otherwise onTouchEvent will be called, If mOnClickListener is set in onTouchEvent, onClick will call. If the onInterceptTouchEvent of ViewGroup returns false, the event will be passed to the clicked child View, and the dispatchTouchEvent of the child View will be called. So far, the event has been transferred from the top-level View to the next level View. The next transfer process is the same as the top-level ViewGroup. In this way, the cycle completes the distribution of the whole event.

    In the first point of this section, we know that the superDispatchTouchEvent method in DecorView calls the dispatchTouchEvent method of the parent class. Let's see its implementation. The code is as follows:

    //ViewGroup.java
        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
         ...
    
                if (actionMasked == MotionEvent.ACTION_DOWN) {
                    //This is mainly used to process the last event when a new event starts
                    cancelAndClearTouchTargets(ev);
                    resetTouchState();
                }
           
                /** Check the event interception, indicating whether the event is intercepted*/
                final boolean intercepted;
                /**
                 * 1. Judge whether the current is pressed
                 */
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || mFirstTouchTarget != null) {
                    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                  //2. The subclass can set the parent class not to intercept through the requestDisallowInterceptTouchEvent method
                    if (!disallowIntercept) {
                      //3
                        intercepted = onInterceptTouchEvent(ev);
                        //Restore events to prevent them from changing
                        ev.setAction(action); 
                    } else {
                        intercepted = false;
                    }
                } else {
                    intercepted = true;
                }
         ...
        }

    From the above code, we can see that if actionMasked == MotionEvent.ACTION\_DOWN or mFirstTouchTarget= If NULL is true, the judgment of note 2 will be executed (mFirstTouchTarget means that if the current event is consumed by the subclass, it will not be true and will be improved later). Disallowuntercept can request the parent class not to intercept the distribution event by calling requestdisallowuntercepttuchevent (true) of the parent class in the subclass, That is, the interception subclass of note 3 is prevented from receiving the pressed event, and on the contrary, onintercepttuchevent (EV) is executed; If true is returned, the event is intercepted.

    Note 1, 2 and 3 onInterceptTouchEvent returns true, indicating that the event is intercepted. Let's explain that intercepted = false when the current ViewGroup does not intercept the event, the event will be sent to its child View for processing. Let's look at the source code of the child View processing, and the code is as follows:

    //ViewGroup.java
    public boolean dispatchTouchEvent(MotionEvent ev) {
     ... 
       
       if (!canceled && !intercepted) {
                                final View[] children = mChildren;
                            for (int i = childrenCount - 1; i >= 0; i--) {
                                final int childIndex = getAndVerifyPreorderedIndex(
                                        childrenCount, i, customOrder);
                                final View child = getAndVerifyPreorderedView(
                                        preorderedList, children, childIndex);
                                if (childWithAccessibilityFocus != null) {
                                    if (childWithAccessibilityFocus != child) {
                                        continue;
                                    }
                                    childWithAccessibilityFocus = null;
                                    i = childrenCount - 1;
                                }
    
                                if (!canViewReceivePointerEvents(child)
                                        || !isTransformedTouchPointInView(x, y, child, null)) {
                                    ev.setTargetAccessibilityFocus(false);
                                    continue;
                                }
    
                                newTouchTarget = getTouchTarget(child);
                                if (newTouchTarget != null) {
                                    newTouchTarget.pointerIdBits |= idBitsToAssign;
                                    break;
                                }
    
                                resetCancelNextUpFlag(child);
                                if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                    mLastTouchDownTime = ev.getDownTime();
                                    if (preorderedList != null) {
                                        for (int j = 0; j < childrenCount; j++) {
                                            if (children[childIndex] == mChildren[j]) {
                                                mLastTouchDownIndex = j;
                                                break;
                                            }
                                        }
                                    } else {
                                        mLastTouchDownIndex = childIndex;
                                    }
                                    mLastTouchDownX = ev.getX();
                                    mLastTouchDownY = ev.getY();
                                    newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                    alreadyDispatchedToNewTouchTarget = true;
                                    break;
                                }
                                ev.setTargetAccessibilityFocus(false);
                            } 
         
       }
      
     ...
    } 

    The above code is also well understood. First, traverse the ViewGroup child, and then judge whether the child element is playing the animation and whether the click event falls within the area of the child element. If a child element meets these two conditions, the event will be passed to the subclass for processing. You can see that what dispatchTransformedTouchEvent actually calls is the dispatchTouchEvent method of the subclass. There is the following section inside it. In the above code, the child transfer is not null, Therefore, it will directly call the dispatchTouchEvent method of the child element, so that the event will be handled by the child element, thus completing a round of event distribution.

    //ViewGroup.java
        private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
                View child, int desiredPointerIdBits) {
         ...
          if (child == null) {
                    handled = super.dispatchTouchEvent(event);
                } else {
                    handled = child.dispatchTouchEvent(event);
                }
           
         ...
          
          
        }

    Here, if child.dispatchTouchEvent(event) returns true, mFirstTouchTarget will be assigned and the for loop will jump out, as shown below:

    //ViewGroup.java
    public boolean dispatchTouchEvent(MotionEvent ev) {
      ...
    newTouchTarget = addTouchTarget(child, idBitsToAssign);
    alreadyDispatchedToNewTouchTarget = true;
      ...
    }
    
    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
            final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
            target.next = mFirstTouchTarget;
                  //At this time, mFirstTouchTarget successfully handles the event on behalf of the child View
            mFirstTouchTarget = target;
            return target;
    }

    These lines of code complete the assignment of mFirstTouchTarget and terminate the traversal of child elements. If the dispatchTouchEvent of the child element returns false, the ViewGroup will continue to traverse and distribute the event to the next child element.

    If the event is not processed after traversing all child elements, ViewGroup will handle the click event by itself. There are two cases in which ViewGroup will handle the event by itself (first, the ViewGroup has no child element. Second, the child element handles the click event, but returns false in the dispatchTouchEvent, which is generally false in the onTouchEvent of the child element)

    The code is as follows:

    public boolean dispatchTouchEvent(MotionEvent ev) {
      ...
       if (mFirstTouchTarget == null) {
                   
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
                            TouchTarget.ALL_POINTER_IDS);
         }
      ...
        
        
    }
    

    It can be seen that if mFirstTouchTarget == null, it means that the child View representing the ViewGroup has not been consumed, and the click event will call its own dispatchTransformedTouchEvent method. Note that the third parameter child in the above code is null. From the previous analysis, it will call super.dispatchTouchEvent(event) Obviously, the dispatchTouchEvent method of the parent class View will be called here, that is, the click event will be handled by the View. Please see the following analysis:

  3. View's handling of click events

    In fact, the process of View handling click events is a little simpler. Note that the View here does not contain ViewGroup. First look at its dispatchTouchEvent method, and the code is as follows:

    //View.java
        public boolean dispatchTouchEvent(MotionEvent event) {
            ...
    
            if (onFilterTouchEventForSecurity(event)) {
                if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                    result = true;
                }
    
                ListenerInfo li = mListenerInfo;
              //1. 
                if (li != null && li.mOnTouchListener != null
                        && (mViewFlags & ENABLED_MASK) == ENABLED
                        && li.mOnTouchListener.onTouch(this, event)) {
                    result = true;
                }
                        //2. 
                if (!result && onTouchEvent(event)) {
                    result = true;
                }
            }
    
            ....
    
            return result;
        }

    The event processing logic in View is relatively simple. Let's first look at note 1. If we set an mtouchlistener click event externally, the onTouch callback will be executed. If the return value of the callback is false, the onTouchEvent method will be executed. It can be seen that the onTouchListener priority is higher than the onTouchEvent method. Let's analyze the onTouchEvent method The implementation code is as follows:

    //View.java
     public boolean onTouchEvent(MotionEvent event) {
            final float x = event.getX();
            final float y = event.getY();
            final int viewFlags = mViewFlags;
            final int action = event.getAction();
    
            final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
    
            /**
             * 1. View The process of handling click events when they are unavailable
             */
            if ((viewFlags & ENABLED_MASK) == DISABLED) {
                if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                    setPressed(false);
                }
                mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                // A disabled view that is clickable still consumes the touch
                // events, it just doesn't respond to them.
                return clickable;
            }
    
            /**
             * 2. If the View has a proxy set, the onTouchEvent method of TouchDelegate is also executed.
             */
            if (mTouchDelegate != null) {
                if (mTouchDelegate.onTouchEvent(event)) {
                    return true;
                }
            }
    
            /**
             * 3. If one of clickable or (viewflags & tooltip) = = tooltip holds, the event will be processed
             */
               if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
                switch (action) {
                    case MotionEvent.ACTION_UP:
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                        if ((viewFlags & TOOLTIP) == TOOLTIP) {
                            handleTooltipUp();
                        }
                        if (!clickable) {
                            removeTapCallback();
                            removeLongPressCallback();
                            mInContextButtonPress = false;
                            mHasPerformedLongPress = false;
                            mIgnoreNextUpEvent = false;
                            break;
                        }
                        // Used to identify quick press
                        boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
    
                            boolean focusTaken = false;
                            if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                                focusTaken = requestFocus();
                            }
    
                            if (prepressed) {
                                setPressed(true, x, y);
                            }
    
                            if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                                removeLongPressCallback();
                                if (!focusTaken) {
                                    if (mPerformClick == null) {
                                        mPerformClick = new PerformClick();
                                    }
                                    /**
                                     * If the click event is set, the mOnClickListener performs an internal callback
                                     */
                                    if (!post(mPerformClick)) {
                                        performClick();
                                    }
                                }
                            }
    
                            if (mUnsetPressedState == null) {
                                mUnsetPressedState = new UnsetPressedState();
                            }
    
                            if (prepressed) {
                                postDelayed(mUnsetPressedState,
                                        ViewConfiguration.getPressedStateDuration());
                            } else if (!post(mUnsetPressedState)) {
                                // If the post failed, unpress right now
                                mUnsetPressedState.run();
                            }
    
                            removeTapCallback();
                        }
                        mIgnoreNextUpEvent = false;
                        break;
    
                    case MotionEvent.ACTION_DOWN:
                       ...
                        //Determine whether it is in the rolling container
                        boolean isInScrollingContainer = isInScrollingContainer();
                        if (isInScrollingContainer) {
                            mPrivateFlags |= PFLAG_PREPRESSED;
                            if (mPendingCheckForTap == null) {
                                mPendingCheckForTap = new CheckForTap();
                            }
                            mPendingCheckForTap.x = event.getX();
                            mPendingCheckForTap.y = event.getY();
                              //Sends an action that delays the execution of a long press event
                            postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                        } else {
                            // Not inside a scrolling container, so show the feedback right away
                            setPressed(true, x, y);
                            checkForLongClick(0, x, y);
                        }
                        break;
    
                    case MotionEvent.ACTION_CANCEL:
                        if (clickable) {
                            setPressed(false);
                        }
                    //Remove some callbacks, such as long press events
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                        break;
    
                    case MotionEvent.ACTION_MOVE:
                        if (clickable) {
                            drawableHotspotChanged(x, y);
                        }
                        if (!pointInView(x, y, mTouchSlop)) {
                              //Remove some callbacks, such as long press events
                            removeTapCallback();
                            removeLongPressCallback();
                            if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                                setPressed(false);
                            }
                            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                        }
                        break;
                }
    
                return true;
            }
    
            return false;
        } 

    Although there are many codes above, the logic is still very clear. Let's analyze it

    1. Judge whether the View is used in the unavailable state, and return a clickable.
    2. Judge whether the View has a proxy set. If the proxy is set, the onTouchEvent method of the proxy will be executed.
    3. If one of clickable or (viewflags & tooltip) = = tooltip holds, the MotionEvent event will be processed.
    4. In MotionEvent event, click onClick and onLongClick callbacks will be executed in up and down respectively.

    Click here. The source code implementation of the event distribution mechanism has been analyzed. Combined with the previously analyzed transmission rules and the following figure, and then combined with the source code, I believe you should understand the event distribution and event processing mechanism.

![image](https://upload-images.jianshu.io/upload_images/27242968-9bfc508e6f06b847.image?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)



Sliding conflict

This section will introduce a very important knowledge point sliding conflict in the View system. It is believed that during development, especially when some sliding effect processing is done, there are more than one layer of sliding and several nested layers of sliding. If they do not solve the sliding conflict, it must be infeasible. Let's take a look at the scenario causing the sliding conflict first.

Sliding conflict scenario and handling rules

1. The external sliding direction is inconsistent with the internal sliding direction

It is mainly the page sliding effect composed of the combination of ViewPager and Fragment. Almost all mainstream applications will use this effect. In this effect, you can switch pages by sliding left and right, and there is often a RecyclerView inside each page. In this case, there is a sliding conflict, but ViewPager handles this sliding conflict internally, so we don't need to pay attention to this problem when using ViewPager. However, if we use ScrollView and other sliding controls, we must manually handle the sliding conflict, otherwise the consequence is that only one of the inner and outer layers can slide, This is because there is a conflict between the sliding events.

Its processing rules are:

When the user slides left and right, the external View needs to intercept the click event. When the user slides up and down, the internal View needs to intercept the click event. At this time, we can solve the sliding conflict according to their characteristics. Specifically, you can determine whether the sliding gesture is horizontal or vertical to correspond to the interception event.

2. The external sliding direction is consistent with the internal sliding direction

This situation is a little more complicated. When the inner and outer layers can slide in the same direction, there is obviously a logic problem. Because is too laggy, the system can't know whether the user wants to slide that layer, so when the fingers slide, there will be problems, or only one layer can slide, or two layers of the inside and outside are sliding carton. In actual development, this scenario mainly refers to that the inner and outer layers can slide up and down at the same time, or the inner and outer layers can slide left and right at the same time.

Its processing rules are:

This kind of thing is special because it cannot be judged according to the sliding angle, distance difference and speed difference, but at this time, a breakthrough can generally be found in the business. For example, the business has regulations that when dealing with a certain state, the external View is required to respond to the sliding of the user, while when in another state, the internal View is required to respond to the sliding of the View, According to such business requirements, we can also get the corresponding processing rules. With the processing rules, we can also proceed to the next step. This scenario may be more abstract through text description. In the next section, we will demonstrate this situation through practical examples.

3. Nesting of 1 + 2 scenes

Scenario 3 is the nesting of scenario 1 and scenario 2, so the sliding conflict in scenario 3 looks more complex. For example, in many applications, there will be such an effect: the inner layer has a sliding effect in Scene 1, and then the outer layer has a sliding effect in Scene 2. Although the sliding conflict in Scene 3 looks complex, it is the superposition of several single sliding conflicts, so it only needs to deal with the conflicts between the inner, middle and outer layers respectively, and the processing method is the same as that in scenes 1 and 2.

Let's take a look at the handling rules of sliding conflict.

Its processing rules are:

Its sliding rules are more complex. Like scenario 2, it can't judge directly according to the sliding angle, distance and speed difference. Similarly, it can only find the breakthrough point from the salesperson. The specific method is the same as scenario 2, which obtains the corresponding processing rules from the business requirements. Code examples will also be given in the next section for demonstration.

Sliding conflict resolution

As mentioned above, we can judge the sliding in Scene 1 according to the sliding distance difference, which is the so-called sliding rule. If we use ViewPager to achieve the effect in scenario 1, we do not need to manually handle the sliding conflict, because ViewPager has already done it for us. However, in order to better demonstrate the sliding conflict resolution idea, ViewPager is not used here. In fact, it is quite simple to get the sliding angle during the sliding process, but what should be done to hand over the click event to the appropriate View for processing? At this time, the event distribution mechanism described in Section 3.4 will be used. For sliding conflict, two ways to solve sliding conflict are given here, external interception and internal interception.

  1. External interception method

    The so-called external interception means that click events are intercepted by the parent container first. If the parent container needs this event, it will be intercepted. If it does not need this event, it will not be intercepted. In this way, the problem of sliding conflict can be solved. This method is more in line with the distribution mechanism of click events. For the external interception method, the onInterceptTouchEvent method needs to be rewritten to intercept the response internally. You can refer to the following code:

        override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
            when (ev.action) {
                MotionEvent.ACTION_DOWN -> {
                    isIntercepted = false
                }
                MotionEvent.ACTION_MOVE -> {
                    //Intercepting mobile events of subclasses
                    if (true) {
                        println("The event distribution mechanism starts distribution ----> Intercepting mobile events of subclasses  onInterceptTouchEvent")
                        isIntercepted = true
                    } else {
                        isIntercepted = false
                    }
    
                }
                MotionEvent.ACTION_UP -> {
                    isIntercepted = false
                }
            }
            return isIntercepted
        }

    The above code is a typical logic of external interception. For different sliding conflicts, you only need to modify the condition that the parent container needs the current click event, and others cannot be modified. Here, the above code is described again. In the onInterceptTouchEvent method, the first is action\_ For the event down, the parent container must return false. Neither intercept ACTION\_DOWN event, because once the parent container intercepts the ACTION\_DOWN, because once the parent container intercepts the ACTION\_DOWN, then the subsequent ACTION\_DOWN, then the subsequent ACTION\_MOVE and ACTION\_UP events will be directly handled by the parent container. At this time, events can no longer be passed to child elements; The second is ACTION\_MOVE event, which can decide whether to intercept according to needs. If it is action\_ For the up event, false must be returned here because of the action\_ The up event itself does not have much meaning.

    Consider a case where an event is handled by a child element if the parent container is in action\_ When up returns true, the child element cannot receive ACTION\_UP event. At this time, the onClick event in the child element cannot be triggered, but the parent container is special. Once it starts to intercept any event, the subsequent events will be handed over to it for processing, and action\_ As the last event, up must also be passed to the parent container, even if the onInterceptTouchEvent method of the parent container is in action\_ false returned when up

  2. Internal interception method

    The internal interception method means that the parent container does not intercept any events, and all events are passed to the child element. If the child element needs this event, it will be consumed directly, otherwise it will be handled by the parent container. This method is inconsistent with the event distribution mechanism in Android. We explained it when explaining the source code, The requestDisalloWInterceptTouchEvent method can work normally. It is slightly more complex than the external interception method. We need to rewrite the dispatchTouchEvent method of the child element

        override fun dispatchTouchEvent(event: MotionEvent): Boolean {
            when (event.action) {
                MotionEvent.ACTION_DOWN -> {
                    println("The event distribution mechanism starts distribution ----> son View  dispatchTouchEvent ACTION_DOWN")
                    parent.requestDisallowInterceptTouchEvent(true)
                }
                MotionEvent.ACTION_MOVE -> {
                    println("The event distribution mechanism starts distribution ----> son View  dispatchTouchEvent ACTION_MOVE")
                    if (true){
                        parent.requestDisallowInterceptTouchEvent(false)
                    }
                }
                MotionEvent.ACTION_UP -> {
                    println("The event distribution mechanism starts distribution ----> son View  dispatchTouchEvent ACTION_UP")
                }
            }
            return super.dispatchTouchEvent(event)
        } 

    The above code is a typical code of the internal interception method. When faced with different sliding strategies, you only need to modify the internal conditions. Other conditions do not need to be changed and cannot be changed. In addition to the processing of child elements, the parent element should also block action by default\_ Events other than down, so that when the child element calls parent.requestDisallowInterceptTouchEvent(false), the parent element can continue to intercept the required events.

    Let's explain it in detail with the actual demo.

actual combat

Scenario 1 sliding conflict case

We customize a ViewPager + RecyclerView, which includes left and right + up and down sliding, so as to meet the sliding conflict of Scene 1. Let's take a look at the complete rendering first:

[image upload failed... (image-1d2523-1636703567722)]

The above screen recording effect solves the conflict between sliding up and down and sliding left and right. The implementation method is to customize the ViewGroup, use the Scroller to achieve the silky feeling like ViewPager, and then add three recyclerviews internally.

Let's take a look at the custom ViewGroup implementation:

class ScrollerViewPager(context: Context?, attrs: AttributeSet?) : ViewGroup(context, attrs) {
    /**
     * Define a Scroller instance
     */
    private var mScroller = Scroller(context)

    /**
     * Determine the minimum moving pixel point to drag
     */
    private var mTouchSlop = 0

    /**
     * Press the x coordinate of the screen with your finger
     */
    private var mDownX = 0f

    /**
     * The current coordinate of the finger
     */
    private var mMoveX = 0f

    /**
     * Record the coordinates of the last trigger press
     */
    private var mLastMoveX = 0f

    /**
     * The left boundary of the interface that can be scrolled
     */
    private var mLeftBorder = 0

    /**
     * The right boundary of the interface that can be scrolled
     */
    private var mRightBorder = 0

    /**
     * Record the X,y of the next interception
     */
    private var mLastXIntercept = 0
    private var mLastYIntercept = 0

    /**
     * Intercept
     */
    private var interceptor = false

    init {
        init()
    }

    constructor(context: Context?) : this(context, null) {
    }


    private fun init() {
        /**
         * Get the shortest movement px value of finger sliding through ViewConfiguration
         */
        mTouchSlop = ViewConfiguration.get(context).scaledPagingTouchSlop


    }


    /**
     * Measure the width and height of child
     */
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        //Number of sub views obtained
        val childCount = childCount
        for (index in 0..childCount - 1) {
            val childView = getChildAt(index)
            //Measure the size of each child control in the ScrollerViewPager
            measureChild(childView, widthMeasureSpec, heightMeasureSpec)

        }
    }

    /**
     * After the measurement, get the size of the child and start taking seats according to the number
     */
    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        if (changed) {
            val childCount = childCount
            for (child in 0..childCount - 1) {
                //Get sub View
                val childView = getChildAt(child)
                //Start taking your seats
                childView.layout(
                    child * childView.measuredWidth, 0,
                    (child + 1) * childView.measuredWidth, childView.measuredHeight
                )
            }
            //Initialize left and right boundaries
            mLeftBorder = getChildAt(0).left
            mRightBorder = getChildAt(childCount - 1).right

        }

    }


    /**
     * External solution 1. Judge according to the vertical or horizontal distance
     */
//    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
//         interceptor = false
//        var x = ev.x.toInt()
//        var y = ev.y.toInt()
//        when (ev.action) {
//            MotionEvent.ACTION_DOWN -> {
//                interceptor = false
//            }
//            MotionEvent.ACTION_MOVE -> {
//                var deltaX = x - mLastXIntercept
//                var deltaY = y - mLastYIntercept
//                interceptor = Math.abs(deltaX) > Math.abs(deltaY)
//                if (interceptor) {
//                    mMoveX = ev.getRawX()
//                    mLastMoveX = mMoveX
//                }
//            }
//            MotionEvent.ACTION_UP -> {
//                //Get the x coordinate of the current movement
//                interceptor = false
//                println("onInterceptTouchEvent---ACTION_UP")
//
//            }
//        }
//        mLastXIntercept = x
//        mLastYIntercept = y
//        return interceptor
//    }

    /**
     * External solution 2. According to the second point coordinate - the first point coordinate, if the difference is greater than touchslope, it is considered to be sliding left and right
     */
    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        interceptor = false
        when (ev.action) {
            MotionEvent.ACTION_DOWN -> {
                //Get your finger and press the coordinates equivalent to the screen
                mDownX = ev.getRawX()
                mLastMoveX = mDownX
                interceptor = false
            }
            MotionEvent.ACTION_MOVE -> {
                //Get the x coordinate of the current movement
                mMoveX = ev.getRawX()
                //Get the difference
                val absDiff = Math.abs(mMoveX - mDownX)
                mLastMoveX = mMoveX
                //When the finger drag value is greater than the touchslope value, it is considered to be sliding and intercept the touch event of the child control
                if (absDiff > mTouchSlop)
                    interceptor =   true
            }
        }
        return interceptor
    }


    /**
     * If the parent container does not intercept the event, the user's touch event will be received here
     */
    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_MOVE -> {
                println("onInterceptTouchEvent---onTouchEvent--ACTION_MOVE ")
                mLastMoveX = mMoveX
                //Get the coordinates of the current slide relative to the upper left corner of the screen
                mMoveX = event.getRawX()
                var scrolledX = (mLastMoveX - mMoveX).toInt()
                if (scrollX + scrolledX < mLeftBorder) {
                    scrollTo(mLeftBorder, 0)
                    return true
                } else if (scrollX + width + scrolledX > mRightBorder) {
                    scrollTo(mRightBorder - width, 0)
                    return true

                }
                scrollBy(scrolledX, 0)
                mLastMoveX = mMoveX
            }
            MotionEvent.ACTION_UP -> {
                //When the finger is raised, determine which child control interface should be rolled back according to the current scroll value
                var targetIndex = (scrollX + width / 2) / width
                var dx = targetIndex * width - scrollX
                /** The second step is to call the startScroll method to roll back elastically and refresh the page*/
                mScroller.startScroll(scrollX, 0, dx, 0)
                invalidate()
            }
        }
        return super.onTouchEvent(event)
    }

    override fun computeScroll() {
        super.computeScroll()
        /**
         * The third step is to rewrite the computeScroll method and complete the logic of smooth scrolling inside it
         */
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.currX, mScroller.currY)
            postInvalidate()
        }
    }
}

The above code is very simple. It handles the conflict of external interception methods in two ways:

  • Judge according to the vertical or horizontal distance
  • According to the second point coordinate - the first point coordinate, if the difference is greater than touchslope, it is considered to be sliding left and right

Of course, we can also use the internal interception method. According to our previous analysis of the internal interception method, we only need to modify the interception logic of the parent container in the distribution event dispatchTouchEvent method of the user-defined recelerview. Please see the code implementation below:

class MyRecyclerView(context: Context, attrs: AttributeSet?) : RecyclerView(context, attrs) {
    /**
     * Record the coordinates of our last slide respectively
     */
    private var mLastX = 0;
    private var mLastY = 0;

    constructor(context: Context) : this(context, null)


    /**
     * Override distribution event
     */
    override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
        val x = ev.getX().toInt()
        val y = ev.getY().toInt()

        when (ev.action) {
            MotionEvent.ACTION_DOWN -> {
            var par =    parent as ScrollerViewPager
                //Request the parent class not to intercept events
                par.requestDisallowInterceptTouchEvent(true)
                Log.d("dispatchTouchEvent", "--->son ACTION_DOWN");
            }
            MotionEvent.ACTION_MOVE -> {
                val deltaX = x - mLastX
                val deltaY = y - mLastY

                if (Math.abs(deltaX) > Math.abs(deltaY)){
                    var par =    parent as ScrollerViewPager
                    Log.d("dispatchTouchEvent", "dx:" + deltaX + " dy:" + deltaY);
                    //Leave it to the parent class
                    par.requestDisallowInterceptTouchEvent(false)
                }
            }
            MotionEvent.ACTION_UP -> {
            }
        }
        mLastX = x
        mLastY = y
        return super.dispatchTouchEvent(ev)
    }

}

You also need to change the parent class onInterceptTouchEvent method

    /**
     * A subclass requesting a parent class is also called an internal interception method
     */
    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        interceptor = false
        when (ev.action) {
            MotionEvent.ACTION_DOWN -> {
                //Get your finger and press the coordinates equivalent to the screen
                mDownX = ev.getRawX()
                mLastMoveX = mDownX
                if (!mScroller.isFinished) {
                    mScroller.abortAnimation()
                    interceptor = true
                }
                Log.d("dispatchTouchEvent", "--->onInterceptTouchEvent,    ACTION_DOWN" );
            }
            MotionEvent.ACTION_MOVE -> {
                //Get the x coordinate of the current movement
                mMoveX = ev.getRawX()
                //Get the difference
                mLastMoveX = mMoveX
                  //If the parent class consumes the mobile event, its onTouchEvent will be called
                interceptor = true
                Log.d("dispatchTouchEvent", "--->onInterceptTouchEvent,    ACTION_MOVE" );
            }
        }
        return interceptor
    }
<?xml version="1.0" encoding="utf-8"?>
<com.devyk.customview.sample_1.ScrollerViewPager //Parent node
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        //Child node
    <com.devyk.customview.sample_1.MyRecyclerView
            android:id="@+id/recyclerView" 
                        android:layout_width="match_parent"
            android:layout_height="match_parent">

    </com.devyk.customview.sample_1.MyRecyclerView>

    <com.devyk.customview.sample_1.MyRecyclerView
            android:id="@+id/recyclerView2" 
                        android:layout_width="match_parent"
            android:layout_height="match_parent">

    </com.devyk.customview.sample_1.MyRecyclerView>

    <com.devyk.customview.sample_1.MyRecyclerView
            android:id="@+id/recyclerView3" 
                        android:layout_width="match_parent"
            android:layout_height="match_parent">

    </com.devyk.customview.sample_1.MyRecyclerView>
</com.devyk.customview.sample_1.ScrollerViewPager>

Here, explain the meaning of the above code. First, rewrite the dispatchTouchEvent event event in mystylerview, distribute the event, and process down and move respectively.

**DOWN: * * when we press our finger, we will execute the dispatchTouchEvent method of the ViewGroup and the onInterceptTouchEvent intercepting event method of the ViewGroup. Because the onInterceptTouchEvent event is rewritten in the ScrollerViewPager, we can see that the event will be intercepted by the parent class when the above DOWN only slides again and does not end, Generally, false is returned. The parent class does not intercept. When the parent class does not intercept the DOWN event, the DOWN event of the dispatchTouchEvent of the child node mystylerview will be triggered. Please note that in the DOWN event, I called the requestdisallowunterceptotouchevent (true) method of the current root node ScrollerViewPager, This means that the parent class is not allowed to execute the onintercepttuchevent method.

MOVE: when our fingers slide, because we ask the parent class not to intercept the child node events, the onInterceptTouchEvent of ViewGroup will not be executed. Now we will execute the MOVE method of the child node. If the X and Y coordinates currently pressed minus the last X and Y coordinates, as long as the absolute value of deltaX > deltay, it is considered to be sliding left and right, Now you need to intercept the child node MOVE event and hand it over to the parent node for processing, so that you can slide left and right in the ScrollerViewPager. On the contrary, it is considered to slide up and down and be handled by child nodes.

It can be seen that the internal interception method is complex. It is necessary to modify not only the internal code of the child node, but also the parent node method. Its stability and maintainability are obviously not as good as the external interception method. Therefore, it is recommended to use the external interception method to solve the time conflict.

Let's take a look at a common APP function, sideslip deletion. Generally, sideslip is realized by a RecyclerView + sideslip custom ViewGroup:

actual combat

Refer to the implementation in this Demo, from which you can learn technologies such as customizing ViewGroup and sliding conflict resolution.

[image upload failed... (image-acb847-1636703567722)]

This article is transferred from https://juejin.cn/post/6844904002753150983 , in case of infringement, please contact to delete.

More Android tutorials can be uploaded on bilibili bili:** https://space.bilibili.com/686960634

Posted by kubis on Thu, 18 Nov 2021 00:15:02 -0800