View事件分發

View中消息的傳遞

對於一顆View樹,它的消息的傳遞是自上而下,即從根節點開始逐層往子類遞歸傳遞的。在消息傳遞的過程中,一旦有View處理了這個消息,那麼傳遞即宣告終止。從這一點看View樹的上層有消息的優先處理權。

一、View中TouchEvent的投遞流程

事件的處理是通過多種形式的InputStage來分別處理,如NativePostImeInputStage、ViewPostImeInputStage、SyntheticInputStage、EarlyPostImeInputStage。這些都重載了onProcess()方法,以ViewPostImeInputStage爲例:

 protected int onProcess(QueuedInputEvent q) {
     if (q.mEvent instanceof KeyEvent) {//按鍵事件
         return processKeyEvent(q);
     } else {
         final int source = q.mEvent.getSource();
         if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {//Pointer事件
            return processPointerEvent(q);
        } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {//Trackball事件
            return processTrackballEvent(q);
        } else {
            return processGenericMotionEvent(q);
        }
    }
}
sequenceDiagram
InputEventReceiver->>InputEventReceiver:dispatchInputEvent()
InputEventReceiver->>InputEventReceiver:onInputEvent()
InputEventReceiver->>ViewRootImpl: enqueueInputEvent()
ViewRootImpl->> ViewRootImpl:doProcessInputEvents()
ViewRootImpl->> ViewRootImpl:deliverInputEvent()
ViewRootImpl->> InputStage:deliver()
InputStage->>ViewPostImeInputStage:onProcess()
ViewPostImeInputStage->>ViewPostImeInputStage:processPointerEvent()
ViewPostImeInputStage->>ViewPostImeInputStage:processPointerEvent()
ViewPostImeInputStage->>View:dispatchPointerEvent()
View->>View:onTouch()
View->>View:onTouchEvent()

當系統判斷當前是SOURCE_CLASS_POINTER類型的事件後,將會調用processPointerEvent()做進一步處理,這個函數會將這一處理權交給View Tree的根元素,即mView的dispatchPointerEvent接口,然後後者再細化判斷。

View.java調用dispatchTouchEvent()負責事件分發:

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

View類事件分發主要考慮兩個因素:

  • onTouch

    View通過setOnTouchListener來設置一個Event監聽。這種方式先與onTouchEvent()

  • onTouchEvent

如果沒有設置OnTouchListener或者mViewFlag != ENABLED 又或者onTouch返回false,那麼系統會將Event傳遞給onTouchEvent。

上述兩種情況再開發中都經常使用,onTouch更爲簡潔高效,onTouchEvent()更適用於View擴展類的情況(重載onTouchEvent)。

下面將分段閱讀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();

        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

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

上面的情況即便在View disable的情況,也會消耗這個事件,只是不做任何迴應而已。

如果View沒有被disable,接下來程序將處理這一Touch事件,TouchEvent還可以細分爲很多種類型,即ACTION_UP, ACTION_DOWN, ACTION_MOVE和ACTION_CANCEL等。

  1. ACTION_DOWN
case MotionEvent.ACTION_DOWN:
setPressed(true, x, y);
checkForLongClick(
ViewConfiguration.getLongPressTimeout(),
x,  y, TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);

在ACTION_DOWN事件處理中,setPress用於指示View對象是否進入press狀態。這樣View就可以設計不同press狀態下的差別顯示(比如Button在press與normal狀態下的背景不同)。另外當收到DOWN事件後,View就開始檢測它會不會演變成長按事件,可以通過ViewConfiguration.getLongPressTimeout()獲取長按時間,在Timeout後系統就需要進行長按處理。如果用戶通過setOnLongClickListener設置了響應的函數,那麼就會回調這些函數。

  1. ACTION_MOVE
    在按下後並拖動,隨後就會產生ACTION_MOVE事件。這個事件隨着用戶的拖動會不斷產生,直到ACTION_UP或者ACTION_CANCEL。
if (!pointInView(x, y, touchSlop)) {
 // Outside button
 // Remove any future long press/tap checks
 removeTapCallback();
 removeLongPressCallback();
 if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
    setPressed(false);
  }
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}

pointInView用於判斷當前手勢是否已經超出了view的範圍,如果是,就會移除長按監聽的操作並且View對象將退出press狀態。

  1. ACTION_UP

手勢操作的結束點,除了改變view的一系列狀態,最重要的操作就是判斷view受否設置了setOnClickListener所要響應的事件。

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

performClick並不會馬上調用執行,而是通過post隊列排隊的方式去處理。這樣做可以讓其他View狀態優先得到更新處理,以保證執行click操作時這些狀態時正確的。

4.ACTION_CANCEL

ACTION_CANCEL比較特殊,並不有用戶主動產生,而是有系統謹慎判斷後得出結果。這個事件說明當前手勢已經被廢棄,後續不會有任何和該手勢相關的事件產生。

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

二、ViewGroup中TouchEvent的投遞流程

ViewGroup與View在接收事件的流程上基本一致,因爲ViewGroup要對子對象進行處理,具體實現爲ViewGroup重載View中的dispatchTouchEvent方法,對View提供的派發機制進行重新規劃。

public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean handled = false;//event是否被處理
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;
            /*Step1*/ 
            // 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();
            }
            /*Step2*/ 
            // 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;
            }    
}

step1: Down事件是後續事件的起點,所以一旦收到ACTION_DOWN程序就會清理以前的狀態cancelAndClearTouchTargets(ev)和resetTouchState();

step2: 變量intercepted代表是否攔截事件。

  1. actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null

如果是DOWN事件,或者mFirstTouchTarget不爲空,分爲兩種情況

1.1、 disallowIntercept 爲false

此時ViewGroup允許攔截,就需要通過intercepted =onInterceptTouchEvent(ev)來判斷是否要真正執行攔截。

1.2、disallowIntercept 爲true

ViewGroup不允許攔截

2.如果不是DOWN事件,而且mFirstTouchTarget爲空,那麼intercepted爲true,表示ViewGroup選擇繼續攔截事件。

step3:如果intercepted爲false,表明ViewGroup不希望攔截這一消息

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 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 (!child.canReceivePointerEvents() || !isTransformedTouchPointInView(x, y, child, null)) {
         ev.setTargetAccessibilityFocus(false);
         continue;
        }
       newTouchTarget = getTouchTarget(child);
        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
            ...
        }
    }
}

執行歸屬判斷的是canReceivePointerEvents和isTransformedTouchPointInView。前者表示這個child是否能接受Pointer Events;後者計算(x, y)這個點是否落在child的範圍內。如果找到了事件的歸屬者,接下來就將事件投遞給它,實現函數爲dispatchTransformedTouchEvent。如果child != null這個函數會調用child的dispatchTouchEvent。如果child == null 就會有當前ViewGroup處理,調用super.dispatchTouchEvent()由於ViewGroup也是View。

總結:

消息的處理過程的理解

ViewGroup直接攔截intercept消息,起決定作用的是onInterceptTouchEvent。Android建議繼承ViewGroup時只重載onInterceptTouchEvent,而不是重載dispatchTouchEvent。因爲後者是所有ViewGroup共性的提取,不應輕易改變;前者是體現所有ViewGroup差異的地方。

  • 當onInterceptTouchEvent返回false時,說明當前的ViewGroup並沒有攔截這個事件,所以它需要繼續往下傳遞,知道找到處理的View.

  • 當onInterceptTouchEvent返回true時,說明當前ViewGroup需要攔截這個事件以供內部處理

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章