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

Posted by Fira on Fri, 12 Nov 2021 15:14:06 +0100

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)
}

}

  1. 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)
}

}

  1. 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 event MotionEvent When a click operation occurs, the event is first passed to the current user Activity ,from Activity of dispatchTouchEvent To dispatch the incident. The specific work is carried out by Activity Internal Window To finish it. Window Will pass the event to DecorView ,DecorView Generally, it is the underlying container of the current interface, that is**setContentView Set parent container**,It is inherited from **FrameLayout** ,It's in Activity Can pass **getWindow().getDecorView()get** ,Because the event was first Activity Start distributing, then we'll look directly at its **dispactchTouchEvent** Method, the code is as follows:

//Activity.java

public boolean dispatchTouchEvent(MotionEvent ev) {
    /**
     * The first trigger to press is 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 Press event execution **onUserInteraction** The empty method is then called. getWindow() of superDispatchTouchEvent Method, here getWindow It's actually its only subclass PhoneWindow Let's look at 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);
}

...

}

stay PhoneWindow of superDispatchTouchEvent In the function DecorView To handle it, then DecorView What is it?

//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 DecorView It's actually inherited FrameLayout ,We know where Activity We can pass getWindow().getDecorView().findViewById() Get the corresponding XML Medium View object , that DecorView When is instantiation performed? also PhoneWindow When was it instantiated? Because these are not the main contents of our explanation today, you can see what I said before [Activity Start source code analysis, which is mentioned in this article](https://juejin.cn/post/6844903983442558989 "https://juejin.cn/post/6844903983442558989 ") 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 is called internally dispatchTouchEvent Method, so it is finally handed over to ViewGroup top-level View To handle the distribution.
  1. 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 click the event to reach the top level ViewGroup After that, it calls its own dispatchTouchEvent Method, and then if its own interception method onInterceptTouchEvent return true ,The event will not continue to be distributed to subclasses if it is set mOnTouchListener Monitor, then onTouch Will be called, otherwise onTouchEvent Will be called if onTouchEvent Set in mOnClickListener that onClick Will be called. If ViewGroup of onInterceptTouchEvent return false,The event will be passed to the clicked child View At this time View of dispatchTouchEvent Will be called. So far, the event has been removed from the top level View Passed to the next level View ,The next delivery process and top-level ViewGroup Similarly, this loop completes the distribution of the whole event.
In the first point of this section, we know that in DecorView Medium superDispatchTouchEvent Method called the of the parent class internally dispatchTouchEvent Method, let's look at 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 perhaps mFirstTouchTarget != null If established, the judgment of note 2 will be executed( mFirstTouchTarget It means that if the current event is consumed by subclasses, it is not tenable and will be increased later), disallowIntercept You can call the parent class in a subclass requestDisallowInterceptTouchEvent(true) Request the parent class not to intercept the distribution event, that is, prevent the intercepting subclass of note 3 from receiving the pressed event, and vice versa onInterceptTouchEvent(ev); If return true Indicates that the event was intercepted.
Notes 1, 2 and 3 are described above onInterceptTouchEvent return true It shows that the event was intercepted. Let's explain it below intercepted = false current ViewGroup When the event is not intercepted, the event will be sent to its children View For processing, let's look at the sub View Processing source code, 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 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 satisfies these two conditions, the event will be passed to the subclass for processing. You can see that,**dispatchTransformedTouchEvent** In fact, what is called is the of the subclass **dispatchTouchEvent** Method, there is the following section inside it, and in the above code child Pass is not null ,Therefore, it calls the child element directly dispatchTouchEvent Method, so that the event is 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) return true , that mFirstTouchTarget Will be assigned and jump out at the same time for Cycle as follows:

//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 are complete mFirstTouchTarget And terminates the traversal of child elements. If the child element's dispatchTouchEvent return false ,ViewGroup The traversal continues, and the event is distributed to the next child element.
If the event is not processed after traversing all child elements, then ViewGroup Click events will be handled by yourself. There are two cases ViewGroup Will handle the event itself (firstly: ViewGroup There is no child element. Second, the child element handles the click event, but dispatchTouchEvent Returned in false,This is usually in the child element onTouchEvent Returned in false )
The code is as follows:

public boolean dispatchTouchEvent(MotionEvent ev) {

...

if (mFirstTouchTarget == null) {

handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
 }

...

}

You can see if mFirstTouchTarget == null When, then it represents ViewGroup Son of View Click events that are not consumed will call their own dispatchTransformedTouchEvent method. Notice the third parameter in the above code child by null ,As you can see from the previous analysis, it will call super.dispatchTouchEvent(event) ,Obviously, the parent class will be called here View of dispatchTouchEvent Method, that is, click the event to start the handover View Please refer to the following analysis:
  1. View's handling of click events
actually View The process of handling click events is a little simpler. Please note here View Not included ViewGroup . Look at it first dispatchTouchEvent Method, 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;
}
View The event processing logic in is relatively simple. Let's look at note 1 first. If we set it externally mOnTouchListener Click the event and it will be executed onTouch Callback, if the return value of the callback is false ,Then it will be executed onTouchEvent Method, visible onTouchListener Priority over onTouchEvent Methods, let's analyze onTouchEvent Method implementation, the 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:... / / judge whether it is in the scroll container. Boolean isinscollingcontainer = isinscollingcontainer(); if (isInScrollingContainer) {                    mPrivateFlags |= PFLAG_PREPRESSED;                    if (mPendingCheckForTap == null) {                        mPendingCheckForTap = new CheckForTap();                    }                    mPendingCheckForTap.x = event.getX();                    mPendingCheckForTap.y = event.getY() ;                   	// Send an operation that delays the execution of a long click 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 the long press event 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 event removetapcallback(); removelongpresscallback(); if ((mprivateflags & pflag_pressed)! = 0) {setpressed (false);} mprivateflags3 & = ~ pflag3_finder_down;}                 break;        }         return true;    }     return false;} 
Although there are many codes above, the logic is still very clear. Let's analyze it
1.  judge View Whether to use in the unavailable state, return a clickable . 
2.  judge View Whether the proxy is set. If the proxy is set, the proxy will be executed onTouchEvent method.
3.  If clickable or (viewFlags & TOOLTIP) == TOOLTIP If one is established, it will be handled MotionEvent event.
4.  stay MotionEvent In the event, the up and down Click onClick and onLongClick Callback.
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.
![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/11/22/16e925cdb76a1c84~tplv-t2oaga2asx-watermark.image)

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. The external interception method needs to be rewritten onInterceptTouchEvent Method 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 onInterceptTouchEvent Method, the first is ACTION\_DOWN For this event, the parent container must return false . Neither interception ACTION\_DOWN Event, because once the parent container intercepts ACTION\_DOWN , This is because once the parent container intercepts ACTION\_DOWN, So the follow-up ACTION\_DOWN, So the follow-up 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 yes ACTION\_UP Event, which must be returned here false , because ACTION\_UP The event itself does not have much meaning.
Consider a case where events are handled by child elements if the parent container is ACTION\_UP Returned at true ,This will cause the child element to fail to receive ACTION\_UP Event, at this time, in the child element onClick Events cannot be triggered, but the parent container is special. Once it starts to intercept any event, subsequent events will be handed over to it for processing ACTION\_UP As the last event, it must also be passed to the parent container, even if the parent container's onInterceptTouchEvent Method in ACTION\_UP Returned at false.
  1. Internal interception method
Internal interception means that the parent container does not intercept any events, and all events are passed to the child elements. If the child elements need this event, it will be consumed directly, otherwise it will be handed over to the parent container for processing. This method and Android The event distribution mechanism in is inconsistent. When explaining the source code, we explained it through requestDisalloWInterceptTouchEvent Method can work normally. It is slightly more complex to use than the external interception method. We need to rewrite the of child elements dispatchTouchEvent method
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, and others do not need to be changed and cannot be changed. In addition to the processing of child elements, the parent elements should also be blocked by default ACTION\_DOWN Other events, so that when the child element calls parent.requestDisallowInterceptTouchEvent(false) ,The parent element can continue to intercept the required events.
Here's a practical example demo Let's be specific.

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:

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.

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

Topics: Android