This article is from: 103Style blog

“Android Development art Exploration” learning record

base on Android-29

The relevant source code has been deleted

Format adjustment, correction of some typos — updated at 2019/12/11 21:12


You can approach this article with the following questions:

  • The main method of event distribution?
  • How does an event pass from an Activity to the corresponding clicked View?
  • When is ACTION_CANCEL called?
  • How does a ViewGroup find the child element that handles the event?
  • What is the difference between OnTouchListener and OnClickListener? In which of the three main methods of event distribution?
  • What is the trigger condition for OnClickListener and when?

If you know all of the above, you’re awesome!


directory

  • Event Distribution Flow Chart
  • Three main related methods and some conclusions are introduced
  • The test verifies the event passing process between Avtivity, ViewGroup, and View
  • Source code parsing for event distribution
    • Acitivity event distribution source code parsing
    • ViewGroup event distribution source code analysis
    • View event distribution source code analysis
  • Answer to a question

Event Distribution Flow Chart

Here are two general flow charts:


An introduction to three main related methods

Click event distribution is accomplished in three important ways:

public boolean dispatchTouchEvent(MotionEvent ev)

Used for event distribution. If the event is passed to the current View, this method must be called, and the return result is affected by the current View’s onTouchEvent and the lower View’s dispatchTouchEvent, indicating whether the event is consumed.


public boolean onInterceptTouchEvent(MotionEvent ev)

ViewGroup method that determines whether to block an event. If the current method intercepts an event, the method does not call back in the same sequence of events. The result is returned indicating whether the current event is intercepted.


public boolean onTouchEvent(MotionEvent event)

Called within dispatchTouchEvent to handle the click event and returns a result indicating whether the current event is consumed. If not, the current View cannot receive the event again within the same sequence of events.


The pseudo-code for the relationship between these three methods is as follows:

public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean consume = false;
    if (onInterceptTouchEvent(ev)) {
        consume = onTouchEvent(ev);
    } else {
        consume = child.dispatchTouchEvent(ev);
    }
    return consume;
}
Copy the code

Sample validation

For reasons of space, the example verification process is documented here: An example of the Android event distribution mechanism verification, which can be viewed in the original article. You should pay attention to the log information printed in the log, and take a look at the event distribution process.


The source code parsing

Avtivity

Let’s start directly with the dispatchTouchEvent from the entrance Avtivity:

//Avtivity.java
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}
public void onUserInteraction() {}Copy the code

As you can see, the onUserInteraction method is an empty implementation by default, so we continue with getWindow().superdispatchTouchEvent (ev),

public class Activity extends ContextThemeWrapper .... { final void attach(...) {... mWindow = new PhoneWindow(this, window, activityConfigCallback); . } public WindowgetWindow() {
        returnmWindow; }}Copy the code

We know that getWindow returns a PhoneWindow, so let’s continue looking at PhoneWindow:

//PhoneWindow.java
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}
private void installDecor() {
    if(mDecor == null) { mDecor = generateDecor(-1); . } protected DecorView generateDecor(int featureId) { ...return new DecorView(context, featureId, this, getAttributes());
}
Copy the code

And the mDecor here we can see in The General decor (Int featureId) returns a DecorView, It inherits from FrameLayout, and the superDispatchTouchEvent method calls the dispatchTouchEvent of the ViewGroup.

//DecorView.java
public class DecorView extends FrameLayout implements ...
public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
}
Copy the code

Then return to the Activity’s dispatchTouchEvent method. When getWindow().superDispatchTouchEvent(ev) returns true, The onTouchEvent method in the Activity is no longer called.

You can see this in the example above by comparing the DOWN event in the log with the TestView setting click event only.


ViewGroup

Then the next to a ViewGroup dispatchTouchEvent method, temporarily hide first meet onFilterTouchEventForSecurity (ev) conditional logic:

public boolean dispatchTouchEvent(MotionEvent ev) {
    if(mInputEventConsistencyVerifier ! = null) { mInputEventConsistencyVerifier.onTouchEvent(ev, 1); }if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
        ev.setTargetAccessibilityFocus(false);
    }
    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
       ....
    }
    if(! handled && mInputEventConsistencyVerifier ! = null) { mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1); }return handled;
}
Copy the code

First of all, we saw a mInputEventConsistencyVerifier, the beginning and end are also in the AS is red, we can see him in the View class initialization:

//View.java
//Consistency verifier for debugging purposes.
protected final InputEventConsistencyVerifier mInputEventConsistencyVerifier =
        InputEventConsistencyVerifier.isInstrumentationEnabled() ?
                new InputEventConsistencyVerifier(this, 0) : null;

//InputEventConsistencyVerifier.java
private static final boolean IS_ENG_BUILD = Build.IS_ENG;
public static boolean isInstrumentationEnabled() {
    return IS_ENG_BUILD;
}

//Build.java
public static final boolean IS_ENG = "eng".equals(TYPE);
/** The type of build, like "user" or "eng". */
public static final String TYPE = getString("ro.build.type");
Copy the code

The value isInstrumentationEnabled() returns false, and the value isInstrumentationEnabled() returns false. All we get mInputEventConsistencyVerifier is null.

Where setTargetAccessibilityFocus behind choose child elements, if it is true, only to find child elements for focal point of View to deal with the event.

The dispatchTouchEvent method code can then be reduced to:

public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
       ....
    }
    return handled;
}
Copy the code

Then let’s look at the meet onFilterTouchEventForSecurity (ev), what was the logic to handle it.


The DOWN event resets the status

The first step is to reset the state in ACTION_DOWN,

final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
if (actionMasked == MotionEvent.ACTION_DOWN) {
    cancelAndClearTouchTargets(ev);
    resetTouchState();
}
Copy the code

Here, the ViewGroup is reset to its initial state, FLAG_DISALLOW_INTERCEPT is cleared, and an ACTION_CANCEL event is triggered if an event was previously handled by a child element.

private void cancelAndClearTouchTargets(MotionEvent event) {
    if(mFirstTouchTarget ! = null) {for(TouchTarget target = mFirstTouchTarget; target ! = null; target = target.next) { dispatchTransformedTouchEvent(event,true, target.child, target.pointerIdBits);
        }
    }
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, ...) {
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL); 
        if(child ! // Distribute ACTION_CANCEL to the child element as handled = child.dispatchTouchEvent(event); } event.setAction(oldAction);returnhandled; }... }Copy the code

ViewGroup Event interception judgment

Then let’s look at the logic that determines whether to intercept an event:

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); }else {
        intercepted = false; }}else {
    intercepted = true;
}
Copy the code

So let’s first look at the condition, not to mention the DOWN event, but what is mFirstTouchTarget? MFirstTouchTarget! = mFirstTouchTarget! = mFirstTouchTarget! = mFirstTouchTarget! = mFirstTouchTarget! = null.

Conversely, as long as the ViewGroup intercepts the event, mFirstTouchTarget! = null; If intercepted = true, these events are handled by the ViewGroup instead of the child elements.

We saw there a FLAG_DISALLOW_INTERCEPT tag, this is the ViewGroup in child elements by calling requestDisallowInterceptTouchEvent (Boolean) to set up, Once a child element calls this method, the ViewGroup will not be able to intercept events other than DOWN. Why do you say “other than DOWN”? Since FLAG_DISALLOW_INTERCEPT is reset for DOWN events, the ViewGroup always uses onInterceptTouchEvent to determine whether or not to intercept a DOWN event.


The processing logic after the interception event of a ViewGroup

Then we can see that when the intercept and not ACTION_CANCEL, if no events before the hands of the child elements, is called directly dispatchTransformedTouchEvent, and the third parameter is null, This method is equivalent to calling dispatchTouchEvent directly.

final boolean canceled = resetCancelNextUpFlag(this)
        || actionMasked == MotionEvent.ACTION_CANCEL;
if(! canceled && ! intercepted) { .... }if (mFirstTouchTarget == null) {
    handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
} else{... } private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, ...) {// The condition here is equivalent to calling dispatchTouchEvent directly on the ViewGroupif (child == null) {
        returnsuper.dispatchTouchEvent(event); }}Copy the code

Now let’s look at mFirstTouchTarget! = null logic:

boolean alreadyDispatchedToNewTouchTarget = false;
if (mFirstTouchTarget == null) {
    ...
} 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; // Pass ACTION_CANCEL to the child element that handled the event in the same logic as when the DOWN event was resetif(dispatchTransformedTouchEvent(ev, cancelChild,target.child,...) ) { handled =true; }... } target = next; }}Copy the code

That is, the ACTION_CANCEL is passed to the child element that handled the event, the same logic as when the DOWN event was reset


ViewGroup does not intercept processing logic

Let’s start with if there are no child elements, you can see that the following code is equivalent to nothing.

if(! canceled && ! intercepted) {if (actionMasked == MotionEvent.ACTION_DOWN
            || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
        final int childrenCount = mChildrenCount;
        if(newTouchTarget == null && childrenCount ! = 0) {... }}}Copy the code

Then when there are child elements:

View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
        ? findChildWithAccessibilityFocus() : null;
if(newTouchTarget == null && childrenCount ! = 0) { finalfloat x = ev.getX(actionIndex);
    final float y = ev.getY(actionIndex);
    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); // Determine if the child element is in the click areaif(! child.canReceivePointerEvents() || ! isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false);
            continue; } resetCancelNextUpFlag(child); // Distribute events to child elementsif (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;
            }
            break;
        }
        newTouchTarget = addTouchTarget(child, idBitsToAssign);
        ev.setTargetAccessibilityFocus(false);
    }
    if(preorderedList ! = null) preorderedList.clear(); } private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) { final TouchTarget target = TouchTarget.obtain(child, pointerIdBits); target.next = mFirstTouchTarget; mFirstTouchTarget = target;return target;
}
Copy the code

Reverse traversal child elements, judge whether the coordinates of the event within the child elements, and then call dispatchTransformedTouchEvent, the inside of the logic to call child. DispatchTouchEvent (transformedEvent), Distribute events to child elements. If a child element handles an event, the index that records the current child element in the child element list is mLastTouchDownIndex. And then assign mFirstTouchTarget to this child element in addTouchTarget. The dispatchTransformedTouchEvent transformedEvent namely the do the position of the transform for animation, etc.

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, ...) {
    if (child == null) {
    } else {
        final float offsetX = mScrollX - child.mLeft;
        final float offsetY = mScrollY - child.mTop;
        transformedEvent.offsetLocation(offsetX, offsetY);
        if (! child.hasIdentityMatrix()) {
            transformedEvent.transform(child.getInverseMatrix());
        }
        handled = child.dispatchTouchEvent(transformedEvent);
    }
    transformedEvent.recycle();
    return handled;
}
Copy the code

From the above analysis, we can see that the final logic is equivalent to:

if (XXX) {
    handled = super.dispatchTouchEvent(event);
} else {
    handled = child.dispatchTouchEvent(event);
}
Copy the code

It may continue to call the Super.DispatchTouchEvent of the ViewGroup or the dispatchTouchEvent method of the View. Super. dispatchTouchEvent is the dispatchTouchEvent that calls the View.

Let’s look at the processing logic of the View’s dispatchTouchEvent method.


The View of dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent event) {
    boolean result = false;
    if (onFilterTouchEventForSecurity(event)) {
        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;
}
Copy the code

So here we can see that the OnTouchListener is set, and if it’s ENABLED, then the onTouch method that we set for OnTouchListener is called, and if we return true, DispatchTouchEvent returns true. If false is returned, the onTouchEvent method is called and the return value of dispatchTouchEvent is the value of onTouchEvent.

Let’s move on to the processing logic of onTouchEvent.


The View of onTouchEvent

Determine the state of the View
final int viewFlags = mViewFlags;
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
        || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
        || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

if ((viewFlags & ENABLED_MASK) == DISABLED) {
    if(action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) ! = 0) {setPressed(false);
    }
    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
    return clickable;
}
Copy the code

If DISABLED, returns whether it is currently clickable.

The default value of a View’s LONG_CLICKABLE property is false, and the value of the CLICKABLE property depends on whether the View is CLICKABLE. For example, a Button is CLICKABLE, so CLICKABLE is true. TextView is not CLICKABLE by default, so CLICKABLE is false.

SetOnLongClickListener and setOnLongClickListener can be used to change the corresponding properties, as shown in their source code:

public void setOnClickListener(@Nullable OnClickListener l) {
    if(! isClickable()) {setClickable(true);
    }
    getListenerInfo().mOnClickListener = l;
}
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
    if(! isLongClickable()) {setLongClickable(true);
    }
    getListenerInfo().mOnLongClickListener = l;
}
Copy the code

View whether the proxy is set

It then determines if the View has set the mTouchDelegate delegate, and if it does, it calls its onTouchEvent method, which works in much the same way as OnTouchListener, but I won’t go into that here.

if(mTouchDelegate ! = null) {if (mTouchDelegate.onTouchEvent(event)) {
        return true; }}Copy the code

Then there is the handling of the corresponding event.


Process the corresponding event

At this point, the event is consumed by the current View, but different events are handled differently. We focus on ACTION_UP events, which call performClickInternal().

public boolean onTouchEvent(MotionEvent event) {
    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
            caseMotionEvent.ACTION_UP: boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) ! = 0;if((mPrivateFlags & PFLAG_PRESSED) ! = 0 || prepressed) {if(! mHasPerformedLongPress && ! mIgnoreNextUpEvent) { ... // This is a tap, so remove the longpress check removeLongPressCallback();if(! focusTaken) {if (mPerformClick == null) {
                                mPerformClick = new PerformClick();
                            }
                            if(! post(mPerformClick)) { performClickInternal(); }}}... } mIgnoreNextUpEvent =false;
                break;
            case MotionEvent.ACTION_DOWN:
                ...
                break;
            case MotionEvent.ACTION_CANCEL:
                ...
                removeTapCallback();
                removeLongPressCallback();
                break;
            case MotionEvent.ACTION_MOVE:
                ...
                break;
        }
        return true;
    }
    return false;
}
Copy the code

The View of performClickInternal

This is to see if OnClickListener is set, and returns true, otherwise returns false.

private boolean performClickInternal() {
    return performClick();
}
public boolean performClick() {
    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;
    }
    return result;
}
Copy the code

So if we wanted to customize the click event, we could just override performClick.

Here, the relevant process is basically clear, we look back at the beginning of the two flow charts, in their own.


Answer to a question

  • Q: The main method of event distribution?

    A: dispatchTouchEvent, onInterceptTouchEvent, onTouchEvent.

  • Q: What is the process of transferring events from an Activity to the corresponding clicked View?

    A: The default process is the Activity. The dispatchTouchEvent – ViewGroup. DispatchTouchEvent – ViewGroup. OnInterceptTouchEvent – > View.dispatchtouchevent → View.onTouchEvent → ViewGroup.onTouchEvent → activity.onTouchEvent.

  • Q: When is ACTION_CANCEL called?

    A: During an ACTION_DOWN event, if there is A child element that handled the event, an ACTION_CANCEL is created and passed to it. After the child consumes the DOWN event, the parent intercepts subsequent events, and when the subsequent event arrives it triggers ACTION_CANCEL to pass to the child that consumes the DOWN event.

  • Q: How does a ViewGroup find the child element that handles the event?

    A: reverse traversal child elements, first of all judge isTargetAccessibilityFocus value, if it is true, then find the current child elements for the focal point of View; If false, the reverse order traversals until it finds a View position that handled the event.

  • Q: What is the difference between OnTouchListener and OnClickListener? In which of the three main methods of event distribution?

    A: If both are set OnTouchListener is called before OnClickListener, because OnTouchListener is called in the dispatchTouchEvent of the View, OnClickListener is called in onTouchEvent.

  • Q: What triggers OnClickListener and when?

    A: The current View must be click-free and OnClickListener must be set. PerformClick () is finally called on ACTION_UP. When you’re customizing a View, if you override onTouchEvent, the IDE will warn you that you’re overwriting performClick.


If there is a description wrong, please remind me, thank you!

The above

If you like it, please give it a thumbs up.


Scan the qr code below, follow my public account Android1024, click attention, don’t get lost.