Event system of View -- event distribution mechanism of View

Keywords: Mobile Android

Time system of View -- event distribution mechanism of View

1. What is event distribution?

  • Complete the whole interaction process by touching the screen with fingers
  • When the user interacts with the mobile phone through the screen, every click, long press, move, etc. is an event
  • Event distribution mechanism: an event is passed from the screen to each View. The View can use this event (consumption event) or ignore this event (non consumption event), which is the control of the whole process.

2. Object of event distribution

  • The system encapsulates events as MotionEvent objects, and the process of event distribution is the process of MotionEvent distribution.

a. Type of event

  • Press (action? Down)
  • Action "move" occurs after pressing, excluding pressing action
  • Action? Up
  • Action? Cancel

b. Event sequence

  • A series of events from the time the finger presses the screen to the time the finger leaves the screen

c. Transmission level

Activity->Window->DecorView->ViewGroup->View

3. Three main methods of event distribution

  • dispatchTouchEvent(MotionEvent ev)

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

  • onInterceptTouchEvent(MotionEvent event)

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

  • onTouchEvent(MotionEvent event)

    In the dispatchTouchEvent method, it is used to handle clicking events and return results to indicate whether the current event is consumed. If it is not consumed, the current View cannot receive events again in the same event sequence.

4. Event distribution analysis

1. Distribution process of activity to click event

  • When a click operation occurs, the time is first passed to the current Activity, so the analysis begins with the Activity's dispatchTouchEvent() method.

Activity.dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}
  • First, the event is handed over to the Window attached to the Activity for distribution

[failed to transfer the image from the external link. The source station may have anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-mo61v08y-1580537188567)(C:\Users \ big wolfhound SKR ~ \ appdata \ roaming \ typora \ typora user images \ 1580528429119. PNG))

  • Through the source code, we know that Window is an abstract class, and PhoneWindow is the implementation class of Window.

PhoneWindow.superDispatchTouchEvent

public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}
  • Because the only implementation class of Window is PhoneWindow, let's see PhoneWindow handle the click event

  • PhoneWindow passed the event directly to DecorView

DecorView.superDispatchEvent

public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
}
  • Through ((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0), you can get the View set by Activity. This mDecor is the View returned by getWindow().getDecorView(), and the View we set through setContentView() is one of its child views.
  • At present, the event is passed to the DecorView, which inherits from the FrameLayout and is the parent View, so the event will finally be passed to the View
  • From here, the event has been passed to the top-level View, that is, the View set in the Activity through setContentView. The top-level View is generally the ViewGroup

2. Distribution process of click event by ViewGroup

// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) {
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (!disallowIntercept) {
        intercepted = onInterceptTouchEvent(ev);
        ev.setAction(action); // restore action in case it was changed
    } else {
        intercepted = false;
    }
} else {
    // There are no touch targets and this action is not an initial down
    // so this view group continues to intercept touches.
    intercepted = true;
}
  • As can be seen from the above code, the event type of ViewGroup is DOWN or mFirstTarget! =If null, the current event will be blocked.
  • mFirstTarget: when an event is successfully processed by a child element of the ViewGroup, mFirstTarget is assigned and points to the child element. mFirstTarget!=null when ViewGroup does not intercept events and hands them over to child elements. mFirstTarget!=null does not work once the event is intercepted by the current ViewGroup. When the action 〝 move and action 〝 up events arrive, if the condition (actionmasked = = motionevent. Action 〝 down | mfirsttouchtarget! = null) is false, the intercepted = true will be directly set
  • Flag "disallow" intersect: this flag bit is set by the requestdisallowainterceptouchevent method of the child View. Once flag ﹣ disallow ﹣ intersect is set, ViewGroup will not be able to intercept click events other than action ﹣ down.
public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
            && ev.getAction() == MotionEvent.ACTION_DOWN
            && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
            && isOnScrollbarThumb(ev.getX(), ev.getY())) {
        return true;
    }
    return false;
}
  • Onintercepttauchevent returns false by default, that is to say, ViewGroup generally does not intercept

Summary: when the ViewGroup decides to intercept the event, the subsequent click event will be handed over to it by default and its oninterceptouchevent method will not be called. The flag "disallow" intersect flag is used to make the ViewGroup no longer intercept the event, provided that the ViewGroup does not intercept the action "down event (because when the ViewGroup judges that the event is action" down ", the flag will be reset).

Next, when the ViewGroup does not intercept the event, the event will be distributed down to its child View for processing

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 there is a view that has accessibility focus we want it
        // to get the event first and if not handled we will perform a
        // normal dispatch. We may do a double iteration but this is
        // safer given the timeframe.
        if (childWithAccessibilityFocus != null) {
            if (childWithAccessibilityFocus != child) {
                continue;
            }
            childWithAccessibilityFocus = null;
            i = childrenCount - 1;
        }

        if (!child.canReceivePointerEvents()
                || !isTransformedTouchPointInView(x, y, child, null)) {
            ev.setTargetAccessibilityFocus(false);
            continue;
        }

        newTouchTarget = getTouchTarget(child);
        if (newTouchTarget != null) {
            // Child is already receiving touch within its bounds.
            // Give it the new pointer in addition to the ones it is handling.
            newTouchTarget.pointerIdBits |= idBitsToAssign;
            break;
        }

        resetCancelNextUpFlag(child);
        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
            // Child wants to receive touch within its bounds.
            mLastTouchDownTime = ev.getDownTime();
            if (preorderedList != null) {
                // childIndex points into presorted list, find original index
                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;
        }

        // The accessibility focus didn't handle the event, so clear
        // the flag and do a normal dispatch to all children.
        ev.setTargetAccessibilityFocus(false);
    }
    if (preorderedList != null) preorderedList.clear();
}
  • Traverse all the child elements of the ViewGroup, and then determine whether the child element can receive the click event. Whether the click event can be accepted is mainly measured by two points: whether the sub element is playing the animation and whether the coordinate position of the click event falls within the sub element area. If a child element satisfies both conditions, the event is passed to it for processing.
  • dispatchTransformedTouchEvent actually calls the dispatchTouchEvent method of the child element, which has the following contents inside
if (child == null) {
    handled = super.dispatchTouchEvent(event);
} else {
    handled = child.dispatchTouchEvent(event);
}
  • If the child element's dispatchTouchEvent method returns true, mFirstTarget will be assigned and jump out of the for loop
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;

The real assignment process of mFirstTarget is completed in addTouchTarget. Whether mFirstTarget is assigned or not will directly affect the event interception of ViewGroup

private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
    final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;
}
  • These lines of code complete the assignment of mFirstTarget and terminate the traversal of child elements. If the child element dispatchTouchEvent returns false, the ViewGroup distributes the event to the next child element.

  • If the event is not properly handled after traversing all the child elements, there are two situations: the first is that the ViewGroup has no child elements; the second is that the child element handles the click event, but returns false in the dispatchTouchEvent, because the onTouchEvent of the child element returns false. In both cases, the view group handles the click event itself.

if (mFirstTouchTarget == null) {
    // No touch targets so treat this as an ordinary view.
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
} 
  • At this time, if the third parameter of dispatchTransformedTouchEvent is null, super.dispatchTouchEvent(event) will be called and submitted to View for processing.

3. View processing of click events

public boolean dispatchTouchEvent(MotionEvent event) {

	......
	
	
    if (onFilterTouchEventForSecurity(event)) {
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }

        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }
    
    ......
    
    
    return result;
}
  • View handles the click event simply. The view here does not contain the ViewGroup, because it has no child elements, so it cannot pass the event down.
  • First, it will determine whether OnTouchListener is set. If OnTouchListener's onTouch method returns true, onTouchEvent will not be called. It can be seen that OnTouchListener has a higher priority than onTouchEvent

onTouchEvent

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;
}
  • The comments in the code are translated as follows:

    Clickable disabled views still consume touch events, but do not respond. That is to say, when the view is not available, the click event will still be consumed.

  • If the view has an agent set, the onTouchEvent method of TouchDelegate is executed

if (mTouchDelegate != null) {
    if (mTouchDelegate.onTouchEvent(event)) {
        return true;
    }
}
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;
            }
            boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
            if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                // take focus if we don't have it already and we should in
                // touch mode.
                boolean focusTaken = false;
                if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                    focusTaken = requestFocus();
                }

                if (prepressed) {
                    // The button is being released before we actually
                    // showed it as pressed.  Make it show the pressed
                    // state now (before scheduling the click) to ensure
                    // the user sees it.
                    setPressed(true, x, y);
                }

                if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                    // This is a tap, so remove the longpress check
                    removeLongPressCallback();

                    // Only perform take click actions if we were in the pressed state
                    if (!focusTaken) {
                        // Use a Runnable and post this rather than calling
                        // performClick directly. This lets other visual state
                        // of the view update before click actions start.
                        if (mPerformClick == null) {
                            mPerformClick = new PerformClick();
                        }
                        if (!post(mPerformClick)) {
                            performClickInternal();
                        }
                    }
                }
            }
            mIgnoreNextUpEvent = false;
            break;
            
            ......
        }
        return true;
    }
  • As long as one of View's CLICKABLE and long'CLICKABLE is true, it will consume this event, that is, onTouchEvent returns true, regardless of whether it is disabled or not.

  • When action UUP occurs, performClick method will be triggered. If View has OnClickListener set, its onClick method will be called internally.

public boolean performClick() {
    // We still need to call this method to handle the cases where performClick() was called
    // externally, instead of through performClickInternal()
    notifyAutofillManagerOnClick();

    final boolean result;
    final ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);
        result = true;
    } else {
        result = false;
    }

    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

    notifyEnterOrExitForAutoFillIfNeeded(true);

    return result;
}
  • The LONG_CLICKABLE property of view is false by default. Whether the CLICKABLE property is false is related to the specific view. Specifically, CLICKABLE view has a CLICKABLE property of true and non CLICKABLE view has a CLICKABLE property of false. setClickable and setLongClickable can change their properties respectively. In addition, setOnClickListener will automatically set the CLICKABLE property of view to true, and setOnLongClickListener is the same.

Published 27 original articles, won praise 8, visited 1190
Private letter follow

Posted by boardy on Sat, 01 Feb 2020 00:18:44 -0800