“Android Event Distribution mechanism”

This article has participated in the activity of “New person creation Ceremony”, and started the road of digging gold creation together

I. Event distribution mechanism

In the Android system, the event distribution mechanism plays an important role. To understand the event distribution mechanism, we can have a deeper understanding of conflicts such as sliding. Custom View can be better expanded, related problems can be thought from the whole process, to find the best solution.

  • How can a simple click event be consumed step by step? Who should be handled and who should not be handled is determined by what factors, which is an unavoidable problem in practical development, especially in the application scenario of custom View.

  • To get a sense of how events are delivered and consumed as a whole:

2. Start the Activity

To analyze a simple initial page that contains only a ViewGroup in the Activity layout, you first need to understand the View hierarchy. If you click on the ViewGroup, see how the event is passed. To get a sense of the Activity hierarchy, look at the code for compatactivity loading processes based on the latest AppCompatActivity:

  • CustomActivity the setContentView ()
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView();
}
Copy the code
  • In the AppCompatActivity
/ / # 1
@Override
public void setContentView(@LayoutRes int layoutResID) {
  getDelegate().setContentView(layoutResID);
}
/ / # 2
@NonNull 
public AppCompatDelegate getDelegate(a) {
  if (mDelegate == null) {
    mDelegate = AppCompatDelegate.create(this.this);
  }
  return mDelegate;
}
//#3 AppCompatDelegateImpl
@Override
public void setContentView(int resId) {
  ensureSubDecor();
  ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
  contentParent.removeAllViews();
  LayoutInflater.from(mContext).inflate(resId, contentParent);
  mAppCompatWindowCallback.getWrapped().onContentChanged();
}
Copy the code

1. What is AppCompatDelegate? Since switching to AppCompatActivity, loading setContentView() is different from the previous process.

$compatDelegate = compatDelegate ($compatDelegate) $compatDelegate ($compatDelegate) $compatDelegate ($compatDelegate)

This class represents a delegate which you can use to extend AppCompat's support to any Activity.When using an AppCompatDelegate.you should call the following methods instead of the Activity method of the same name.Copy the code

See, AppCompatDelegate is actually a delegate class, which is added to make activities compatible. Almost all activities are supported with methods of the same name.

3.AppCompatDelegate is an abstract class, so the implementation details need to find its implementation class, which is -appCompatDelegateImpl, so what does setContentView() do? The entire call flow from #1 to #3, plus our CustomActivity, should be: CoustomActivity#setContentView->AppCompatActivity#setContentView->AppCompatActivity#getDelegate->AppCompatDelegate#setCo ntentView

  • The AppCompatDelegate implementation class AppCompatDelegateImpl

Take a quick look at setContentView and see what it does:

@Override
public void setContentView(int resId) {
  ensureSubDecor();
  ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
  contentParent.removeAllViews();
  LayoutInflater.from(mContext).inflate(resId, contentParent);
  mAppCompatWindowCallback.getWrapped().onContentChanged();
}
Copy the code
1.ensureSubDecor()

DecorView, DecorView, Decor if you’re familiar with the Activity startup process, the Decor is familiar. **ensureSubDecor()** What is created?

private void ensureSubDecor(a) {
  if(! mSubDecorInstalled) { mSubDecor = createSubDecor(); }/ /...
}

private ViewGroup createSubDecor(a) {
  TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
  / /...
  ensureWindow();
  mWindow.getDecorView();
  final LayoutInflater inflater = LayoutInflater.from(mContext);
  ViewGroup subDecor = null;
  if(! mWindowNoTitle) {if(! mWindowNoTitle) {// If we're floating, inflate the dialog title decor
      subDecor = (ViewGroup) inflater.inflate(
      R.layout.abc_dialog_title_material, null);
      // Floating windows can never have an action bar, reset the flags
      mHasActionBar = mOverlayActionBar = false;
    } else if (mHasActionBar) {
      
    }
  }
  mWindow.setContentView(subDecor);
  //....
  return subDecor;
}
Copy the code

1. By analyzing the creation process of createSubDecor, we found that it was not a DecorView in Window, but a subDecorView created after the creation of DecorView, including whether it contains actionBar, floating, etc. This is equivalent to the titleBar in the previous DecorView.

2. Wait until the subDecorView creation process is complete and the view hierarchy is Activity->PhoneWindow->DecorView->subDecorView.

3. When **ensureSubDecor()** executed:

ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mAppCompatWindowCallback.getWrapped().onContentChanged();
Copy the code

SubDecor through findViewById is actually a parent container, and the id of the parent container has been determined – R.I.D.C. tent

Add our own layout (corresponding to resId) to subDecorView by dynamically loading. The level of Activity->PhoneWindow->DecorView->subDecorView->cutomView.

2. Hierarchy
  • In the initial creation of the Activity, addView is used to attach the View layer by layer to the container (without analyzing the specific process of course). Intuitively, the View on the top layer is added last. Based on this feature, when the event is transmitted in the source code of the child View using reverse order traversal, increase the hit probability.
  • ACTION_DOWN–ACTION_UP, ACTION_DOWN–MOVE–MOVE… ACTION_UP. Since the event is applied to the Activity first, start with the Activity.
The Activity of the dispatchTouchEvent ();/**
   * Called to process touch screen events.  You can override this to
   * intercept all touch screen events before they are dispatched to the
   * window.  Be sure to call this implementation for touch screen events
   * that should be handled normally.
   * @param ev The touch screen event.
   * @return boolean Return true if this event was consumed.
   */
  public boolean dispatchTouchEvent(MotionEvent ev) {
      if (ev.getAction() == MotionEvent.ACTION_DOWN) {
          onUserInteraction();
      }
      if (getWindow().superDispatchTouchEvent(ev)) {
          return true;
      }
      return onTouchEvent(ev);
  }

  public void onUserInteraction(a) {}/**
 * Called when a touch screen event was not handled by any of the views
 * under it.  This is most useful to process touch events that happen
 * outside of your window bounds, where there is no view to receive it.
 * @return Return true if you have consumed the event, false if you haven't.
 * The default implementation always returns false.
 */
public boolean onTouchEvent(MotionEvent event) {
    if (mWindow.shouldCloseOnTouch(this, event)) {
        finish();
        return true;
    }
    return false;
}
Copy the code
  • And you can see thatonTouchEvenThe default implementation of t is false, and as the comments clearly explain, the event ends there. But there is a premisegetWindow().superDispatchTouchEvent(ev) = falseAnd getWindow returns window, the only implementation of which is the interfacePhoneWindow.superDispatchTouchEvent(ev)The method of the parent class is calledViewGroup.dispatchTouchEvent:
PhoneWindow
@Override
   public boolean superDispatchTouchEvent(MotionEvent event) {
       return mDecor.superDispatchTouchEvent(event);
   }
Copy the code

1. The mDecor = DecorView, DecorView inherited from FrameLayout FrameLayout inherited from Viewgroup mDecor. SuperDispatchTouchEvent (event), the final call is dispatchTouchEvent method of Viewgroup.

  • To summarize, when an event is received by an activity and can be passed down, Have passed the order for the activity. The dispatchTouchEvent – > PhoneWindow. SuperDispatchTouchEvent (ev) – > DecorView. SuperDispatchTouchEvent (event) – > V IewGroup dispatchTouchEvent, event resulting to a ViewGroup analyzed dispatchTouchEvent:
1.VIewGroup#dispatchTouchEvent()
/ /...
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
  if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget ! =null) {
    final booleandisallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) ! =0;
    if(! disallowIntercept) {// Determine whether the viewGroup needs to intercept the event
      intercepted = onInterceptTouchEvent(ev);
      ev.setAction(action); // restore action in case it was changed
    } else {
      intercepted = false; }}}/ /...

Copy the code

1. When an event is delivered to the dispatchTouchEvent method of the ViewGroup, the previously mentioned sequence of completed events always starts with ACTION_DOWN, which is determined first.

In the second step, check whether the ViewGroup needs to intercept this event. By default, the ViewGroup returns false in onInterceptTouchEvent.

// ViewGroup defaults to not intercepting event return false
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;
}
Copy the code

3. In the same way as the traversal operation of the sub-view, notice that the form here is in reverse order, judge whether the View is visible, whether the animation is being performed, whether the click range is above it, and thus determine whether the View consumes the event:

if (newTouchTarget == null&& childrenCount ! =0) {
    final float x = ev.getX(actionIndex);
    final float y = ev.getY(actionIndex);
    // Find a child that can receive the event.
    // Scan children from front to back.
    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);
    // 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();
         }         
Copy the code
2.VIew#dispatchTouchEvent()
// View dispatchTouchEvent method
public boolean dispatchTouchEvent(MotionEvent event) {
  if (onFilterTouchEventForSecurity(event)) {
     if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
         result = true;
     }
     //noinspection SimplifiableIfStatement
     // Include, long press, click, onTouch, etc.
     ListenerInfo li = mListenerInfo;
     if(li ! =null&& li.mOnTouchListener ! =null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) {
                 result = true;
      }
    //mOnTouchListener has the highest priority
      if(! result && onTouchEvent(event)) { result =true; }}}Copy the code

1. There is no method for intercepting events in a View. The default is to handle events.

2.ListenerInfo contains long press, click, onTouch, etc. There is a detail here, if the View is set to mOnTouchListener, it has high priority, before onTouchEvent. So let’s see what happens in onTouchEvent.

  • The View of the onTouchEvent ()
public boolean onTouchEvent(MotionEvent event) {
  case MotionEvent.ACTION_UP:
       mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
       if ((viewFlags & TOOLTIP) == TOOLTIP) {
           handleTooltipUp();
        }
        if(! clickable) { removeTapCallback(); removeLongPressCallback(); mInContextButtonPress =false;
           mHasPerformedLongPress = false;
           mIgnoreNextUpEvent = false;
           break;
         }
  case MotionEvent.ACTION_DOWN:
  if(! clickable) { checkForLongClick(ViewConfiguration.getLongPressTimeout(), x, y,TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);break; }}/** * Defines the default duration in milliseconds before a press turns into * a long press */
private static final int DEFAULT_LONG_PRESS_TIMEOUT = 500; 
Copy the code

CheckForLongClick (); DEFAULT_LONG_PRESS_TIMEOUT (); Analysis of how to judge the long press:

private void checkForLongClick(long delay, float x, float y, int classification) {
  if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {
     mHasPerformedLongPress = false;
     if (mPendingCheckForLongPress == null) {
         mPendingCheckForLongPress = newCheckForLongPress(); } mPendingCheckForLongPress.setAnchor(x, y); mPendingCheckForLongPress.rememberWindowAttachCount(); mPendingCheckForLongPress.rememberPressedState(); mPendingCheckForLongPress.setClassification(classification); postDelayed(mPendingCheckForLongPress, delay); }}public boolean postDelayed(Runnable action, long delayMillis) {
    final AttachInfo attachInfo = mAttachInfo;
    if(attachInfo ! =null) {
        return attachInfo.mHandler.postDelayed(action, delayMillis);
     }
    // Postpone the runnable until we know on which thread it needs to run.
    // Assume that the runnable will be successfully placed after attach.
    getRunQueue().postDelayed(action, delayMillis);
    return true;
}

private final class CheckForLongPress implements Runnable {
  private int mOriginalWindowAttachCount;
  private float mX;
  private float mY;
  private boolean mOriginalPressedState;
  private int mClassification;
  
  @Override
  public void run(a) {
     if((mOriginalPressedState == isPressed()) && (mParent ! =null) && mOriginalWindowAttachCount == mWindowAttachCount) {
        recordGestureClassification(mClassification);
        if (performLongClick(mX, mY)) {
            mHasPerformedLongPress = true; }}}}public boolean performLongClick(float x, float y) {
   mLongClickX = x;
   mLongClickY = y;
   final boolean handled = performLongClick();
   mLongClickX = Float.NaN;
   mLongClickY = Float.NaN;
   return handled;
}
Copy the code

2. DEFAULT_LONG_PRESS_TIMEOUT (default: 500ms) sends a 500ms Runnable to the message queue via handler. If the event is consumed within 500ms, return true and the long press event will be processed, otherwise the event will be removed in ACTION_UP -removeLongPressCallback.

  • View click event handling
// in the ACTION_UP branch of the onTouchEvent method
if(! mHasPerformedLongPress && ! mIgnoreNextUpEvent) { removeLongPressCallback();if(! focusTaken) {if (mPerformClick == null) {
         mPerformClick = new PerformClick();
    }
    if(! post(mPerformClick)) { performClickInternal(); }}}Copy the code

1. Click events are also not called directly, and are also posted in a Runnable manner. The advantage of this is that the view status update is not affected before the click starts.

2. Are events not processed for a state clickable that is impossible to click? The answer is no:

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;
}
Copy the code

As you can see, the onTouchEvent method will be called even if the view is impossible to click, but the event is not handled by default.

3. Briefly summarize the process
  • For a ViewGroup, the event is passed to dispatchTouchEvent. If the onInterceptTouchEvent returns true, the event is intercepted. The important thing is that the event is handled by the ViewGroup. OnTouchEvent will be called. If onInterceptTouchEvent returns false, the event will be passed down to the child, at which point the child’s dispatchTouchEvent will be called, and so on, until the event is fully handled.
  • When the View needs to process an event, if OnTouchListener is set (with the highest priority), the onTouch method of OnTouchListener will be called, and OnClickListener’s priority is at the end of the event passing.
  • A complete sequence of events is consumed in the order of Activity->PhoneWindow->View; If the onTouchEvent of one of the last views returns false, the event is thrown up and the parent’s onTouchEvent is called. If all views handle the event, the final event is passed to the Activity. The onTouchEvent for the Activity is called.
  • In general, a sequence of events can only be consumed by one View. All events in the same sequence will be handled directly by the View, and its onInterceptTouchEvent will not be called again. If the child view invokes the requestDisallowInterceptTouchEvent, will decide whether the parent view intercept events * * (except action_down events, Action_down resets FLAG_DISALLOW_INTERCEPT status
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    if(disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) ! =0)) {
        // We're already in this state, assume our ancestors are too
        return;
    }
    if (disallowIntercept) {
        mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
    } else {
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    }
    // Pass it up to our parent
    if(mParent ! =null) { mParent.requestDisallowInterceptTouchEvent(disallowIntercept); }}Copy the code
  • Once a View has started processing events, if it does not consume ACTION_DOWN (onTouchEvent returns false), then no other events in the same sequence of events are assigned to it and the event will be reassigned to its parent, whose onTouchEvent will be called.
  • If a View consumes no events other than ACTION_DOWN, the click event will disappear, the parent element’s onTouchEvent will not be called, and the current View will receive subsequent events, which will eventually be passed on to the Activity.
  • The onInterceptTouchEvent method of a ViewGroup returns false by default. The View does not have an onInterceptTouchEvent method. Once an event is passed to it, Then its onTouchEvent method will be called.
  • The View’s onTouchEvent method consumes the event by default (returning true), unless it is unclickable (both clickable and longClickable are false). The longClickable property of a View defaults to false, the clickable property of a Button defaults to true, and the TextView defaults to false. Disable does not affect event consumption. Even if a view is disable, events will still be consumed, but the user does not perceive it, that is, there is no feedback.
Three, what is the use?

There are cases in development where only lists are displayed, i.e., non-clickable lists. How to implement this requirement? Of course, if RecyclerView is used as an example, it can be prohibited in item. Can the event transfer not consume click events by default?

As mentioned above, if a View doesn’t consume an ACTION_DOWN event that’s onTouchEvent and returns false, isn’t that enough? Simple use:

1. Customize a non-clickable RecyclerView
/** * Created by Sai * on 2022/01/28 16:35. */
public class UnClickableRecyclerView extends RecyclerView {
    public UnClickableRecyclerView(@NonNull @NotNull Context context) {
        super(context);
    }

    public UnClickableRecyclerView(@NonNull @NotNull Context context, @Nullable @org.jetbrains.annotations.Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public UnClickableRecyclerView(@NonNull @NotNull Context context, @Nullable @org.jetbrains.annotations.Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onTouchEvent(MotionEvent e) {
        return false;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent e) {
        return true;
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        return super.dispatchTouchEvent(ev); }}Copy the code

1. Override onTouchEvent to return false, and onInterceptTouchEvent to return true, indicating that the event is intercepted and not consumed.