【Android原始碼】View的事件分發機制

指間沙似流年發表於2017-12-23

Android事件分發完全解析之事件從何而來

Activity的事件分發過程

關於事件是如何而來的,可以參考上面的連結,事件的產生是使用者的操作觸發了Linux的input子系統。

當一個點選事件產生的時候,事件最先從底層傳遞給當前的Activity,由Activity的dispatchTouchEvent來進行事件分發。其中具體的工作是由Window來完成的,而我們知道Window是一個抽象類,它的具體實現是PhoneWindow:

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

// PhoneWindow.java
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
   return mDecor.superDispatchTouchEvent(event);
}
複製程式碼

當Window的superDispatchTouchEvent返回false的時候,標識當前事件沒有人處理,所有的View的onTouchEvent都返回的false,這個時候就交給Activity的onTouchEvent來處理。

而在PhoneWindow中則是將事件傳遞給了DecorView,而DecorView是什麼呢?

DecorView是我們setContentView的父佈局,所以事件會進一步傳遞給我們設定的佈局中。 關於DecorView的詳細分析:【Android原始碼】Activity如何載入佈局

ViewGroup的事件分發

一般情況下,我們設定的佈局的最外層都是ViewGroup,那麼由DecorView傳遞過來的事件,首先交給ViewGroup的dispatchTouchEvent來處理:

// 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;
}
複製程式碼

首先ViewGroup會在事件為ACTION_DOWNmFirstTouchTarget != null的情況下來判斷是否需要攔截當前事件,其中ACTION_DOWN表示當前事件為按下的事件,mFirstTouchTarget表示成功處理事件的子View,當mFirstTouchTarget != null的時候說明,事件已經被子元素處理了。

這樣的話,如果mFirstTouchTarget != null成立的時候,ACTION_MOVEACTION_UP都不會呼叫onInterceptTouchEvent查詢是否需要攔截事件,這樣同一個事件序列都會交給同一個子View來處理。

當然還有一種特殊情況,FLAG_DISALLOW_INTERCEPT標記,當子View通過requestDisallowInterceptTouchEvent設定請求父類不要攔截的時候,ViewGroup將不能攔截ACTION_DOWN以外的其他事件。因為在ACTION_DOWN的時候,系統會重置FLAG_DISALLOW_INTERCEPT標記:

private void resetTouchState() {
   clearTouchTargets();
   resetCancelNextUpFlag(this);
   mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
   mNestedScrollAxes = SCROLL_AXIS_NONE;
}
複製程式碼

從上面的程式碼可以看到當ViewGroup需要攔截事件之後,後續的事件都會預設交給它來處理,並且不會再呼叫onInterceptTouchEvent

這個結論說明了一點,onInterceptTouchEvent並不會每次的事件都會被呼叫,如果我們需要提前處理所有的事件,那麼就需要在dispatchTouchEvent中來處理。

接下來我們繼續看下面的程式碼,當ViewGroup不攔截事件的時候,事件會繼續下發給ViewGroup的子View來處理。

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, 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);
}
複製程式碼
  1. 首先遍歷ViewGroup的所有子元素

  2. 判斷子元素是否能接收到事件,判斷標準是子元素是否在播放動畫和事件的座標是否在子元素的範圍內。

    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;
    }
    複製程式碼
  3. 當有子元素滿足條件的時候,事件就會交給子元素來處理。

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
           View child, int desiredPointerIdBits) {
       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);
       }      
    }
    複製程式碼
  4. 當子元素的dispatchTouchEvent返回true的時候,mFirstTouchTarget會被賦值並跳出迴圈。

    newTouchTarget = addTouchTarget(child, idBitsToAssign);
    alreadyDispatchedToNewTouchTarget = true;
    break;
    複製程式碼
  5. 當子元素的dispatchTouchEvent返回false的時候,ViewGroup會繼續遍歷。

  6. 當ViewGroup沒有子元素或者子元素處理了點選事件但是dispatchTouchEvent返回false的情況下,ViewGroup會自己處理事件,這個時候child為null,則呼叫super.dispatchTouchEvent(transformedEvent)

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

View的事件分發處理

當ViewGroup不停的向下處理,找到了符合條件的子View之後,事件就交給了View來處理,因為View裡面沒有子元素,所以處理事件就比較簡單了:

public boolean dispatchTouchEvent(MotionEvent event) {
   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;
       }
   }
}
複製程式碼
  1. 判斷有沒有設定OnTouchListener,如果設定了OnTouchListener,並且onTouch的返回值為true的情況下,onTouchEvent就不會被呼叫了。
  2. 當上述的情況不成立的情況下,會呼叫onTouchEvent
if ((viewFlags & ENABLED_MASK) == DISABLED) {
  if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
      setPressed(false);
  }
  // A disabled view that is clickable still consumes the touch
  // events, it just doesn't respond to them.
  return (((viewFlags & CLICKABLE) == CLICKABLE
          || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
          || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
複製程式碼

OnTouchEvent首先判斷當前的View是否處於不可用狀態,不可用狀態下,事件照樣會被處理掉。

接下來就是OnTouchEvent處理事件的過程,其中DOWM和MOVE沒有什麼特殊的地方,特殊的是在UP的情況下:

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)) {
           performClick();
       }
   }
}
複製程式碼

當View的CLICKABLE或者LONG_CLICKABLE有一個為true的時候,會消耗事件。並且此時會觸發performClick方法,此時View的OnClickListener就會被呼叫:

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

   sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
   return result;
}
複製程式碼

從上面的分析可以得出一個結論,OnTouchListener的優先順序大於OnTouchEvent,並且OnClickListener的優先順序最低。當OnTouchListeneronTouch返回true的時候,後面的都不會被觸發,這也就能解決有時寫自定義View的事件分發時的一些奇怪BUG的解釋。

到此為止,整個View的事件分發過程就完成了。

相關文章