1. 程式人生 > >Android應用層View觸控事件分發機制

Android應用層View觸控事件分發機制

概述

前兩篇部落格從原始碼的角度對View繪製流程進行了分析,那麼當用戶需要跟View進行互動的時候,比如點選按鈕的時候,按鈕是如何得到點選事件的呢?當用戶在螢幕上進行點選或觸控的時候,事件是如何傳遞到各個View的呢?這個就是本篇部落格研究的點:View事件分發機制。只有同時掌握View事件分發機和View繪製流程,並輔以一定的練習,才能真正掌握自定義View。下面開始進入正題!注:原始碼基於API25。

觸控事件的來源及View事件分發入口

還記得之前說過,在Activity的attach方法裡面會新建一個PhoneWindow作為頂層Window,如下所示:

final void
attach(Context context, ActivityThread aThread, Instrumentation instr, IBinder token, int ident, Application application, Intent intent, ActivityInfo info, CharSequence title, Activity parent, String id, NonConfigurationInstances lastNonConfigurationInstances, Configuration config, String referrer, IVoiceInteractor voiceInteractor, Window window) { attachBaseContext(context); mFragments.attachHost(null
/*parent*/); mWindow = new PhoneWindow(this, window); mWindow.setWindowControllerCallback(this); mWindow.setCallback(this); mWindow.setOnWindowDismissedCallback(this); mWindow.getLayoutInflater().setPrivateFactory(this); .......... .......... }

在上面的程式碼當中有一句:mWindow.setCallback(this),這句話給Window設定了一個Callback回撥介面給Activity,來看一下這個回撥介面:

/**
   * API from a Window back to its caller.  This allows the client to
   * intercept key dispatching, panels and menus, etc.
   */
  public interface Callback {

      public boolean dispatchKeyEvent(KeyEvent event);

      public boolean dispatchKeyShortcutEvent(KeyEvent event);

      /**
       * Called to process touch screen events.  At the very least your
       * implementation must call
       * {@link android.view.Window#superDispatchTouchEvent} to do the
       * standard touch screen processing.
       *
       * @param event The touch screen event.
       *
       * @return boolean Return true if this event was consumed.
       */
      public boolean dispatchTouchEvent(MotionEvent event);

      .............

可以發現這個回撥接口裡面有很多回調方法,前三個方法都是對事件進行分發,第三個方法dispatchTouchEvent就是觸控事件分發。當android系統發生觸控事件時,會把觸控事件傳送給頂層Window(至於是怎麼傳遞給Window的,這裡暫時不深入研究,涉及到WindowManager,WindowManagerService等跨程序的呼叫過程,也涉及到Activity的啟動過程分析),這裡是PhoneWindow,由於給PhoneWindow設定了回撥介面,在Activity當中實現了這個介面,因此,我們檢視Activity當中的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);
   }

可以看出,如果Action為ACTION_DOWN,首先會呼叫onUserInteraction方法,這個方法如下所示:

 /**
     * Called whenever a key, touch, or trackball event is dispatched to the
     * activity.  Implement this method if you wish to know that the user has
     * interacted with the device in some way while your activity is running.
     * This callback and {@link #onUserLeaveHint} are intended to help
     * activities manage status bar notifications intelligently; specifically,
     * for helping activities determine the proper time to cancel a notfication.
     *
     * <p>All calls to your activity's {@link #onUserLeaveHint} callback will
     * be accompanied by calls to {@link #onUserInteraction}.  This
     * ensures that your activity will be told of relevant user activity such
     * as pulling down the notification pane and touching an item there.
     *
     * <p>Note that this callback will be invoked for the touch down action
     * that begins a touch gesture, but may not be invoked for the touch-moved
     * and touch-up actions that follow.
     *
     * @see #onUserLeaveHint()
     */
    public void onUserInteraction() {
    }

關於這個方法,註釋已經說的很明白了,一般和onUserLeaveHint方法配對使用,主要是用來幫助Activity管理狀態列通知。

  • 呼叫完onUserInteraction方法之後,就會呼叫getWindow.superDispatchTouchEvent方法,也就是呼叫PhoneWindow的superDispatchTouchEvent方法。
    如果這個方法返回true,就直接返回true,否則會呼叫Activity的onTouchEvent方法。

  • 我們可以在Activity重寫dispatchTouchEvent方法來對所有的觸控事件進行攔截,防止其分發至window。

  • 當Activity的onTouchEvent被呼叫的時候,說明Window的superDispatchTouchEvent方法返回false,也就是沒有消耗事件,事件最終交給Activity進行處理,因此我們也可以在Activity當中重寫onTouchEvent方法來進行事件處理。

來看下PhoneWindow的superDispatchTouchEvent方法:

 @Override
  public boolean superDispatchTouchEvent(MotionEvent event) {
      return mDecor.superDispatchTouchEvent(event);
  }

可以看出其呼叫的是頂層檢視DecorView的superDispatchTouchEvent方法,如下所示:

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

可以看出,DecorView呼叫了其父類的dispatchTouchEvent方法,跳進去看發現呼叫的是ViewGroup的dispatchTouchEvent方法。
這個也可以理解,因為DecorView是繼承自FrameLayout,而FrameLayout是繼承自ViewGroup。

通過以上分析可以看出,View觸控事件的入口是DecorView, 也就是ViewGroup。整個事觸控件的傳遞過程如下:

-> 頂層PhoneWindow得到觸控事件,呼叫其dispatchTouchEvent方法
-> Activity當中收到dispatchTouchEvent回撥方法,呼叫mWindow的superDispatchTouchEvent方法
-> 呼叫PhoneWindow的superDispatchTouchEvent方法
-> 呼叫DecorView的superDispatchTouchEvent方法
-> 最終呼叫ViewGroup的dispatchTouchEvent方法
-> View觸控事件分發入口

ViewGroup事件分發機制

既然View觸控事件的入口是ViewGroup的dispatchTouchEvent方法,說明這個方法至關重要,接下來看下這個dispatchTouchEvent方法,這個方法比較長,我會在程式碼中加入註釋:

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

           //ACTION_DOWN的話就恢復初始狀態,清除TouchTarget
           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();
           }

           // 檢測事件攔截
           final boolean intercepted;//這個標誌用於是否攔截事件,如果攔截的話,就交給自身的這個ViewGroup進行處理
           //如果是ACTION_DOWN或者mFirstTouchTarget不為空(說明已經有了TouchTarget),就開始判斷是否攔截事件
           if (actionMasked == MotionEvent.ACTION_DOWN
                   || mFirstTouchTarget != null) {
               //是否不允許攔截,在ViewGroup當中有一個requestDisallowInterceptTouchEvent(boolean disallowIntercept)方法
               //這個函式可以用來設定是否攔截,一般用在子View當中,通過呼叫父View的這個方法來阻止父View攔截事件
               final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
               //允許攔截的話,就呼叫onInterceptTouchEvent方法,一般我們需要重寫這個方法,來根據需求來進行事件攔截
               if (!disallowIntercept) {
                   intercepted = onInterceptTouchEvent(ev);
                   ev.setAction(action); // restore action in case it was changed
               } else {
               //不允許攔截
                   intercepted = false;
               }
           } else {
              //如果沒有TouchTarget且當前action不是初始的ACTION_DOWN,就攔截
              //如果當前ViewGroup攔截了ACTION_DOWN,那麼剩下的ACTION_UP,ACTION_MOVE事件都是交給它處理,且onInterceptTouchEvent方法
              // 不會再次呼叫,因為此時mFirstTouchTarget==null且action!=ACTION_DOWN
               // There are no touch targets and this action is not an initial down
               // so this view group continues to intercept touches.
               intercepted = true;
           }

           // If intercepted, start normal event dispatch. Also if there is already
           // a view that is handling the gesture, do normal event dispatch.
           if (intercepted || mFirstTouchTarget != null) {
               ev.setTargetAccessibilityFocus(false);
           }

           // 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;
           //如果事件沒有取消且沒有攔截事件
           if (!canceled && !intercepted) {

               // If the event is targeting accessiiblity focus we give it to the
               // view that has accessibility focus and if it does not handle it
               // we clear the flag and dispatch the event to all children as usual.
               // We are looking up the accessibility focused host to avoid keeping
               // state since these events are very rare.
               View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                       ? findChildWithAccessibilityFocus() : null;
              //發現沒有,這裡只是對ACTION_DOWN進行處理,那麼ACTION_UP和ACTION_MOVE呢?
               if (actionMasked == MotionEvent.ACTION_DOWN
                       || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                       || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                   final int actionIndex = ev.getActionIndex(); // always 0 for down
                   final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                           : TouchTarget.ALL_POINTER_IDS;

                   // Clean up earlier touch targets for this pointer id in case they
                   // have become out of sync.
                   removePointersFromTouchTargets(idBitsToAssign);

                   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.
                       //開始遍歷子View,找到能夠接收事件的子View
                       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;
                           }
                           //判斷子view是否能接收pointer事件或者當前的觸控點在子view的邊界內,
                           //如果這兩個沒有一個滿足,就continue,跳到迴圈的下一步,也就是下一個子view
                           if (!canViewReceivePointerEvents(child)
                                   || !isTransformedTouchPointInView(x, y, child, null)) {
                               ev.setTargetAccessibilityFocus(false);
                               continue;
                           }

                           newTouchTarget = getTouchTarget(child);
                           // 如果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);
                           // 這個函式內部呼叫了child.dispatchTouchEvent方法
                           if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                           // 有子View接收這個事件
                               // 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();
                               //把child的TouchTarget加入到連結串列的開頭且返回child的TouchTarget
                               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();
                   }

                   if (newTouchTarget == null && mFirstTouchTarget != null) {
                       // Did not find a child to receive the event.
                       // Assign the pointer to the least recently added target.
                       newTouchTarget = mFirstTouchTarget;
                       while (newTouchTarget.next != null) {
                           newTouchTarget = newTouchTarget.next;
                       }
                       newTouchTarget.pointerIdBits |= idBitsToAssign;
                   }
               }
           }

           //這裡有三種情況:一種是當前ViewGroup攔截了事件,一種是沒有子View,還有一種是子View的dispatchTouchEvent方法返回了false
           //這三種情況下就交給當前ViewGroup進行處理
           // 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);
           } else {
             // 我們發現前面只是對ACTION_DOWN進行了分發,當某個子當某個子View返回true時,會中止Down事件的分發,
             // 同時在ViewGroup中記錄該子View。接下去的Move和Up事件將由該子View直接進行處理,如下所示。
               // Dispatch to touch targets, excluding the new touch target if we already
               // dispatched to it.  Cancel touch targets if necessary.
               TouchTarget predecessor = null;
               TouchTarget target = mFirstTouchTarget;
               while (target != null) {
                   final TouchTarget next = target.next;
                   if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                       handled = true;
                   } else {
                       final boolean cancelChild = resetCancelNextUpFlag(target.child)
                               || intercepted;
                       if (dispatchTransformedTouchEvent(ev, cancelChild,
                               target.child, target.pointerIdBits)) {
                           handled = true;
                       }
                       if (cancelChild) {
                           if (predecessor == null) {
                               mFirstTouchTarget = next;
                           } else {
                               predecessor.next = next;
                           }
                           target.recycle();
                           target = next;
                           continue;
                       }
                   }
                   predecessor = target;
                   target = next;
               }
           }

           // Update list of touch targets for pointer up or cancel, if needed.
           if (canceled
                   || actionMasked == MotionEvent.ACTION_UP
                   || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
               resetTouchState();
           } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
               final int actionIndex = ev.getActionIndex();
               final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
               removePointersFromTouchTargets(idBitsToRemove);
           }
       }

       if (!handled && mInputEventConsistencyVerifier != null) {
           mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
       }
       return handled;
   }

以上對ViewGroup的dispatchTouchEvent方法進行了分析,總結如下:

1、每次當觸控事件為ACTION_DOWN的時候就會清除之前的狀態,開始一次新的事件分發
2、如果當前ViewGroup攔截了ACTION_DOWN,那麼剩下的ACTION_UP,ACTION_MOVE事件都是交給它處理
3、在自定義繼承自ViewGroup的View的時候,通過重寫onInterceptTouchEvent對事件進行攔截,事件攔截僅僅是針對於ViewGroup,對於View來說不存在事件攔截的說法
4、ViewGroup當中有一個requestDisallowInterceptTouchEvent(boolean disallowIntercept)方法,可以用來設定是否攔截,一般用在子View當中,通過呼叫父View的這個方法來阻止父View進行事件攔截
5、有三種情況,觸控事件會交給當前的ViewGroup進行處理,此時就把ViewGroup當成普通的View,走的是View事件分發邏輯,呼叫的是View的dispatchTouchEvent方法:

  • 一種是當前ViewGroup攔截了事件
  • 一種是沒有子View,
  • 還有一種是子View的dispatchTouchEvent方法返回了false

6、事件分發只是針對ACTION_DOWN進行了分發,當某個子View返回true時,會中止Down事件的分發,同時在ViewGroup中記錄該子View。接下去的Move和Up事件將由該子View直接進行處理!這個就有點類似於:一旦一個活交給你幹了,你就得幹到底的意思!

View事件分發機制之dispatchTouchEvent方法

以上講的是ViewGroup的事件分發機制,對於View來說,觸控事件都是由父ViewGroup分發而來,呼叫的是View的dispatchTouchEvent方法,如下所示:

public boolean dispatchTouchEvent(MotionEvent event) {
       // If the event should be handled by accessibility focus first.
       if (event.isTargetAccessibilityFocus()) {
           // We don't have focus or no virtual descendant has it, do not handle the event.
           if (!isAccessibilityFocusedViewOrHost()) {
               return false;
           }
           // We have focus and got the event, then use normal event dispatch.
           event.setTargetAccessibilityFocus(false);
       }

       boolean result = false;

       if (mInputEventConsistencyVerifier != null) {
           mInputEventConsistencyVerifier.onTouchEvent(event, 0);
       }

       final int actionMasked = event.getActionMasked();
       if (actionMasked == MotionEvent.ACTION_DOWN) {
           // Defensive cleanup for new gesture
           stopNestedScroll();
       }

       if (onFilterTouchEventForSecurity(event)) {
           if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
               result = true;
           }
           //noinspection SimplifiableIfStatement
           ListenerInfo li = mListenerInfo;
           // 如果滿足:mOnTouchListener!=null、View是ENABLED的、mOnTouchListener的onTouch方法返回true,那麼result為true
           if (li != null && li.mOnTouchListener != null
                   && (mViewFlags & ENABLED_MASK) == ENABLED
                   && li.mOnTouchListener.onTouch(this, event)) {
               result = true;
           }
          //如果上面的result為true,那麼就不會呼叫下面的onTouchEvent方法
           if (!result && onTouchEvent(event)) {
               result = true;
           }
       }

       if (!result && mInputEventConsistencyVerifier != null) {
           mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
       }

       // Clean up after nested scrolls if this is the end of a gesture;
       // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
       // of the gesture.
       if (actionMasked == MotionEvent.ACTION_UP ||
               actionMasked == MotionEvent.ACTION_CANCEL ||
               (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
           stopNestedScroll();
       }

       return result;
   }

可以看出:

  • 如果View是使能的、設定了TouchListener介面、介面中的onTouch方法返回true的話,這三個條件同時滿足的話就不會呼叫下面的onTouchEvent方法!
  • 比如對於一個Button, 如果它是使能的,且給它設定一個TouchListener,在onTouch方法裡面返回true,那麼就無法響應點選事件(這個大家可以驗證一下),
  • 按鈕的點選事件以及長按事件是在onTouchEvent方法裡面響應的
  • 當View不使能的時候,是可以繼續呼叫onTouchEvent方法的
  • 如果onTouchEvent返回true,那麼整個dispatchTouchEvent也就返回true,代表當前View消耗了事件。

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();
       // view不使能
       if ((viewFlags & ENABLED_MASK) == DISABLED) {
         //ACTION_UP事件,設定按下狀態為false
           if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
               setPressed(false);
           }
           //一個不使能的view,如果是可點選的依然會消耗這個事件,比如button,即使是不使能的,也會返回true消耗事件,只是不對事件作出響應而已。
           //因為button預設是可點選的,除非手動設定為不可點選
           // 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方法,如果返回true,就消耗事件。
       // 這個觸控代理常常用在擴大View的點選區域,比如一個圖示太小,就可以擴大其點選區域
       if (mTouchDelegate != null) {
           if (mTouchDelegate.onTouchEvent(event)) {
               return true;
           }
       }
       // 如果是可點選的
       if (((viewFlags & CLICKABLE) == CLICKABLE ||
               (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
               (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
           switch (action) {
               case MotionEvent.ACTION_UP:
                   boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                   if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                      // 如果View還沒有獲得焦點的話就主動獲得焦點
                       // 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);
                      }
                      // 如果長按的動作沒有發生且沒有忽略下一個ACTION_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.
                               //這個PerformClick是一個Runnable物件
                               if (mPerformClick == null) {
                                   mPerformClick = new PerformClick();
                               }
                               //把mPerformClick給post到訊息佇列
                               if (!post(mPerformClick)) {
                                // 如果上述Runnable執行失敗,就直接呼叫performClick方法,在這個方法裡面呼叫
                                // OnClickListener回撥介面
                                   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();
                   }
                   mIgnoreNextUpEvent = false;
                   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);
                       // 檢測長按,如果長按下成功,會把mHasPerformedLongPress置為true,這樣點選事件就得不到響應
                       // 也就是說長按事件會遮蔽點選事件
                       checkForLongClick(0, x, y);
                   }
                   break;

               case MotionEvent.ACTION_CANCEL:
                   //復位
                   setPressed(false);
                   removeTapCallback();
                   removeLongPressCallback();
                   mInContextButtonPress = false;
                   mHasPerformedLongPress = false;
                   mIgnoreNextUpEvent = false;
                   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;
       }

       return false;
   }

從以上程式碼可以看出:

  • 一個不使能的view,如果是可點選的依然會消耗這個事件,比如button,即使是不使能的,也會返回true消耗事件,只是不對事件作出響應而已。
    因為button預設是可點選的,除非手動設定為不可點選
  • 這要這個View是可點選的,不管是否使能,都會消耗事件
  • 如果長按檢測成功,會把mHasPerformedLongPress置為true,這樣點選事件就得不到響應,也就是說長按事件會遮蔽點選事件
  • 還有一點,像ImageView這種控制元件預設是不可點選的,但是如果給它設定OnClickListener或者onLongClickListener的話,就會主動把其設定為可點選

以上從原始碼的角度對觸控事件的來源、ViewGroup事件分發機制、View事件分發機制進行了解讀。
在自定義View的時候,當需要對觸控事件進行處理的時候,一般是重寫onTouchEvent方法,擁有子類的View一般還需要重寫onInterceptTouchEvent方法進行事件攔截,要想讓這兩個方法很好的配合使用,就需要熟悉並且理解觸控事件分發機制,再配合之前的View繪製流程,就能自定義出各式各樣的View啦!

感謝大家的閱讀!有啥問題,歡迎指出,謝謝!