The introduction

Android event dispatchTouchEvent () onInterceptTouchEvent() onTouchEvent() Which method will be called if it returns true/false? Without a fundamental understanding of the implementation mechanism of this piece, the actual use of the time is still confused. From the source code analysis, understand the code logic which can be flexibly used to solve practical problems, so much nonsense, into the theme.

Event distribution in View

By default, the View is the endpoint of the event distribution consumption. Let’s take a look at the source code to see how the View receives the event. There are two main methods related to the event distribution: dispatchTouchEvent() and onTouch(). So let’s see what dispatchTouchEvent() does in the View;

DispatchTouchEvent () and onTouchEvent() return values
  • Public Boolean dispatchTouchEvent() : By default, this method dispatchTouchEvent is sent to the target view, and the target view may be itself. If true, the target view to consume the event was found, and the event was consumed. If false, it was not found
  • OnTouchEvent () : This method handles consume touch events if the event is consumed and false if the event is not consumed
  • A complete touch consists of the three most important events: press, slide, and lift. When a View group does event distribution, the first thing it receives is a press, When ACTION_DOWN is not distributed successfully, that is, the view of the event to be consumed is not found, the ACTION_MOVE and ACTION_UP after the whole touch event will not be distributed

Without delay on the code, the key place added my SAO qi Chinese notes

The dispatchTouchEvent() method in the View

public boolean dispatchTouchEvent(MotionEvent event) {

        boolean result = false;
        if(mInputEventConsistencyVerifier ! = null) { mInputEventConsistencyVerifier.onTouchEvent(event, 0); } final int actionMasked = event.getActionMasked();if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }

        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true; } //Flag1: If OnTouchListener is set and the onTouch() method in OnTouchListener listener is set totrueSet result totrue
            if(li ! = null && li.mOnTouchListener ! = null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result =true; } //Flag2: if the result of the previous step isfalse, execute the view's own onTouchEvent(MotionEvent E) method and set result totrueIf onTouchEvent(e) returnstrue
            if(! result && onTouchEvent(event)) { result =true; }}if(! result && mInputEventConsistencyVerifier ! = null) { mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); }if(actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_CANCEL || (actionMasked == MotionEvent.ACTION_DOWN && ! result)) { stopNestedScroll(); }return result;
    }Copy the code

Read the above code, you can know;

  • Normally when we set OnTouchListener to a view which is a touch listener we will execute the onTouch () method in our touch listener first
  • The onTouchEvent() method is executed when the onTouch() method returns false, so the onTouchEvent method is not executed when the onTouch() method returns true.

The View of the onTouchEvent ()

So once we’ve separated the View’s onDispatchTouchEvent method, let’s look at the View’s onTouchEvent method

public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();

        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if(action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) ! = 0) {setPressed(false); } //Flag3: Disabled view and clickable still consumes the event, but does not handle it.return (((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
        }
        if(mTouchDelegate ! = null) {if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }
//Flag4
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                caseMotionEvent.ACTION_UP: boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) ! = 0;if((mPrivateFlags & PFLAG_PRESSED) ! = 0 || prepressed) { boolean focusTaken =false;
                        if(isFocusable() && isFocusableInTouchMode() && ! isFocused()) { focusTaken = requestFocus(); }if (prepressed) {

                            setPressed(true, x, y);
                       }

                        if(! mHasPerformedLongPress && ! mIgnoreNextUpEvent) { // This is a tap, so remove the longpress check removeLongPressCallback(); // Only perform take click actionsif 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();
                                }
//Flag4:
                                if(! post(mPerformClick)) { performClick(); }}}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;

                case MotionEvent.ACTION_DOWN:
                    mHasPerformedLongPress = false;

                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }


                    boolean isInScrollingContainer = isInScrollingContainer();

                    // For views inside a scrolling container, delay the pressed feedback for
                    // a short period in case this is a scroll.
                    if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        // Not inside a scrolling container, so show the feedback right away
                        setPressed(true, x, y);
                        checkForLongClick(0, x, y);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    setPressed(false);
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_MOVE:
                    drawableHotspotChanged(x, y);

                    // Be lenient about moving outside of buttons
                    if(! pointInView(x, y, mTouchSlop)) { // Outside button removeTapCallback();if((mPrivateFlags & PFLAG_PRESSED) ! = 0) { // Remove any future long press/tap checks removeLongPressCallback();setPressed(false); }}break;
            }
//Flag5
            return true;
        }

        return false;
    }Copy the code

Key points:

  • In Flag4 and Flag6, when the view is clickable, it will return true
  • In Flag6, a click event occurs when the gesture is raised, and as a result of the View’s dispatch method, we know that if the View is set to OnTouchListener and returns true View in onTouch (), the clickon event is disabled
  • In Flag3 when the View is DISABLED, if the View is CLICKABLE or LONGCLICKABLE it will consume and return true, This means that OnClickListener set when the View is DISABLED will be DISABLED
  • Note: When onTouchEvent returns true, it means that the distribution of the event has found a place to consume it, that is, this View, so after pressing the slide lift these actions are all handed over to consume here; Return false to indicate that the View does not consume you and should not send events in the future.

Event distribution consumption in ViewGroup

The ViewGroup dispatchTouchEvent ()

The code is quite long, which makes it easy to view the logic inside. It has been deleted appropriately, and comments have been added in key places

(Source android-24)

 public boolean dispatchTouchEvent(MotionEvent ev) {

        boolean handled = false;
        if(onFilterTouchEventForSecurity (ev)) {/ / first determines whether the events to intercept, namely to intercepted assignment final Boolean intercepted.if(actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget ! = null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) ! = 0; // If interception is allowed, leave it to intercepted () for judgmentif(! disallowIntercept) { intercepted = onInterceptTouchEvent(ev); // If interception is not allowed, set it tofalse
                } else {
                    intercepted = false; }}else{// Return ev if it is not ACTION_DOWN and no target is foundtrue
                intercepted = true;
            }

            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false; // If the event is not intercepted, find the view that will consume the event and place it in newTouchTargetif(! 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) { final ArrayList<View> preorderedList = buildTouchDispatchChildList(); final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); 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); newTouchTarget = getTouchTarget(child); // The target of the found event distribution breaks out of the loopif(newTouchTarget ! = null) { newTouchTarget.pointerIdBits |= idBitsToAssign;break; } // The event is distributed to the child, if the child consumes, and newTouchTarget is assigned to, mFirstTouchTarget breaks out of the loopif (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break; / /}}}if(newTouchTarget == null && mFirstTouchTarget ! = null) { // Did not find a child to receive the event. // Assign the pointer to the least recently added target. newTouchTarget = mFirstTouchTarget;while(newTouchTarget.next ! = null) { newTouchTarget = newTouchTarget.next; }}}} // If no consumption target is found, treat the ViewGroup as a View, since the ViewGroup is a subclass of View, that is, execute super.dispatchTouchEvent(); Which is dispatchTouchEvent() of the View;if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else{// Distribute events to already found consumption targets TouchTarget fossil = 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; }}return handled;
    }Copy the code

The key point

  • DisallowIntercept said this parameter is false by default allows intercept events, if don’t need to true said intercept intercept events, can be achieved by requestDisallowInterceptTouchEvent set (), A child view of a ViewGroup can call this method and tell it to stop intercepting the event and leave it to me
  • It is up to the ViewGroup’s own onInterceptTouchEvent() to decide whether to intercept
  • If intercepted, it goes directly to super.dispatchTouchEvent(Event) for consumption
  • If you don’t intercept and you find the sub-view that you want to “qualify” to distribute the event, If the event is not dispatched, if its own view’s dispatchTouchEvent () returns false or if no child view is found to do it, it will also call super.DispatchTouchEvent ()
  • drawing

The ViewGroup InterceptTouchEvent ()

pubic boolean onInterceptTouchEvent() {return false;
}Copy the code

False is returned by default

Event interception and distribution in an Activity

  • Q1: Strictly speaking, an Activity is not a View and certainly not a ViewGroup. How is its event distribution performed?
  • Q2: Why doesn’t ac have onInterceptTouchEvent()?

    First, the first question: find the answer in the source code

DispatchTouchEvent () in Activity:
    public boolean dispatchTouchEvent(MotionEvent ev) {

        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }Copy the code
PhoneWindow superDispatchTouchEvent ()
   public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }Copy the code
DecorView
   private final class DecorView extends FrameLayout{
         public boolean superDispatchTouchEvent(MotionEvent event) {
            returnsuper.dispatchTouchEvent(event); }}Copy the code

See the nature of the Activity using the View’s dispatchTouchEvent(); The second problem is solved. The View dispatchTouchEvent() does not require onInterceptTouchEvent().

application

Frequently encountered problems

  • Why only ACTION_DOWN events are received and no sample code is received
Class MyViewGroup extends ViewGroup{public Boolean dispatchTouchEvent (MotionEvent e) {log.d ()"Li Fan Fan"."MyViewGroup --- dispatchTouchEvent --- "+e.. getAction());return  super.dispatchTouchEvent(e);
    }
    public boolean onInterceptTouchEvent(MotionEvent e){
      Log.d("Li Fan Fan"."MyViewGroup --- onInterceptTouchEvent --- "+e.. getAction()); retrun super.onInterceptTouchEvent(e) } public boolean onTouchEvent(MotionEvent e){ Log.d("Li Fan Fan"."MyViewGroup --- dispatchTouchEvent --- "+e.. getAction()); Retrun super.onTouchEvent(e)}} extends TextView{public Boolean DispatchTouchEvent (MotionEvent e) {returnsuper.dispatchTouchEvent(e); } public Boolean onTouchEvent(MotionEvent e){retrun super.onTouchEvent(e)}} // XML <MyViewGroup Android :background="#ffffff"
       android:layout_width="match_parent"
       android:layout_height="300dp">
            <MyView
                //--android:clickable="true" 
                android:background="#ff0000"
                android:text="MyButton"
                android:textSize="20sp"
                android:textColor="#00ff00"
                android:id="@+id/my_view"
                android:layout_width="match_parent"
                android:layout_height="100dp" />
</MyViewGroup>Copy the code
The phenomenon of

Run the above code and you’ll see that we do a click event, look at the log, and only receive ACTION_DOWN in the ViewGroup and View but no subsequent events

The source code

By default, if the ViewGroup does not intercept the event distribution process (the onInterceptTouchEvent() returns false), the event distribution process will be handed to the child view. The return value of executing ViewGrop is determined by the return value of dispatchTouchEvent() of the child view. By default, onTouchEvent() in the view returns false, Cause the ViewGroup to hand over to its onTouchEvent which is false by default. The final result is that the viewgroup dispatch return value is false, which means that neither the viewgroup itself nor its sub-views consume this event, so subsequent ACTION_MOVE, ACTION_UP and other events cannot be received

To solve
    1. Overriding onTouchEvent() in MyView returns true. The following

      public boolean onTouchEvent(MotionEvent e){
      //super.ononTouchEvent(e)
         return true;
      }Copy the code

      Note: If you do this you will receive the full event but the click event set on MyView will be invalid. why? Tip: disable super.ononTouchEvent(e); The consequences of this line of code

    1. Set MyView to onClickable. The source code is as follows:
//View onTouchEvent source code (simplified) public BooleanonTouchEvent() {if ((viewFlags & ENABLED_MASK) == DISABLED) {

                return (((viewFlags & CLICKABLE) == CLICKABLE
                        || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                        || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
            }

       if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
            (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                if (mPerformClick == null) {
                    mPerformClick = new PerformClick();
                }
                if(! post(mPerformClick)) { performClick(); }break;
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_CANCEL:
                break;
            case MotionEvent.ACTION_MOVE:
                break;
            }
        return true;
    }
    retrun false;Copy the code

Note that if our View is DISABLED and ONCLICKABLE the event will be consumed but not processed

    1. Set the OnTouchListener to MyView and return true in the onTouch () method according to the dispatchTouchEvent() in the View above. This will also cause the onTouchEvent not to execute. So if you set click-listening to a view, it’s not going to work.

Fixation of trailer

Android source analysis of the event distribution consumption application

  • How to resolve event conflict
  • Customize a simple Viewpager
  • How do RefreshSwipLayout consume and distribute events