Detailed explanation of event distribution mechanism

Posted by jh_dempsey on Mon, 21 Oct 2019 16:24:31 +0200

Common events

Since it's event distribution, there must be events to distribute, so let's first learn about several common events.

According to the object-oriented idea, events are encapsulated as MotionEvent objects. Because this article does not focus on this, it only involves several common events related to finger touch:

Event brief action? Down triggered when the finger first touches the screen. Action? Move is triggered when the finger is sliding on the screen, and will be triggered multiple times. Action? Up triggered when the finger leaves the screen. Triggered when the action? Cancel event is intercepted by the upper layer.

For single touch, a simple interaction process is as follows:

Action? Down? > action? Move? > action? Up

In this case, action? Move has been triggered multiple times. If you just click (press the finger and then lift it), action? Move will not be triggered.

View correlation

Q: why does View have dispatchTouchEvent?

A: we know that View can register many event listeners, such as: click event (onClick), long click event (onLongClick), touch event (onTouch), and View itself has onTouchEvent method, so the question is, who should manage so many event related methods? There is no doubt that it is dispatchTouchEvent, so View will also have event distribution.

It's believed that seeing a lot of small partners here will raise questions. There are so many event listeners in View. Which one should be executed first?

Q: what is the order of method calls related to View events?

A: if you don't look at the source code, what will happen if you think about making your own design?

Clicking the event (onClickListener) requires two two events (ACTION_DOWN and ACTION_UP) to trigger. If it is first allocated to onClick judgment, when it is judged that the user's finger has left the screen, the day lily is cold, which will cause View to be unable to respond to other events. (last)

In the same way, it takes a long time to wait for the result of an on long click listener. It must not be in the front, but it should be in the front of onClick because there is no need for action "up". (onlongclicklistener > onclicklistener)

Touch event (onTouchListener) if the user registers a touch event, it means that the user has to handle the touch event himself, which should be at the top. (first) View processing (ontoucheevent) provides a default processing method. If the user has already processed it, it will not be needed, so it should be placed behind the onTouchListener. (onTouchListener > ontoucheevent)

So the scheduling order of events should be ontouchlistener > ontoucheevent > onlongclicklistener > onclicklistener.

Let's take a look at the actual test results:

Press the finger, do not move, wait for a moment and then lift it.

Listener : onTouchListener ACTION_DOWN

[GcsView ]: onTouchEvent ACTION_DOWN

Listener : onTouchListener ACTION_UP

[GcsView ]: onTouchEvent ACTION_UP

As you can see, the test results also support our conjecture, because the long-term click listener does not need action UUP, so it will trigger after action UU down.

Next, let's see how the source code is designed (a lot of irrelevant code is omitted):

public boolean dispatchTouchEvent(MotionEvent event) {

...
boolean result = false;    // result is the return value, mainly used to tell the caller whether the event has been consumed.
if (onFilterTouchEventForSecurity(event)) {
    ListenerInfo li = mListenerInfo;
    /** 
     * If OnTouchListener is set and the current View is clickable, the listener's onTouch method is called.
     * If the return value of the onTouch method is true, set result to true.
     */
    if (li != null && li.mOnTouchListener != null
            && (mViewFlags & ENABLED_MASK) == ENABLED
            && li.mOnTouchListener.onTouch(this, event)) {
        result = true;
    }
  
    /** 
     * If result is false, its onTouchEvent is called.
     * If the onTouchEvent return value is true, set result to true.
     */
    if (!result && onTouchEvent(event)) {
        result = true;
    }
}
...
return result;
//Copy code

}If the source code is still too long, the pseudo code implementation should be as follows (omit some security judgments), simple and crude:

public boolean dispatchTouchEvent(MotionEvent event) { if

(mOnTouchListener.onTouch(this, event)) { return true; } else if (onTouchEvent(event)) {

return true; } return false; }

Just when you are addicted to the "subtle" logic of the source code, you may not find two things missing. When you get back to your senses, take a look. Oops, where are OnClick and OnLongClick?

Don't worry. The specific call locations of OnClick and OnLongClick are in onTouchEvent. Look at the source code (also omit a lot of irrelevant code):

public boolean onTouchEvent(MotionEvent event) {
...
final int action = event.getAction();
// Check various clickable
if (((viewFlags & CLICKABLE) == CLICKABLE ||
        (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
        (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
    switch (action) {
        case MotionEvent.ACTION_UP:
            ...
            removeLongPressCallback();  // Remove long press
            ...
            performClick();             // Check Click
            ...
            break;
        case MotionEvent.ACTION_DOWN:
            ...
            checkForLongClick(0);       // Inspection long press
            ...
            break;
        ...
    }
    return true;                        // ◀︎ indicates the event is consumed
}
return false;
//Copy code

}

Note that there is a return true in the above code, and as long as the View can be clicked, it returns true, indicating that the event has been consumed.

I have a RelativeLayout, I have a View, Ugh, RelativeLayout - View

<RelativeLayout android:background="#CCC" android:id="@+id/layout" android:onClick="myClick"

android:layout_width="200dp" android:layout_height="200dp"> <View android:clickable="true"

android:layout_width="200dp" android:layout_height="200dp" />

Now that you have a RelativeLayout - View, you are happy to be RelativeLayout

Set a click event, myClick. However, you will find that no matter how you click, you will not receive information. Look carefully and find that there is an attribute android:clickable="true" in the internal View. It is this seemingly inconspicuous attribute that consumes the event. Therefore, we can draw the following conclusions:

  1. Regardless of whether the View itself registers click events, as long as the View is clickable, it will consume events.
  2. Whether an event is consumed is determined by the return value. true indicates consumption and false indicates no consumption, regardless of whether an event is used.

Let's talk about the event distribution of View. Let's take a look at the event distribution of ViewGroup.

ViewGroup related

The event distribution of ViewGroup (usually all kinds of layouts) is relatively troublesome, because ViewGroup should not only consider itself, but also consider all kinds of childviews. If it can't handle it well, it is easy to cause all kinds of event conflicts, which is the so-called "foster parents know it hard".

1. Judge whether you need it (ask if oninterceptouchevent intercepts it). If you need it, call your own onTouchEvent.

2. Ask ChildView if you don't need or are not sure. Generally speaking, call ChildView where your fingers touch.

3. If the child ChildView does not need to call its onTouchEvent.

The pseudo code should be as follows:

public boolean dispatchTouchEvent(MotionEvent ev) {
boolean result = false;             // The default state is no consumption

if (!onInterceptTouchEvent(ev)) {   // If there is no interception, give it to the child View.
    result = child.dispatchTouchEvent(ev);
}

if (!result) {                      // If the event is not consumed, ask yourself onTouchEvent
    result = onTouchEvent(ev);
}

return result;
}
//Copy code

Some people may have questions here. I've read the source code. There are more than 200 lines of dispatchTouchEvent in ViewGroup. If you make these lines, you want to fool me. Don't think I read less.

Of course, the above source code is not perfect, and there are many problems that have not been solved, such as:

  1. There may be multiple childviews in the ViewGroup. How to determine which one to assign?

This is very easy, that is, to traverse all childviews. If the points touched by fingers are in the ChildView area, they will be distributed to this View.

  1. How should childviews of this point be allocated when they overlap?

When the ChildView overlaps, it is usually assigned to the ChildView displayed at the top.

How to judge which is on the top? Later loaded ones usually overwrite the previous ones, so the top one is the last one. As follows:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" 
android:id="@+id/activity_main"
android:layout_width="match_parent" 
android:layout_height="match_parent"
tools:context="com.gcssloop.viewtest.MainActivity">
<View
    android:id="@+id/view1"
    android:background="#E4A07B"
    android:layout_width="200dp"
    android:layout_height="200dp"/>
<View
    android:id="@+id/view2"
    android:layout_margin="100dp"
    android:background="#BDDA66"
    android:layout_width="200dp"
    android:layout_height="200dp"/>
</RelativeLayout>
//Copy code

When the fingers click on the overlapped area, it can be divided into the following situations:

When only View1 is clickable, the event will be assigned to View1. Even if it is blocked by View2, this part is still the clickable area of View1.

When only View2 is clickable, events are assigned to View2.

When both View1 and view2 can be clicked, the event will be assigned to the post loaded view2. View2 will consume the event and View1 will not receive the event.

Be careful:

The above is clickable. Clickable includes many situations, as long as you register with View

Any listener of onClickListener, onLongClickListener, OnContextClickListener or android:clickable="true" indicates that the View is clickable.

In addition, some views are clickable by default, such as Button, CheckBox, etc.

Registering an OnTouchListener with a View does not affect the clickable state of the View. Even if the OnTouchListener is registered with View, the event will not be consumed as long as it does not return true.

  1. ViewGroup and ChildView register event listeners (onClick, etc.) at the same time. Which one will execute?

The event is given priority to ChildView. It will be consumed by ChildView and ViewGroup will not respond.

  1. All events should be consumed by the same View

In the above example, we can understand that the same click event can only be viewed by one View.

Why consumption? It is mainly to prevent event response confusion. If different events are assigned to different views in a complete event again, it is easy to cause event response confusion.

(the onClick event in the View needs to receive action "down" and action "up" at the same time to trigger. If it is assigned to different views, the onClick event will not be triggered correctly.)

In order to ensure that all events are consumed by one View, Android makes a special judgment on the first event (action [down]. Only when the action [down] event is consumed by View, can View receive subsequent events (clickable control will consume all events by default), and will pass all subsequent events, and will not pass them to other views, unless the upper View does. Interception.

If the upper level View intercepts the event currently being processed, it will receive an action "Cancel", indicating that the current event has ended and subsequent events will not be delivered.

Source code:

In fact, if you can understand the above content, you can use event distribution without looking at the source code, but more content can be mined out of the source code.

public boolean dispatchTouchEvent(MotionEvent ev) {
// Tune trial
if (mInputEventConsistencyVerifier != null) {
    mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}

// Judge whether the event is aimed at the accessible focus view (content added late, personal guess is related to screen assistance, and it is convenient for blind people to use the device)
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
    ev.setTargetAccessibilityFocus(false);
}

boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
    final int action = ev.getAction();
    final int actionMasked = action & MotionEvent.ACTION_MASK;

    // Process the first action menu down.
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        // Clear all previous states
        cancelAndClearTouchTargets(ev);
        resetTouchState();
    }

    // Check if interception is required.
    final boolean intercepted;
    if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        if (!disallowIntercept) {
            intercepted = onInterceptTouchEvent(ev);    // Ask if you want to intercept
            ev.setAction(action);                         // Restore operations to prevent changes
        } else {
            intercepted = false;
        }
    } else {
          // There is no target to handle this event, and it is not a new event event (action? Down) to intercept.
        intercepted = true;
    }

      // Determine whether the event is for an accessible focus view
    if (intercepted || mFirstTouchTarget != null) {
        ev.setTargetAccessibilityFocus(false);
    }

    // Check whether the event is cancelled.
    final boolean canceled = resetCancelNextUpFlag(this)
            || actionMasked == MotionEvent.ACTION_CANCEL;

    final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
    TouchTarget newTouchTarget = null;
    boolean alreadyDispatchedToNewTouchTarget = false;
      
      // If not cancelled or blocked (enter event distribution)
    if (!canceled && !intercepted) {

        // If the event is for the accessibility focus view, we provide it to the view with the accessibility focus.
          // If it doesn't handle it, we clear the flag and dispatch the event to all childviews as usual. 
        // We detect and avoid maintaining this state because these things are very rare.
        View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                ? findChildWithAccessibilityFocus() : null;

        if (actionMasked == MotionEvent.ACTION_DOWN
                || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
            final int actionIndex = ev.getActionIndex();
            final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                    : TouchTarget.ALL_POINTER_IDS;

            // Clear the early touch target of this pointer ID to prevent out of sync.
            removePointersFromTouchTargets(idBitsToAssign);

            final int childrenCount = mChildrenCount;
            if (newTouchTarget == null && childrenCount != 0) {
                final float x = ev.getX(actionIndex);    // Get touch position coordinates
                final float y = ev.getY(actionIndex);
                // Find ChildView that can accept events
                final ArrayList<View> preorderedList = buildOrderedChildList();
                final boolean customOrder = preorderedList == null
                        && isChildrenDrawingOrderEnabled();
                final View[] children = mChildren;
                  // ▼ note, scan forward from the end
                for (int i = childrenCount - 1; i >= 0; i--) {
                    final int childIndex = customOrder
                            ? getChildDrawingOrder(childrenCount, i) : i;
                    final View child = (preorderedList == null)
                            ? children[childIndex] : preorderedList.get(childIndex);

                    // If there is a view with accessibility focus, we want it to get events first.
                      // If not, we will perform normal dispatch. 
                      // Although this may be distributed twice, it ensures a more secure execution at a given time.
                    if (childWithAccessibilityFocus != null) {
                        if (childWithAccessibilityFocus != child) {
                            continue;
                        }
                        childWithAccessibilityFocus = null;
                        i = childrenCount - 1;
                    }

                      // Check that the View allows events to be accepted (i.e. visible or animating)
                      // Check if the touch position is in the View area
                    if (!canViewReceivePointerEvents(child)
                            || !isTransformedTouchPointInView(x, y, child, null)) {
                        ev.setTargetAccessibilityFocus(false);
                        continue;
                    }

                      // getTouchTarget determines whether child is included in mFirstTouchTarget
                      // If target is returned, if null is not returned 
                    newTouchTarget = getTouchTarget(child);
                    if (newTouchTarget != null) {
                        // ChildView is ready to accept events within its region.
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                        break;    // ◀︎ the target View has been found, jump out of the loop
                    }

                    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);
                }
                if (preorderedList != null) preorderedList.clear();
            }

            if (newTouchTarget == null && mFirstTouchTarget != null) {
                // ChildView receive event not found
                newTouchTarget = mFirstTouchTarget;
                while (newTouchTarget.next != null) {
                    newTouchTarget = newTouchTarget.next;
                }
                newTouchTarget.pointerIdBits |= idBitsToAssign;
            }
        }
    }

    // Distribute TouchTarget
    if (mFirstTouchTarget == null) {
        // Without TouchTarget, treat the current ViewGroup as a normal View.
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
                TouchTarget.ALL_POINTER_IDS);
    } else {
        // Distribute TouchTarget, and avoid assigning to new targets if we have already done so. 
          // If necessary, cancel the distribution.
        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;
                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 necessary, update the pointer's touch target list or cancel.
    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 handled;
}
//Copy code

Core points

Principle of event distribution: responsibility chain mode, events are delivered layer by layer until they are consumed.

View's dispatchTouchEvent is mainly used to schedule its own listener and onTouchEvent.

The scheduling order of events in View is ontouchlistener > ontoucheevent > onlongclicklistener > onclicklistener.

Regardless of whether the View itself registers click events, as long as the View is clickable, it will consume events.

Whether an event is consumed is determined by the return value. true indicates consumption and false indicates no consumption, regardless of whether an event is used.

When there may be more than one ChildView in the ViewGroup, assign the event to the ChildView containing the click location.

ViewGroup and ChildView register event listeners (onClick, etc.) at the same time, which are consumed by ChildView.

Events generated in a touch process should be consumed by the same View, all received or all rejected.

As long as you accept action "down", you will accept all events. If you refuse action "down", you will not receive subsequent content.

If the current event being processed is blocked by the upper View, an action "Cancel" will be received, and subsequent events will not be delivered.

Topics: Android Attribute less