Android 事件分發(fā)機制-源碼分析

事件分發(fā).png

說明

  • 具體流程如圖所示

  • 對于dispatchTouchEvent , onTouchEvent 返回 true 就是自己消費了调卑,返回 false 就傳到父View 的onTouchEvent方法

  • ViewGroup 想把事件分發(fā)給自己的 onTouchEvent,需要在onInterceptTouchEvent方法中返回 true 把事件攔截下來

  • ViewGroup 的 onInterceptTouchEvent 默認不攔截园爷,所以 super.onInterceptTouchEvent() = false

  • View(這里指沒有子View)沒有攔截器,所以 View 的dispatchTouchEventsuper.dispatchTouchEvent(event)默認把事件分發(fā)給自己的onTouchEvent

源碼解析(以下內容部分來自書籍《Android開發(fā)藝術探索》)


Activity對點擊事件的分發(fā)過程

  • 當一個點擊事件發(fā)生時,事件首先傳遞給當前 Activity ,由 Activity 的 dispatchTouchEvent() 來進行事件的分發(fā)毕荐,具體的工作是由 Activity 內部 Window來完成倾贰。Window 會將事件傳遞給 decor view, decor view 一般就是當前 Activity 的頂層 View冕碟, 源碼如下:

Activity#dispatchTouchEvent

 public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
  • 事件交給 Activity 所附屬的 Window 進行分發(fā), 如果返回 true 整個循環(huán)就結束了匆浙,返回 false 就表示沒有人要處理安寺,交由 Activity 的onTouchEvent處理;可知 Activity 調用 getWindow().superDispatchTouchEvent(ev)把事件分發(fā)給ViewGroup, 所以我們來看

Windows#superDispatchTouchEvent

public abstract boolean superDispatchTouchEvent(MotionEvent event)
  • 可以看出 Window 是個抽象類首尼,且 Window 唯一實現(xiàn)的是 PhoneWindow挑庶,所以接下來我們看看 PhoneWindow 怎么處理該方法的

PhoneWindow#superDispatchTouchEvent

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
  • 到這里清晰言秸, 是在 PhoneWidow 將事件傳遞給了 DecorView, 至于什么是 DercorView ,看源碼里如何解釋的:
// This is the top-level view of the window, containing the window decor.
    private DecorView mDecor;

這個 mDecor 顯然就是getWindow().getDecorView() 所返回的 View迎捺,而我們通過 setContentView 設置的 View 就是是它的一個子 view井仰。目前事件傳遞到了 DecorView 這里,由于 DecorView 繼承自 FrameLayout 且又是父 View破加,所以最終事件會傳遞給 我們所設置setContentView的頂級 View 一般來說都是 ViewGroup(不傳遞給他怎么響應用戶點擊事件呢??)

ViewGroup 對點擊事件的分發(fā)

  • ViewGroup 對點擊事件的分發(fā)過程主要實現(xiàn)在 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_DOWN 合是,這個很好理解
    • 或者 mFirstTouchTarget != null , 這個從后面代碼可以看出锭环,當事件由 ViewGroup 的子元素成功處理時聪全,mFirstTouchTarget 就會被賦值并指向子元素
  • 且當ACTION_MOVEACTION_UP 事件到來時,由于actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null這個條件為 false 辅辩,將導致 ViewGroup 的 onInterceptTouchEvent 不被調用难礼,并且同一序列中的其他事件都會默認交給他處理。

  • 但是這里有個特殊情況玫锋,那就是FLAG_DISALLOW_INTERCEPT標記尿招,這個標記是子 View 通過requestDisallowInterceptTouchEvent方法來設置李命。一旦設置了該標記棠涮,ViewGroup 將無法攔截除了 ACTION_DOWN 以外的事件盟蚣。為什么說除了ACTION_DOWN 以外的事件,這點從源碼也可以看出:因為在 ViewGroup 在事件分發(fā)時节沦,如果是ACTION_DOWN就會重置FLAG_DISALLOW_INTERCEPT標記键思,將導致子View 設置的這個標記失效。因此 當事件為ACTION_DOWN時 ViewGroup 總是會調用自己的onInterceptTouchEvent來詢問是否攔截事件甫贯。
    我們看看上面代碼的前一句代碼就明白了:
    代碼位置

     // 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();
       }
    
    • 從上面代碼可以看出吼鳞, ViewGroup 在ACTION_DOWN事件時會做重置操作:會在resetTouchState()FLAG_DISALLOW_INTERCEPT標記進行重置,因此子 View調用requestDisallowInterceptTouchEvent方法并不能影響 ViewGroup 對ACTION_DOWN事件的處理
  • 從上面分析我們可以總結出兩點:

    • onInterceptTouchEvent 不是每次事件都會被調用叫搁,如果我們想在當前的 ViewGroup 處理所有的點擊事件赔桌,就要選擇onInterceptTouchEvent方法中處理,只有這個方法能確保每次都被調用
    • FLAG_DISALLOW_INTERCEPT 給我們提供了另一種思路去解決滑動沖突的方法:在子 View 攔截處理

接下來我們看 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);
}
  • 如上面源碼所示常熙,首先操作的是遍歷 ViewGroup 的所有子元素纬乍,然后判斷是否能夠接收到點擊事件碱茁。是否能夠接收點擊事件主要有兩點判斷:

    • 子元素是否在播放動畫
    • 點擊事件的坐標是否在子元素坐標區(qū)域內
      可以看到他調用了dispatchTransformedTouchEvent方法來傳遞事件裸卫,所以我們來看看該方法
      ViewGroup#dispatchTransformedTouchEvent
     private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
              View child, int desiredPointerIdBits) {
          final boolean handled;
    
          // Canceling motions is a special case.  We don't need to perform any transformations
          // or filtering.  The important part is the action, not the contents.
          final int oldAction = event.getAction();
          if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
              event.setAction(MotionEvent.ACTION_CANCEL);
              if (child == null) {
                  handled = super.dispatchTouchEvent(event);
              } else {
                  handled = child.dispatchTouchEvent(event);
              }
              event.setAction(oldAction);
              return handled;
          }
          
      ........
    }
    
    • 可以看到在該方法里如果 傳遞的 child 不是 null 他會直接調用的是子元素的 dispatchTouchEvent 方法來把事件傳遞給子元素
  • 再回到 遍歷 ViewGroup 的所有子元素的方法中,可以看到在循環(huán)的最后過程中纽竣,判斷如果子元素的 dispatchTouchEvent 返回 true 墓贿,那么這個 ViewGroup 就暫時不考慮事件在子元素內部是怎么分發(fā)的茧泪,而且 mFirstTouchTarget 就會被賦值同時跳出 for 循環(huán),如下所示:
    代碼位置

    newTouchTarget = addTouchTarget(child, idBitsToAssign);
    alreadyDispatchedToNewTouchTarget = true;
    break;
    
    • 在這里完成了mFirstTouchTarget的賦值并且終止了對子元素的遍歷聋袋。其實對mFirstTouchTarget的賦值是在addTouchTarget方法里完成的:
      ViewGroup#addTouchTarget
       private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
         final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
         target.next = mFirstTouchTarget;
         mFirstTouchTarget = target;
         return target;
     }
    
    • mFirstTouchTarget 其實是一種單鏈表結構队伟,他是否被賦值將直接影響到 ViewGroup 對事件的攔截策略,若果mFirstTouchTarget 為 null 幽勒,那么 ViewGroup 就默認攔截接下來同一序列中所有的點擊事件
  • 如果遍歷所有的子元素后事件都沒有被合適處理嗜侮,這里包含兩種情況:

    • ViewGroup 沒有子元素
    • 子元素處理了點擊事件,但是在 dispatchTouchEvent 中返回了 false (這一般是因為子元素在 onTouch中返回了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);
     }
    

View對點擊事件的分發(fā)過程

  • View 對點擊事件的處理稍微簡單點啥容,首先看他的 dispatchTouchEvent 方法
    View#dispatchTouchEvent

    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;
      }
    }
    
    • 由于 View 不包含子元素锈颗,所以他無法傳遞事件只能自己處理。從上面的源碼可以看出咪惠,View 對事件的處理首先會判斷有沒有設置 OnTouchListener如果設置了且 OnTouchListener中的 onTouch放回 true击吱,那么 View 的 onTouchEvent 就不會被調用。由此可見 OnTouchListener 的優(yōu)先級高于 onTouchEvent
  • 接著我們再看onTouchEvent的實現(xiàn)遥昧。先看當 View 處于不可用狀態(tài)下的點擊事件處理過程:
    View#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));
    }
    
    • 很明顯:不可用狀態(tài)下的 View 照樣會消耗點擊事件覆醇,盡管它看起來不可用
  • 接著onTouchEvent,如果 View 設置有代理炭臭,那么還會執(zhí)行 TouchDelegateonTouchEvent方法

    if (mTouchDelegate != null) {
      if (mTouchDelegate.onTouchEvent(event)) {
          return true;
      }
    }
    
  • 再看onTouchEvent中對點擊事件的具體處理:

     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();
                           }
                       }
                   }
                   .....
               }
               break;
       }
       ....
       return true;
    }
    
    • 可以看出只要 View 的 CLICKABLELONG_CLICKABLE 有一個為 true永脓,那么他就會消耗這個事件,即 onTouchEvent 返回 true鞋仍,不管他是不是 DISABLE 狀態(tài)憨奸。然后在ACTION_UP發(fā)生時會觸發(fā) 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;
      }
    
    • 若果 View 設置了 OnClickListener,那么performClick() 方法內部會調用它的onClick方法

End. 到此就結束了

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末凿试,一起剝皮案震驚了整個濱河市排宰,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌那婉,老刑警劉巖板甘,帶你破解...
    沈念sama閱讀 222,681評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異详炬,居然都是意外死亡盐类,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,205評論 3 399
  • 文/潘曉璐 我一進店門呛谜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來在跳,“玉大人,你說我怎么就攤上這事隐岛∶睿” “怎么了?”我有些...
    開封第一講書人閱讀 169,421評論 0 362
  • 文/不壞的土叔 我叫張陵聚凹,是天一觀的道長割坠。 經常有香客問我齐帚,道長,這世上最難降的妖魔是什么彼哼? 我笑而不...
    開封第一講書人閱讀 60,114評論 1 300
  • 正文 為了忘掉前任对妄,我火速辦了婚禮,結果婚禮上敢朱,老公的妹妹穿的比我還像新娘剪菱。我一直安慰自己,他們只是感情好拴签,可當我...
    茶點故事閱讀 69,116評論 6 398
  • 文/花漫 我一把揭開白布琅豆。 她就那樣靜靜地躺著,像睡著了一般篓吁。 火紅的嫁衣襯著肌膚如雪茫因。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,713評論 1 312
  • 那天杖剪,我揣著相機與錄音冻押,去河邊找鬼。 笑死盛嘿,一個胖子當著我的面吹牛洛巢,可吹牛的內容都是我干的。 我是一名探鬼主播次兆,決...
    沈念sama閱讀 41,170評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼稿茉,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了芥炭?” 一聲冷哼從身側響起漓库,我...
    開封第一講書人閱讀 40,116評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎园蝠,沒想到半個月后渺蒿,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 46,651評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡彪薛,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,714評論 3 342
  • 正文 我和宋清朗相戀三年茂装,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片善延。...
    茶點故事閱讀 40,865評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡少态,死狀恐怖,靈堂內的尸體忽然破棺而出易遣,到底是詐尸還是另有隱情彼妻,我是刑警寧澤,帶...
    沈念sama閱讀 36,527評論 5 351
  • 正文 年R本政府宣布训挡,位于F島的核電站澳骤,受9級特大地震影響,放射性物質發(fā)生泄漏澜薄。R本人自食惡果不足惜为肮,卻給世界環(huán)境...
    茶點故事閱讀 42,211評論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望肤京。 院中可真熱鬧颊艳,春花似錦、人聲如沸忘分。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,699評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽妒峦。三九已至重斑,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間肯骇,已是汗流浹背窥浪。 一陣腳步聲響...
    開封第一講書人閱讀 33,814評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留笛丙,地道東北人漾脂。 一個月前我還...
    沈念sama閱讀 49,299評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像胚鸯,于是被迫代替她去往敵國和親骨稿。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,870評論 2 361

推薦閱讀更多精彩內容