Android 從源碼分析 Android 觸摸事件分發過程

前言

上篇文章我們用 demo 分析了 Android 觸摸事件的分發過程,這次我們將嘗試從源碼的角度分析 Android 觸摸事件的分發過程。

Activity 對觸摸事件的分發

當一個觸摸事件發生時,最先傳遞給 Activity,由 Activity 的 dispatchTouchEvent 方法進行事件的分發。

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

由上面源碼的第五行可以看出事件會交給 Window 的 superDispatchTouchEvent 方法進行處理。通過跟蹤源碼可以得知 Window 的 superDispatchTouchEvent 方法是個抽象方法。而 PhoneWindow 是 Window 的唯一實現類。因此事件交由 PhoneWindow 的 superDispatchTouchEvent 方法處理。

    public boolean superDispatchTouchEvent(KeyEvent event) {  
      return mDecor.superDispatcTouchEvent(event);  
    }

由上面源碼可知,事件交給了 DecorView 的 superDispatcTouchEvent 方法。

    public boolean superDispatchTouchEvent(MotionEvent event) {  
       return super.dispatchTouchEvent(event);  
    } 

DecorView 的 superDispatcTouchEvent 方法又調用了其父類的 dispatchTouchEvent 方法。DecorView 的父類是 FrameLayout,而 FrameLayout 的父類是 ViewGroup。但是 FrameLayout 裏沒有 dispatchTouchEvent 所以此處的 dispatchTouchEvent 方法就是調用的 ViewGroup 的 dispatchTouchEvent 方法。

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

這段代碼是判斷是否要攔截當前事件。當事件是 ACTION_DOWN 或者 mFirstTouchTarget != null 時將判斷是否要攔截事件。ACTION_DOWN 好理解,但是 mFirstTouchTarget != null 是什麼意思?這裏我們暫且不管,後面將給出解釋。

這裏我們需要注意 FLAG_DISALLOW_INTERCEPT 這個標記位。這個標記位是通過 requestDisallowInterceptTouchEvent 方法來設置的,一般用於子 View 中。FLAG_DISALLOW_INTERCEPT 一旦設置後,ViewGroup 將無法攔截除了 ACTION_DOWN 以外的其他事件。

    final View[] children = mChildren;
     for (int i = childrenCount - 1; i >= 0; i--) {
       final int childIndex = customOrder
                                    ? getChildDrawingOrder(childrenCount, i) : i;
       final View child = (preorderedList == null)
                                    ? children[childIndex] : preorderedList.get(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);
    }

上面這段代碼是遍歷 ViewGroup 的所有子元素,然後判斷子元素是否能接受到事件。是否能接受事件主要有兩點判斷:子元素是否在播放動畫和觸摸事件的座標是否落在子元素的區域內。由源碼可以得知,dispatchTransformedTouchEvent 調用了子元素的 dispatchTouchEvent 方法。如果子元素成功處理事件,則會執行 if 裏面的代碼。注意 48 行 addTouchTarget 方法。

    private TouchTarget addTouchTarget(View child, int pointerIdBits){
      TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
      target.next = mFirstTouchTarget;
      mFirstTouchTarget = target;
      return target;  }

在這裏設置了 mFirstTouchTarget ,回到剛開始判斷是否要攔截事件的時候,有一個 mFirstTouchTarget != null 的條件。這裏我們知道如果有子元素處理了事件 mFirstTouchTarget != null 成立。

如果遍歷了所有子元素後事件都沒有被處理,這裏有兩種情況:第一,ViewGroup 中沒有子元素;第二種,子元素處理事件,但是 dispatchTouchEvent 返回了 false,這一般是因爲子元素在 onTouchEvent 中返回了 false。在這兩種情況下 ViewGroup 會自己處理點擊事件。

View

dispatchTouchEvent

    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 對事件的處理過程,首先會判斷有沒有設置
OnTouchListener,如果 OnTouchListener 中的 onTouch 方法返回 true,那麼 onTouchEvent 就不會被調用,否則將會執行。

onTouchEvent

    if ((viewFlags & ENABLED_MASK) == DISABLED) {
       if (event.getAction() == 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));
    }

由上面的代碼可以看出即使 View 處於 DISABLED 狀態時也是會消耗事件的。

    if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
       switch (event.getAction()) {
         case MotionEvent.ACTION_UP:
           boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
           if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
             // take focus if we don't have it already and we should in
             // touch mode.
             boolean focusTaken = false;
             if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                focusTaken = requestFocus();
             }
             if (prepressed) {
               // The button is being released before we actually
               // showed it as pressed.  Make it show the pressed
               // state now (before scheduling the click) to ensure
               // the user sees it.
               setPressed(true, x, y);
             }
             if (!mHasPerformedLongPress) {
               // 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();
                }
              }
            }
            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();
          }
          break;
          case MotionEvent.ACTION_DOWN:
            mHasPerformedLongPress = false;
            if (performButtonActionOnTouchDown(event)) {
                break;
            }
           // Walk up the hierarchy to determine if we're inside a scrolling container.
           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);
            }
            break;
            case MotionEvent.ACTION_CANCEL:
              setPressed(false);
              removeTapCallback();
              removeLongPressCallback();
            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;
             }
            return true;
        }

由上面源碼可以看出,只要 View 的 CLICKABLE 和 LONG_CLICKABLE 有一個爲 true 那麼它就會消耗這個事件,即 onTouchEvent 返回 true。

View 的 LONG_CLICKABLE 默認爲 false,而 CLICKABLE 是否爲 false 和具體的 View 有關,即可點擊的 View 的 CLICKABLE 爲 true ,不可點擊的爲 false。通過 setOnClickListener 和 setOnLongClickListener 可以分別將 View 的 CLICKABLE 和 LONG_CLICKABLE 設爲 true。具體代碼不再貼出。

發佈了29 篇原創文章 · 獲贊 2 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章