Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> Android編程入門 >> 深入了解觸摸事件的分發

深入了解觸摸事件的分發

編輯:Android編程入門

1. 觸摸動作及事件序列

(1)觸摸事件的動作

    觸摸動作一共有三種:ACTION_DOWN、ACTION_MOVE、ACTION_UP。當用戶手指接觸屏幕時,便產生一個動作為ACTION_DOWN的觸摸事件,此時若用戶的手指立即離開屏幕,會產生一個動作為ACTION_UP的觸摸事件;若用戶手指接觸屏幕後繼續滑動,當滑動距離超過了系統中預定義的距離常數,則產生一個動作為ACTION_MOVE的觸摸事件,系統中預定義的用來判斷用戶手指在屏幕上的滑動是否是一個ACTION_MOVE動作的這個距離常量叫做TouchSlop,可通過ViewConfiguration.get(getContext()).getScaledTouchSlop()獲取。

(2)事件序列

    當用戶的手指接觸屏幕,在屏幕上滑動,又離開屏幕,這個過程會產生一系列觸摸事件:ACTION_DOWN-->若干個ACTION_MOVE-->ACTION_UP。這一系列觸摸事件即為一個事件序列。

 

2. 觸摸事件的分發

(1)概述

    當產生了一個觸摸時間後,系統要負責把這個觸摸事件給一個View(TargetView)來處理,touch事件傳遞到TargetView的過程即為touch事件的分發。

    觸摸事件的分發順序:Activity-->頂級View-->頂級View的子View-->. . .-->Target View

    觸摸事件的響應順序:TargetView --> TargetView的父容器 --> . . . -->頂級View -->Activity

(2)toush事件分發的具體過程

  a. Activity對touch事件的分發

    當用戶手指接觸屏幕時,便產生了一個touch事件,封裝了touch事件的MotionEvent最先被傳遞給當前Activity,Activity的dispatchTouchEvent方法負責touch事件的分發。分發touch事件的實際工作由當前Activity的Window完成,而Window會將touch事件傳遞給DecorView(當前用戶界面頂級View)。Activity的dispatchTouchEvent方法代碼如下:

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

    根據以上代碼可以知道,touch事件會交由Window的superDispatchTouchEvent進行分發,若這個方法返回true,意味touch事件的分發過程結束,返回false則說明經過層層分發,沒有子View對這個事件進行處理,即所有子View的onTouchEvent方法都返回false(即這個touch事件沒有被“消耗”)。這時會調用Activity的onTouchEvent方法來處理這個touch事件。

    在Window的superDispatchTouchEvent方法中,首先會把touch事件分發給DecorView,因為它是當前用戶界面的頂級View。Window的superDispatchTouchEvent方法如下:

 public abstract boolean superDispatchTouchEvent(MotionEvent ev);

    是個抽象方法,這個方法由Window的實現類PhoneWindow實現,PhoneWindow的superDispatchTouchEvent方法的代碼如下:

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

    由以上代碼可得,PhoneWindow的superDispatchTouchEvent方法實際上是通過DecorView的superDispatchTouchEvent方法來完成自己的工作,也就是說,當前Activity的Window直接將這個touch事件傳遞給了DecorView。也就是說,目前touch事件已經經過了如下的分發:Activity-->Window-->DecorView。

b. 頂級View對touch事件的分發

    經過Activity與Window的分發,現在touch事件已經被傳遞到了DecorView的dispatchTouchEvent方法中。DecorView本質上是一個ViewGroup(更具體的說是FrameLayout),ViewGroup的dispatchTouchEvent方法所做的工作可以分為如下幾個階段,第一個階段的主要代碼如下:

1 //Handle an initial down.
2 if (actionMasked == MotionEvent.ACTION_DOWN) {
3     //Throw away all previous state when starting a new touch gesture.
4     //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.
5     cancelAndClearTouchTargets(ev);
6     resetTouchState();
7 }

    第一階段的主要工作有倆:一是在第6行的resetTouchState方法中完成了對FLAG_DISALLOW_INTERCEPT標記的重置;二是第5行的cancelAndClearTouchTargets方法會清除當前MotionEvent的touch target。關於FLAG_DISALLOW_INTERCEPT標記和touch target,在下文會有相關說明。

    第二階段的主要工作是決定當前ViewGroup是否攔截本次的touch事件,主要代碼如下:

 //Check for interception.
 final boolean intercepted;
 if (actionMasked == MotionEvent.ACTION_DOWM || 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.
     intercept =true;
 }

    由以上代碼我們可以知道,當一個touch事件被傳遞到ViewGroup時,會先判斷這個touch事件的動作是否是ACTION_DOWN,如果這個事件是ACTION_DOWN或者mFirstTouchTarget不為null,就會根據FLAG_DISALLOW_INTERCEPT標記決定是否攔截這個touch事件。那麼mFirstTouchTarget是什麼呢?當touch事件被ViewGroup的子View成功處理時,mFirstTouchTarget就會被賦值為成功處理touch事件的View,也就是上面提高的touch target。

    總結一下上述代碼的流程:在子View不干預ViewGroup的攔截的情況下(上述代碼中的disallowIntercept為false),若當前事件為ACTION_DOWN或者mFirstTouchTarget不為空,則會調用ViewGroup的onInterceptTouchEvent方法來決定最終是否攔截此事件;否則(沒有TargetView並且此事件不是ACTION_DOWN),當前ViewGroup就攔截下此事件。 一旦ViewGroup攔截了某次touch事件,那麼mFirstTouchTarget就不會被賦值,因此當再有ACTION_MOVE或是ACTION_UP傳遞到該ViewGroup時,mTouchTarget就為null,所以上述代碼第3行的條件就為false,ViewGroup會攔截下來。由此可得到的結論是:一旦ViewGroup攔截了某次事件,則同一事件序列中的剩余事件也會它默認被攔截而不會再詢問是否攔截(即不會再調用onInterceptTouchEvent)。

    這裡存在一種特殊情形,就是子View通過requestDisallowInterceptTouchEvent方法設置父容器的FLAG_DISALLOW_INTERCEPT為true,這個標記指示是否不允許父容器攔截,為true表示不允許。這樣做能夠禁止父容器攔截除ACTION_DOWN以外的所有touch事件。之所以不能夠攔截ACTION_DOWN事件,是因為每當ACTION_DOWN事件到來時,都會重置FLAG_DISALLOW_INTERCEPT這個標記位為默認值(false),所以每當開始一個新touch事件序列(即到來一個ACTION_DOWN動作),都會通過調用onInterceptTouchEven詢問ViewGroup是否攔截此事件。當ACTION_DOWN事件到來時,重置標記位的工作是在上面的第一階段完成的。   

    接下來,會進入第三階段的工作

 final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL;
 final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
 TouchTarget newTouchTarget = null;
 boolean alreadyDispatchedToNewTouchTarget = false;
 if (!canceled && !intercepted) {
     // 不是ACTION_CANCEL並且不攔截
     if (actionMasked == MotionEvent.ACTION_DOWN) {
           // 若當前事件為ACTION_DOWN則去尋找這次事件新出現的touch target
           final int actionIndex = ev.getActionIndex(); // always 0 for down
 
           ...
 
           final int childrenCount = mChildrenCount;
           if (newTouchTarget == null && childrenCount != 0) {
               // 根據觸摸的坐標尋找能夠接收這個事件的touch target
               final float x = ev.getX(actionIndex);
               final float y = ev.getY(actionIndex);
 
               final View[] children = mChildren;
               // 遍歷所有子View
               for (int i = childrenCount - 1; i >= 0; i--) {
                   final int childIndex = i;
                   final View child = children[childIndex];
                   // 尋找可接收這個事件並且touch事件坐標在其區域內的子View
                   if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) {
                       continue;
                   }
 
                   newTouchTarget = getTouchTarget(child); // 找到了符合條件的子View,賦值給newTouchTarget
                   if (newTouchTarget != null) {
                       //Child is already receiving touch within its bounds.
                       //Give it the new pointer in addition to ones it is handling.
                       newTouchTarget.pointerIdBits |= idBitsToAssign;
                       break;
                   }
                   resetCancelNextUpFlag(child);
                   // 把ACTION_DOWN事件傳遞給子組件進行處理
                   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();
                       //把mFirstTouchTarget賦值為newTouchTarget,此子View成為新的touch事件的起點
                       newTouchTarget = addTouchTarget(child, idBitsToAssign);
                       alreadyDispatchedToNewTouchTarget = true;
                       break;
                  }                      
              }
          }
     }
 }

    當ViewGroup不攔截本次事件,則touch事件會分發給它的子View進行處理,相關代碼從第21行開始:遍歷所有ViewGroup的子View,尋找能夠處理此touch事件的子View,若一個子View不在播放動畫並且touch事件坐標位於其區域內,則該子View能夠處理此touch事件,並且會把該子View賦值給newTouchTarget。

    若當前遍歷到的子View能夠處理此touch事件,就會進入第38行的dispatchTransformedTouchEvent方法,該方法實際上調用了子View的dispatchTouchEvent方法。dispatchTransformedTouchEvent方法中相關的代碼如下:

 if (child == null) {
     handled = super.dispatchTouchEvent(event);
 } else {
     handled = child.dispatchTouchEvent(event);
 }

    若dispatchTransformedTouchEvent方法傳入的child參數不為null,則會調用child(即處理touch事件的子View)的dispatchTouchEvent方法。若該子View的dispatchTouchEvent方法返回true,則dispatchTransformedTouchEvent方法也會返回true,則表示成功找到了一個處理該事件的touch target,會在第55行把newTouchTarget賦值給mFirstTouchTarget(這一賦值過程是在addTouchTarget方法內部完成的),並跳出對子View遍歷的循環。若子View的dispatchTouchEvent方法返回false,ViewGroup就會把事件分發給下一個子View。

    若遍歷了所有子View後,touch事件都沒被處理(該ViewGroup沒有子View或是所有子View的dispatchTouchEvent返回false),ViewGroup會自己處理touch事件,相關代碼如下:

 if (mFirstTouchTarget == null) {
     handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
 }

    由以上代碼可知,ViewGroup自己處理touch事件時,會調用dispatchTransformedTouchEvent方法,傳入的child參數為null。根據上文的分析,傳入的chid為null時,會調用super.dispatchTouchEvent方法,即調用View類的dispatchTouchEvent方法。

 

c. View對touch事件的處理

    View的dispatchTouchEvent方法的主要代碼如下:

 public boolean dispatchTouchEvent(MotionEvent event) {
     boolean result = false;
     . . .
     
     if (onFilterTouchEventForSecurity(event)) {
         //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;
         }
         . . .
         return result;
 }

    由上述代碼可知,View對touch事件的處理過程如下:由於View不包含子元素,所以它只能自己處理事件。它首先會判斷是否設置了OnTouchListener,若設置了,會調用onTouch方法,若onTouch方法返回true(表示該touch事件已經被消耗),則不會再調用onTouchEvent方法;若onTouch方法返回false或沒有設置OnTouchListener,則會調用onTouchEvent方法,onTouchEvent對touch事件進行具體處理的相關代碼如下:

 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) {
                 . . .
                 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) {
                             mPerformClck = new PeformClick();
                         }
                         if (!post(mPerformClick)) {
                             performClick();
                         }
                     }
                 }
                 . . .
             }
             break;
     }
     . . .
     return true;
 }

    由以上代碼可知,只要View的CLICKABLE屬性和LONG_CLICKABLE屬性有一個為true(View的CLICKABLE屬性和具體View有關,LONG_CLICKABLE屬性默認為false,setOnClikListener和setOnLongClickListener會分別自動將以上倆屬性設為true),那麼這個View就會消耗這個touch事件,即使這個View處於DISABLED狀態。若當前事件是ACTION_UP,還會調用performClick方法,該View若設置了OnClickListener,則performClick方法會在其內部調用onClick方法。performClick方法代碼如下:

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

 

以上是我學習Android中觸摸事件分發後的簡單總結,很多地方敘述的還不夠清晰准確,如有問題歡迎大家在評論區一起討論 :)

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved