I still don't understand Android's touch feedback mechanism

Posted by ricta on Thu, 18 Nov 2021 14:22:33 +0100

Recommended by Haowen:
Author: riane

Let's analyze in detail what our fingers have experienced from touching various views on the screen to the end of this click event.

Event type

There are three types of touch events:

 int action = MotionEventCompat.getActionMasked(event);
    switch(action) {
        case MotionEvent.ACTION_DOWN:
            break;
        case MotionEvent.ACTION_MOVE:
            break;
        case MotionEvent.ACTION_UP:
            break;
    }

In addition to these three types, there is an ACTION_CANCEL, which indicates that the current gesture is cancelled. It can be understood that a child view receives the action sent to it by the parent view_ Down event: when the parent view intercepts the event and no longer forwards the event to the child view, an action will be given to the child view_ Cancel event.

Event distribution mechanism of Activity

When we touch the screen, we will first pass the click event to the Activity, which is completed by PhoneWindow in the Activity. Then PhoneWindow will pass the event to the root DecorView of the whole control tree, and then DecorView will hand over the event processing to ViewGroup.

### Activity.dispatchTouchEvent
  public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
  // It is distributed by Window. When getWindow().superDispatchTouchEvent(ev) returns true
  //dispatchTouchEvent returns true,
  //Method ends and the onTouchEvent method is not executed
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev); 1
    }
### PhoneWindow
//Pass the event to the ViewGroup for processing
   @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

When the event is not processed, it will return to the onTouchEvent method of the Activity

### Activity.onTouchEvent
public boolean onTouchEvent(MotionEvent event) {
  //true is returned only when the click event is outside the Window boundary. Generally, false is returned
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }

        return false;
    }

### Window.shouldCloseOnTouch
  public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
  //Is it a Down event? Is the event coordinate waiting within the boundary
        final boolean isOutside =
                event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(context, event)
                || event.getAction() == MotionEvent.ACTION_OUTSIDE;
        if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
          //Return true: indicates that the event is outside the boundary, that is, the consumption event
            return true;
        }
        return false;
    }

Event distribution mechanism of ViewGroip

As can be seen from the above distribution mechanism, the event distribution mechanism of ViewGroup starts with dispatchTouchEvent().

	### ViewGroup.dispatchTouchEvent
    public boolean dispatchTouchEvent(MotionEvent ev) {
      if (mInputEventConsistencyVerifier != null) {
                mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
            }
       ......

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

            // Handle an initial down.
            if (actionMasked == MotionEvent.ACTION_DOWN) {             
                cancelAndClearTouchTargets(ev);
              //Reset all touch states to prepare for a new cycle
                resetTouchState();
            }

            // The event is processed and mFirstTouchTarget is the first touch target
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
              //The sub View sets the flag through requestdisallowuntercepttouchevet_ DISALLOW_ The interrupt flag indicates that the parent View will no longer intercept events
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                  //Return false default
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // recovery
                } else {
                    intercepted = false;
                }
            } else {            
                intercepted = true;
            }

           ......

            // Check for cancelation.
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;

            // Update list of touch targets for pointer down, if needed.
            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
          //Do not cancel do not intercept events
            if (!canceled && !intercepted) {
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;

                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) {
                        ......

                      //Traverse the child elements of the ViiewGroup. If the child element can receive the click event, it will be handed over to the child element for processing
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);
                           ......

                             //Within the range of the subview or whether the subview is playing the animation
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }

                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                // Within touch range
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            resetCancelNextUpFlag(child);

                          //The dispatchTouchEvent() of the View is called inside the condition judgment
                          //It realizes the transmission of click events from View Group to child views
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                               .....
                    }
                    ......
                }
            }
            ......
        return handled;
    }

  //intercept
  //Return false: do not intercept (default)
  // Return true: intercept, that is, the event stops being delivered downward (onInterceptTouchEvent() needs to be manually copied, which returns true)
     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;
    }

Seeing that the dispatchTransformedTouchEvent method is called above, how does the View Group distribute events to child views

### ViewGroup.dispatchTransformedTouchEvent
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

        final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
          //If there is a child View, call the dispatchTouchEvent(event) method of the child View. If there is no child View,
        // The super.dispatchTouchEvent(event) method is called.
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }
       ......
        // Done.
        transformedEvent.recycle();
        return handled;
    }  

It can be seen that the event distribution is always delivered to the ViewGroup first, then traverse the sub View to find the clicked sub View, and then deliver it to the sub View.

TouchTarget: a one-way linked list that records which pointers (fingers) are pressed by each sub View

Three key method relationships can be represented by pseudo code:

public boolean dispatchTouchEvent(MotionEvent ev){

  boolean consume = false;

  if(onInterceptTouchEvent(ev)){
    	consume = onTouchEvent(ev);
  } else {
    	consume = childDispatchTouchEvent(ev);
  }
  return consume;
}

//Picture case

Event distribution mechanism of View

The event is passed to the dispatchTouchEvent method of View.

### View.dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
        ......
        boolean result = false;
        ......

        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //The onTouchListener method takes precedence over the onTouchEvent method
            ListenerInfo li = mListenerInfo;
          //true will be returned only if three conditions are met
           //mOnTouchListener !=  Null montouchlistener variable is assigned in View.setOnTouchListener()
          //(mviewflags & enabled_mask) = = enabled determines whether the currently clicked control is enabled
          // The ontouchlistener.ontouch (this, event) callback control registers the onTouch() of the Touch event
            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;
    }

If one of the three conditions is not met, the event will be passed to onTouchEvent() of View. The onTouchEvent method will be analyzed in detail below

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

   		//Click and long press
        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

   		//If disabled, eat the event
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
          //Lift it up and still set setPressed(false), indicating that it has been clicked
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;

            return clickable;
        }

   	//Touch agent to add click area
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

   		//Tooltip tooltiptext is an interpretation tool
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    if ((viewFlags & TOOLTIP) == TOOLTIP) {
                      //Let go and disappear ToolTip
                        handleTooltipUp();
                    }
                    if (!clickable) {
                      //Cancel listener
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        break;
                    }
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                //Press or pre press status
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {

                        boolean focusTaken = false;
                      //Can get focus
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                      //If it is pre pressed
                        if (prepressed) {
                            setPressed(true, x, y);
                        }

                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // Cancel long press callback
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {

                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                  //Trigger Click
                                    performClickInternal();
                                }
                            }
                        }

                       ......

                        removeTapCallback();
                    }
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_DOWN:
                //Did you touch the screen
                    if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                        mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                    }
                    mHasPerformedLongPress = false;

                //Do not click Check long press to set a long press listener
                    if (!clickable) {
                        checkForLongClick(0, x, y);
                        break;
                    }

                //Judge right click
                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }

                    // In a sliding control
                    boolean isInScrollingContainer = isInScrollingContainer();

                //For views within the scrolling container, if this is scrolling, delay the feedback pressed for a short period of time.
                //I don't know whether you operate the parent View or the child View in the sliding control
                //If it is not sliding, you can override shouleDelayChildPressedState to false without delaying the pressing time of the child View
                    if (isInScrollingContainer) {
                      //The setting status is set to pre press
                        mPrivateFlags |= PFLAG_PREPRESSED;

                        if (mPendingCheckForTap == null) {
                          //Set press wait
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        //This is the core, if not in the sliding control, press again
                        setPressed(true, x, y);

                        checkForLongClick(0, x, y);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    if (clickable) {
                        setPressed(false);
                    }
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    break;

                case MotionEvent.ACTION_MOVE:
                //Clickable ripple change
                    if (clickable) {
                        drawableHotspotChanged(x, y);
                    }

                    // If the hand is out of bounds
                    if (!pointInView(x, y, mTouchSlop)) {
                        // Cancel all
                        removeTapCallback();
                        removeLongPressCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            setPressed(false);
                        }
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    }
                    break;
            }

            return true;
        }

        return false;
    }

Finally, the performClick () method is called

### View.performClick 
public boolean performClick() {

        notifyAutofillManagerOnClick();

        final boolean result;
        final ListenerInfo li = mListenerInfo;
   // If the View has a click event set, the onClick method will execute.
   //onTouch() precedes onClick()
        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;
    }

Event delivery summary

  1. The same event sequence refers to the period from the finger touching the screen to the finger leaving. The whole event sequence starts with the down event, including a varying number of move events, and ends with the up event.
  2. Under normal circumstances, an event sequence can only be intercepted and consumed by one View.
  3. Once a View is intercepted, this event sequence can only be processed by it, and its onInterceptToucheEvent will not be called again.
  4. Once a View starts processing events, if it does not consume action_ For the down event, other events in the same event sequence will not be handed over to it for processing, and the event will be handed over to its parent element for processing, that is, onTouchEvent of the parent element will be called.
  5. If View does not consume action_ For other events other than down, the click event will disappear. At this time, the onTouchEvent of the parent element will not be called, and the current View can continue to receive subsequent events. Finally, these disappeared click events will be passed to the Activity for processing.
  6. ViewGroup does not intercept any events by default.
  7. The onTouchEvent of View will consume events by default (return true), unless it is not clickable (clickable and longClickable are false at the same time). The longClickable attribute of View is false by default, and the clickable attribute depends on different situations. For example, the clickable attribute of Button is true by default, while the clickable attribute of TextView is false by default.
  8. The enable property of View does not affect the default return value of onTouchEvent. Even if a View is in the disable state, as long as one of its clickable or longClickable is true, its onTouchEvent returns true.
  9. The event delivery process is from the outside to the inside, that is, the event is always delivered to the parent element first, and then distributed by the parent element to the child View. The event distribution process of the parent element can be intervened in the child element through the requestdisallowintercepttuchevent method, but the action_ Except for the down event.

Complete schematic diagram:

Topics: Android Design Pattern kotlin