Read the process of Android touchevent event event distribution, interception and processing

What is an event? Events are a series of touchevents caused by the user touching the mobile phone screen, including action_ DOWN、ACTION_ MOVE、ACTION_ UP、ACTION_ Cancel, etc. after these actions are combined, they become click events, long press events, etc.

In this article, log test is used to understand the event distribution, interception and processing process of Android touchevent. Although I have read some other articles, source codes and related materials, I still feel that I need to write down the log and draw pictures to understand, otherwise it is easy to forget the whole process of event transmission. So write this article so that after reading this article, you can basically understand the whole process and draw your own pictures for others to see.

Let's first look at several classes, mainly to draw an interface superimposed by three viewgroups, and mark the log during event distribution, interception and processing

GitHub address: https://github.com/libill/TouchEventDemo

Here, three viewgroups are added to an activity for analysis. It is worth noting that there is no onintercepttouchevent method for activity and view.

Add code that does not intercept and handle any events. See how events are passed. Select info and view log

As can be seen from the flowchart, event distribution starts from the activity and then to the ViewGroup. In this process, as long as the ViewGroup does not intercept processing, it will finally return to the ontouchevent method of the activity.

Modify the dispatchtouchevent of viewgroup2.java. Return returns true so that the event is not distributed

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
 Log.i(TAG,"dispatchTouchEvent    action:" + StringUtils.getMotionEventName(ev));
 Log.d(TAG,"onInterceptTouchEvent action:" + StringUtils.getMotionEventName(ev) + " " + true);
 return true;
}

Log at this time

As can be seen from the picture, when the dispatchtouchevent of viewgroupon2 returns true, the event will not be distributed to viewgroup3, nor will it be distributed to the ontouchevent of activity. Instead, the event stops when it reaches the dispatchtouchevent of viewgroupon2. Dispatchtouchevent returns true, indicating that the event does not need to be distributed.

Modify the onintercepttouchevent of viewgroup2.java, and return true to intercept the event

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    Log.i(TAG,"dispatchTouchEvent    action:" + StringUtils.getMotionEventName(ev));
    boolean superReturn = super.dispatchTouchEvent(ev);
    Log.d(TAG,"dispatchTouchEvent    action:" + StringUtils.getMotionEventName(ev) + " " + superReturn);
    return superReturn;
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    Log.i(TAG,"onInterceptTouchEvent action:" + StringUtils.getMotionEventName(ev));
    Log.d(TAG,"onInterceptTouchEvent action:" + StringUtils.getMotionEventName(ev) + " " + true);
    return true;
}

Log at this time

It can be seen that if viewgroup2 intercepts the event, it will not continue to be distributed to viewgroup3; Moreover, viewgroup3 intercepts the event and does not process the event. It will pass the event to the ontouchevent method of the activity.

Modify the ontouchevent of viewgroup2.java. Return returns true to handle the event

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    Log.i(TAG,"onInterceptTouchEvent action:" + StringUtils.getMotionEventName(ev) + " " + true);
    return true;
}

@Override
public boolean onTouchEvent(MotionEvent ev) {
    Log.i(TAG,"onTouchEvent          action:" + StringUtils.getMotionEventName(ev));
    Log.d(TAG,"onTouchEvent          action:" + StringUtils.getMotionEventName(ev) + " " + true);
    return true;
}

It can be concluded from the process that when the oninterceptotouchevent and ontouchevent of viewgroup2 return true, the event will eventually go to the ontouchevent method of viewgroup2 to process the event, and subsequent events will come here.

It is clear from the log analysis above. Is that enough? In fact, it's not enough. We have to analyze why events are distributed like this from the perspective of source code.

First look at the dispatchtouchevent under activity

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

Onuserinteraction method

public void onUserInteraction() {
}

You can learn from the code

GetWindow method of activity

public Window getWindow() {
    return mWindow;
}

How is mwindow assigned? It is assigned in the attach method of activity. In fact, mwindow is phonewindow.

Attach method of activity

final void attach(Context context,ActivityThread aThread,Instrumentation instr,IBinder token,int ident,Application application,Intent intent,ActivityInfo info,CharSequence title,Activity parent,String id,NonConfigurationInstances lastNonConfigurationInstances,Configuration config,String referrer,IVoiceInteractor voiceInteractor,Window window,ActivityConfigCallback activityConfigCallback) {
    attachBaseContext(context);

    mFragments.attachHost(null /*parent*/);

    mWindow = new PhoneWindow(this,window,activityConfigCallback);
    mWindow.setWindowControllerCallback(this);
    mWindow.setCallback(this);
    mWindow.setOnWindowDismissedCallback(this);
    mWindow.getLayoutInflater().setPrivateFactory(this);
	...
}

Superdispatchtouchevent method of phonewindow

private DecorView mDecor;

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}

Superdispatchtouchevent of devorview

public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
}

Mdecor is a decorview that inherits FrameLayout, so events are distributed to 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;
        }

There are two situations to judge whether to intercept, that is, when a certain condition is true, onintercepttouchevent will be executed to judge whether to intercept the event.

In another case, when disallowintercept is true, intercepted is directly assigned false and will not be intercepted. FLAG_ DISALLOW_ Interpt is set through the requestdisallowintercepttouchevent method. It is used to set it in the child view. After setting, ViewGroup can only intercept down events, but cannot intercept other move, up and cancel events. Why can ViewGroup intercept down events? Because the ViewGroup is reset during the down event, look at the following code

// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
    // Throw away all prevIoUs state when starting a new touch gesture.
    // The framework may have dropped the up or cancel event for the prevIoUs gesture
    // due to an app switch,ANR,or some other state change.
    cancelAndClearTouchTargets(ev);
    resetTouchState();
}

private void resetTouchState() {
    clearTouchTargets();
    resetCancelNextUpFlag(this);
    mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    mNestedScrollAxes = SCROLL_AXIS_NONE;
}

It can be learned from the source code that after the ViewGroup intercepts the event, it will no longer call onintercepttouchevent, but will be directly handed over to the ontouchevent of mfirsttouchtarget for processing. If the ontouchevent is not processed, it will eventually be handed over to the ontouchevent of activity.

When the ViewGroup does not intercept events, it will traverse the sub view to distribute the events to the sub 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 (!canViewReceivePointerEvents(child)
            || !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,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;
    }
}

Judge whether the sub view can receive click events through canviewreceivepointerevents. Two conditions must be met, one of which is indispensable: 1. The coordinate of the click event falls in the area of the sub view; 2. The child view is not playing an animation. After satisfying the condition, calling dispatchTransformedTouchEvent is actually the dispatchTouchEvent calling the sub View.

private static boolean canViewReceivePointerEvents(@NonNull View child) {
    return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
            || child.getAnimation() != null;
}

protected boolean isTransformedTouchPointInView(float x,float y,View child,PointF outLocalPoint) {
    final float[] point = getTempPoint();
    point[0] = x;
    point[1] = y;
    transformPointToViewLocal(point,child);
    final boolean isInView = child.pointInView(point[0],point[1]);
    if (isInView && outLocalPoint != null) {
        outLocalPoint.set(point[0],point[1]);
    }
    return isInView;
}

private boolean dispatchTransformedTouchEvent(MotionEvent event,boolean cancel,int desiredPointerIdBits) {
    final boolean handled;
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }

    ...

    // Perform any necessary transformations and dispatch.
    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } 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);
    }

    // Done.
    transformedEvent.recycle();
    return handled;
}

When dispatchtransformedtouchevent returns true, end the for loop traversal and assign a value of newtouchtarget, which is equivalent to finding a view that can receive events. There is no need to continue looking.

newTouchTarget = addTouchTarget(child,idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;

Assign mfirsttouchtarget in the addtouchtarget method.

private TouchTarget addTouchTarget(@NonNull View child,int pointerIdBits) {
    final TouchTarget target = TouchTarget.obtain(child,pointerIdBits);
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;
}

In another case, when mfirsttouchtarget is empty, ViewGroup handles the event itself. Note here that the third parameter is null, and ViewGroup's super.dispatchtouchevent will call view's dispatchtouchevent.

if (mFirstTouchTarget == null) {
    // No touch targets so treat this as an ordinary view.
    handled = dispatchTransformedTouchEvent(ev,canceled,null,TouchTarget.ALL_POINTER_IDS);
}

How does the dispatchtouchevent of view handle events?

public boolean dispatchTouchEvent(MotionEvent event) {
    boolean result = false;
	...
    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;
}

When the view is unavailable, the event will still be processed, but it seems unavailable.

Then execute mtouchdelegate.ontouchevent

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

Let's see how the up event is handled

/**
 * <p>Indicates this view can display a tooltip on hover or long press.</p>
 * {@hide}
 */
static final int TOOLTIP = 0x40000000;

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

                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;
            ...
    }

    return true;
}

As can be seen from the above code, when one of clickable and tooltip is true, the event will be consumed and ontouchevent will return true. The performclick method is called internally by performclick.

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

If the view has onclicklistener set, performclick will call the internal onclick method.

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

Set clickable through setonclicklistener and long through setonlongclicklistener_ Clickable long press event. After setting, ontouchevent returns true. Here we have analyzed the distribution process of click events.

Address: http://libill.github.io/2019/09/09/android-touch-event/

This paper refers to the following contents:

1. Exploration of Android development Art

The content of this article comes from the network collection of netizens. It is used as a learning reference. The copyright belongs to the original author.
THE END
分享
二维码
< <上一篇
下一篇>>