The last article on the View distribution mechanism of the source, this time to talk about the way to solve the conflict View sliding and principle.

I. Sliding conflict scenes and causes

There are two main scenarios for sliding collisions:

  • The parent ViewGroup slides in the same direction as the child View
  • The parent ViewGroup and the child View slide in different directions

So why is there a sliding conflict, like if the parent ViewGroup and the child View slide in the same direction, I need to slide both of them. In the last blog post, we analyzed the event distribution mechanism, and mentioned that the onInterceptTouchEvent method of the ViewGroup returns false by default, meaning that the ViewGroup does not intercept events by default. When a ViewGroup receives an event, it looks for a child View that can handle the event because it does not intercept the event. Once the child View handles the DOWN event, by default all other events in the same sequence are handled by the child View. The effect is that the child View can slide, but the parent ViewGroup never slides, and slide conflicts occur.

2. The solution of sliding conflict

There are two ways to solve sliding conflict: external interception and internal interception

2.1 ViewPager sliding Conflicts

For example, when we use ViewPager, we tend to combine a Fragment with a ListView inside the Fragment. Here the ViewPager has resolved the sliding conflict for us, so it doesn’t conflict when used. By default, the ViewPager onInterceptTouchEvent method returns false. Since the ListView can scroll, that means the ListView can handle events, so all events are handled by the ListView. So what we see is that the ListView can scroll vertically, but the ViewPager can’t scroll horizontally.

You can rewrite the ViewPager so that the onInterceptTouchEvent method of the ViewPager returns the default false. Inside the ViewPager is multiple ListViews.

public class MyViewPager extends ViewPager {
    public MyViewPager(@NonNull Context context) {
        super(context);
    }

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

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return false; }}Copy the code

Operation effect is shown in figure

So how does the ViewPager resolve sliding conflicts like this, leading to external interception.

2.2 External interception method

2.2.1 principle

The so-called external interception method, is when the event is passed to the parent container, through the parent container to determine whether they need this event, if yes, intercept the event, do not intercept the event, pass the event to the child View. MyViewPager and ListView obviously have a sliding conflict, let’s analyze it. What we want is for the ViewPager to scroll horizontally, and the ListView to scroll vertically, but the ViewPager to not move, so we need to specify event handling conditions for the ViewGroup, resulting in the following pseudocode.

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
        case MotionEvent.ACTION_MOVE:
            if(ViewPager requires this event) {return true;
            }
            break;
        default:
            break;
    }
    return false;
}
Copy the code

Now let’s look at why this code resolves sliding conflicts.

When overwriting the ViewGroup onInterceptTouchEvent method, the ViewGroup cannot intercept DOWN and UP events. Because once the ViewGroup intercepts the DOWN event, that is, the mFirstTouchTarget and mFirstTouchTarget remain empty, no other events in the same sequence of events are passed DOWN; If the ViewGroup intercepts an UP event, the child View will not fire the click event, because the child View’s click event is fired at the UP event.

  • When a ViewPager receives a DOWN event, the ViewPager does not intercept the DOWN event by default. The DOWN event is handled by the ListView. Since the ListView can scroll, that is, consume the event, the mFirstTouchTarget of the ViewPager is assigned a value. Find the child View that handles the event. Then the ViewPager receives the MOVE event,
  • If the event is not needed by the ViewPager, then the ListView will handle the event.
  • If this event is required by the ViewGroup, since the DOWN event is handled by the ListView, the mFirstTouchEventTarget will be assigned a value and onInterceptedTouchEvent will be called. In this case, since the ViewPager is interested in this event, The onInterceptedTouchEvent method returns true, indicating that the ViewPager intercepts the event. In this case, the current MOVE event will disappear, be passed down or handled by itself, and the mFirstTouchTarget will be reset to NULL.
  • When the MOVE event comes again, since mFristTouchTarget is null, all subsequent events are handed over to the ViewPager.

2.2.2 Solution

The ViewPager can handle event conditions in a variety of ways, such as horizontal and vertical sliding speeds, horizontal and vertical sliding distances, etc. If the horizontal sliding distance is greater than the vertical sliding distance, the ViewPager will process the event, and if the horizontal sliding distance is greater than the vertical sliding distance, the Event will be passed to the ListView.

public class MyViewPager extends ViewPager {
    private int mLastX;
    private int mLastY;

    public MyViewPager(@NonNull Context context) {
        super(context);
    }

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

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        // Some ViewPager flags must be set to super, otherwise the effect will not be seen
        super.onInterceptTouchEvent(ev);

        boolean isIntercepted = false;
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_MOVE:
                if (needEvent(ev)) {
                    isIntercepted = true;
                }
                break;
            default:
        }
        mLastX = (int) ev.getX();
        mLastY = (int) ev.getY();

        LogUtils.d(" lastX = " + mLastX + " lastY = " + mLastY);
        return isIntercepted;
    }

    private boolean needEvent(MotionEvent ev) {
        // If the horizontal scroll distance is larger than the vertical scroll distance, the event is handled by the ViewPager
        returnMath.abs(ev.getX() - mLastX) > Math.abs(ev.getY() - mLastY); }}Copy the code

Operation effect:

2.2.3 summary

  • The external interception method is mainly the parent container to control the interception of events, if the event is needed by the parent container, it will be intercepted, and if not, it will be passed down.
  • The parent container cannot intercept DOWN events or UP events.

2.2.4 Common Templates

public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean isIntercept = false;
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // The DOWN event cannot be intercepted, otherwise the event will not be distributed to child views
                isIntercept = false;
                break;
            case MotionEvent.ACTION_MOVE:
                // Determine whether to intercept events based on conditions
                isIntercept = needThisEvent();
                break;
            case MotionEvent.ACTION_UP:
                // Once the parent intercepts the UP event, the child View will not be able to trigger the click event
                isIntercept = false;
                break;
            default:
                break;
        }
        return isIntercept;
    }
Copy the code

2.3 Internal interception method

2.3.1 Conflict Scenarios

Let’s talk about a slightly more complicated co-sliding conflict. The inner LinearLayout of the ScrollView has three sub-views, ImageView, ListView and TextView from top to bottom.

First up and down renderings:

You can see that the desired effect now is to touch the area outside the ListView, and the ScrollView can slide without limitation. There are several situations when touching the ListView area. When the ListView scrolls to the top (the ListView is in the initial state), if the finger slides down, the ScrollView slides down; When the ListView scrolls to the bottom, if the finger slides up, the ScrollView slides up; otherwise, the ListView scrolls.

2.3.2 principle

Internal interception: the ViewGroup does not intercept events by default. The child View controls the event processing. If the child View needs the event, it handles it by itself.

To use internal intercepts, you need to override both the onInterceptTouchEvent method of the parent ViewGroup and the dispatchTouchEvent method of the child ViewGroup that needs to resolve the conflict.

Child View pseudocode

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            // Disallows the parent container from intercepting events
            getParent().requestDisallowInterceptTouchEvent(true);
            break;
        case MotionEvent.ACTION_MOVE:
            if(Current View does not require this event) {// Allow the parent container to intercept events
                getParent().requestDisallowInterceptTouchEvent(false);
            }
            break;
        default:
            break;
    }
    return super.dispatchTouchEvent(ev);
    }
Copy the code

ViewGroup pseudo code

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            return false;
        default:
            return true; }}Copy the code

Here we combine ScrollView and ListView as a specific example and flow chart for analysis.

First of all, the parent container ScrollView cannot intercept the DOWN event, it must distribute the DOWN event to the child View, which is the ListView, because once the parent container intercepts the DOWN event, no other event in the same sequence of events will be transmitted to the child View. This has been analyzed in the event distribution source code analysis. I won’t repeat it here.

The mFristTonchTarget is not null because the DOWN event is handled by the View, and the mFristTonchTarget is not null. By default, onInterceptedTouchEvent is called. If this method returns true for that event, the event will be intercepted by the parent. The event will obviously not be passed to the child View, but we need to pass the event to the child View and let the child View control the event handling. So how do we pass events to child views? As you can see from the source code, there is another judgment before calling the onInterceptedTouchEvent method.

if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget ! =null) {
    // Whether to disable event interception. Default is false
    final booleandisallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) ! =0;
    if(! disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action);// restore action in case it was changed
    } else {
        intercepted = false; }}else {
    intercepted = true;
}
Copy the code

As you can see from the source code, the onInterceptTouchEvent method is called based on the value of disallowIntercept, which defaults to false. If you can set disallowIntercept to true, you can bypass onIntercepted and pass the event to the child View. FLAG_DISALLOW_INTERCEPT is set to true for mGroupFlags before a MOVE event. If the onInterceptedTouchEvent method is passed to the child View, it will bypass the onInterceptedTouchEvent method. How to set the flag bit to the ViewGroup before the MOVE event? We can see this method in ViewGroup.

public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

    if(disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) ! =0)) {
        return;
    }

    if (disallowIntercept) {
        mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
    } else {
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    }

    // Pass it up to our parent
    if(mParent ! =null) { mParent.requestDisallowInterceptTouchEvent(disallowIntercept); }}Copy the code

If you can see, in the call requestDisallowInterceptTouchEvent method, parameter is true, is installed FLAG_DISALLOW_INTERCEPT mGroupFlags flags, The value of disallowIntercept will be true. As for call timing, we simply call this method when the child View receives a DOWN event, after which the parent ViewGroup passes the event directly to the child View that handles the DOWN event.

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            // Disallows the parent container from intercepting events
            getParent().requestDisallowInterceptTouchEvent(true);
            break; . }... }}Copy the code

If the following event is of interest to the child View, it will be handled directly. If the child View is not interested in the event, it will be returned to the parent View for processing. So, how do I get events that the child View doesn’t need back to the parent View? At this point, one might say, in event distribution, isn’t the event that the child View can’t handle automatically handed over to the parent ViewGroup? We said that the child View can not handle the event will be passed to the parent ViewGroup processing, this is for the default DOWN event distribution process, but in this case is not DOWN event and there is manual intervention, is this really the case? Let’s look at the source code.

The mFirstTouchEvent of the parent ViewGroup is not null. The mFirstTouchEvent of the parent ViewGroup is null. The subsequent MOVE event is not needed by the child View, that is, the child View does not process it, so the child View’s dispatchTouchEvent method returns false.

public boolean dispatchTouchEvent(MotionEvent ev) {...if (mFirstTouchTarget == null) {
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
    } else {
        TouchTarget predecessor = null;
        TouchTarget target = mFirstTouchTarget;
        while(target ! =null) {
            final TouchTarget next = target.next;
            if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                handled = true;
            } else {
                final boolean cancelChild = resetCancelNextUpFlag(target.child)
                        || intercepted;

                / / child View does not handle events, child View dispatchTouchEvent returns false, dispatchTransformedTouchEvent to false
                if (dispatchTransformedTouchEvent(ev, cancelChild,
                        target.child, target.pointerIdBits)) {
                    handled = true;
                }
                if (cancelChild) {
                    if (predecessor == null) {
                        mFirstTouchTarget = next;
                    } else {
                        predecessor.next = next;
                    }
                    target.recycle();
                    target = next;
                    continue; } } predecessor = target; target = next; }}if (canceled
            || actionMasked == MotionEvent.ACTION_UP
            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
        resetTouchState();
    } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
        final int actionIndex = ev.getActionIndex();
        final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
        removePointersFromTouchTargets(idBitsToRemove);
    }

    if(! handled && mInputEventConsistencyVerifier ! =null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
    }

    // Return false directly
    return handled;
}
Copy the code

As you can see from the source code, the dispatchTouchEvent method of the ViewGroup will return false directly in this case, and will not handle MOVE events that are not of interest to the current child View. The parent container of the parent ViewGroup will also return false directly. Until passed to the Activity, the event is processed by the Activity or disappears. And when another MOVE event comes, the MOVE will still be passed to the child View, but the child View is not interested in the current MOVE event, that is, all subsequent MOVE events will not be handled by the parent ViewGroup, which is obviously a problem. So how does a child View give event handling to its parent ViewGroup when it’s not interested in an event? We in child View by calling the ViewGroup requestDisallowInterceptTouchEvent method, prohibit the parent ViewGroup to intercept events, can also be in the View of events is not interested in, Call the ViewGroup requestDisallowInterceptTouchEvent method, allows the parent container to intercept events.

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
        case MotionEvent.ACTION_MOVE:
            if(Current View does not require this event) {// Allow the parent container to intercept events
                getParent().requestDisallowInterceptTouchEvent(false);
            }
            break;
        default:
            break;
    }
    return super.dispatchTouchEvent(ev);
    }
Copy the code

For the child View, the control logic for event handling is complete, but not for the parent ViewGroup. You must override the ViewGroup onInterceptedTouchEvent method to make the MOVE and UP events return true, indicating that the child View is not interested in intercepting events. It is understandable that the parent ViewGroup intercepts MOVE events, but why intercept UP events? The parent ViewGroup can only receive click events if it intercepts UP events.

2.3.3 Specific implementation

Now let’s actually resolve the sliding conflicts between ScrollView and ListView. In fact, the internal interception template is already reflected in the pseudocode. Just implement the child View’s judgment on event handling. We need to listen for the ListView scrolling to the top and bottom, and when the ListView scrolls to the top and the finger touches down or the ListView scrolls to the bottom and the phone touches up, we hand the event over to the ScrollView.

public class MyListView extends ListView implements AbsListView.OnScrollListener {

    private boolean isScrollToTop;
    private boolean isScrollToBottom;

    private int mLastX;
    private int mLastY;

    public MyListView(Context context) {
        this(context, null);
    }

    public MyListView(Context context, AttributeSet attrs) {
        this(context, attrs, -1);
    }

    public MyListView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init(a) {
        setOnScrollListener(this);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        LogUtils.d("" + Constants.getActionName(ev.getAction()));
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);
                mLastX = (int) ev.getX();
                mLastY = (int) ev.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                if (superDispatchMoveEvent(ev)) {
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:
                LogUtils.d("ACTION_UP");
                break;
            default:
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

    /** * passes the event to the parent container **@param ev
     * @return* /
    private boolean superDispatchMoveEvent(MotionEvent ev) {
        / / decline in
        boolean canScrollBottom = isScrollToTop && (ev.getY() - mLastY) > 0;
        boolean canScrollTop = isScrollToBottom && (ev.getY() - mLastY) < 0;

        return canScrollBottom || canScrollTop;
    }

    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {}@Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        isScrollToBottom = false;
        isScrollToTop = false;

        if (firstVisibleItem == 0) {
            android.view.View firstVisibleItemView = getChildAt(0);
            if(firstVisibleItemView ! =null && firstVisibleItemView.getTop() == 0) {
                LogUtils.d("##### scroll to top ######");
                isScrollToTop = true; }}if ((firstVisibleItem + visibleItemCount) == totalItemCount) {
            View lastVisibleItemView = getChildAt(getChildCount() - 1);
            if(lastVisibleItemView ! =null && lastVisibleItemView.getBottom() == getHeight()) {
                LogUtils.d("##### scroll to the bottom ######");
                isScrollToBottom = true; }}}}Copy the code

By default, ScrollView intercepts MOVE events in the drag state, but not UP events. If you need to intercept UP events, you can override the onInterceptTouchEvent method of ScrollView, but it is not necessary to intercept UP events. If the parent ViewGroup does not need to trigger the click event, it does not need to intercept.

public class MyScrollView extends ScrollView {
    public MyScrollView(Context context) {
        super(context);
    }

    public MyScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted  = super.onInterceptTouchEvent(ev);
        if (ev.getAction() == MotionEvent.ACTION_UP) {
            intercepted = true;
        }
        returnintercepted; }}Copy the code

2.3.4 summary

  • The internal interception method is to give control of the event to the child View, if the child View needs the event, it will process the event, do not need to pass the event to the parent ViewGroup, let the parent ViewGroup process.
  • Child View by calling the parent ViewGroup requestDisallowInterceptTouchEvent to intervene in the parent ViewGroup to intercept state of the event
  • The parent ViewGroup cannot intercept DOWN events. The interception status of MOVE or UP events depends on the situation

Ok, so that’s the end of the two ways to resolve sliding conflicts, but it’s important to note that ViewPager and ListView sliding conflicts can be resolved not only by external interception, but also by internal interception, as in the second scenario. The solution is not absolute, we have to do is to choose the most convenient implementation of the solution.