Android的事件傳遞

在Android尤其是手機(jī)端的開(kāi)發(fā)中,很多情況下涉及到點(diǎn)擊事件,或者說(shuō)是觸摸事件的特殊處理。比如滑動(dòng)沖突等,因此熟知Android事件傳遞的流程就顯得格外重要。今天老衲就帶大家理一理Android事件傳遞的相關(guān)細(xì)節(jié)。

方法介紹

事件序列

是指當(dāng)手指接觸屏幕至手指離開(kāi)屏幕是所產(chǎn)生的一系列Down-->Move-->Up事件。

事件傳遞涉及到三個(gè)方法和兩個(gè)監(jiān)聽(tīng)(onClick也算,原因在源碼分析中可以看到):

分發(fā)事件
//@return True 表示事件被該View處理
public boolean dispatchTouchEvent(MotionEvent ev)

如果事件能夠傳遞到該View(或activity)則該方法一定會(huì)被調(diào)用。

攔截事件
//@return True 表示事件被該View攔截
public boolean onInterceptTouchEvent(MotionEvent ev)

在上述方法的內(nèi)部調(diào)用,同一事件序列下,該方法只執(zhí)行一次

處理事件
//@return True 表示事件被該View消耗
public boolean onTouchEvent(MotionEvent event) 
子元素用來(lái)干預(yù)父元素的事件分發(fā)過(guò)程,ACTION_DOWN除外
public void requestDisallowInterceptTouchEvent
OnTouchListener及OnClickListener
public void onClick(View v)
public boolean onTouch(View v, MotionEvent event)
  1. OnTouchListener內(nèi)回調(diào)的優(yōu)先級(jí)高于onTouchEvent,并且會(huì)根據(jù)Listener返回值來(lái)影響onTouchEvent方法的執(zhí)行。
  2. OnClickListener回調(diào)的執(zhí)行是在OnTouchEvent方法內(nèi),根據(jù)UP,DOWN事件的時(shí)長(zhǎng)來(lái)判斷是否需要執(zhí)行click方法(前提是View可以點(diǎn)擊)

事件傳遞的偽代碼

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {    
  boolean result = false; 
  //如果View需要攔截該事件,則事件處理交給onTouchEvent   
  if (onInterceptTouchEvent(ev)){        
      result = onTouchEvent(ev);    
  }else{        
      //如果View不需要攔截事件,則事件交給子View來(lái)繼續(xù)該判斷。直至事件被消耗
      result = child.dispatchTouchEvent(ev);       
  }    
  return result;
}

事件分發(fā)流程的源碼分析

事件分發(fā)的流程為activity--->window--->View,

Step1. activity-->window

public boolean dispatchTouchEvent(MotionEvent ev) {    
  //空方法,當(dāng)需要知道用戶與設(shè)備在互動(dòng)時(shí)調(diào)用,可重寫(xiě)
  if (ev.getAction() == MotionEvent.ACTION_DOWN) {        
      onUserInteraction();    
  }    
  //通過(guò)window將事件傳遞給View樹(shù)來(lái)處理,當(dāng)返回true(View消耗了)則事件傳遞結(jié)束
  if (getWindow().superDispatchTouchEvent(ev)) {        
      return true;    
  }    
  //如果View并未處理,則交給activity的onTouchEvent來(lái)處理。
  return onTouchEvent(ev);
}

Step2. window-->view

window是一個(gè)抽象類,superDispatchTouchEvent方法的具體實(shí)現(xiàn)是在phoneWindow中。

public class PhoneWindow extends Window implements MenuBuilder.Callback {
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
// This is the view in which the window contents are placed. It is either
// mDecor itself, or a child of mDecor where the contents go.
private ViewGroup mContentParent;

//直接調(diào)用了DecorView的superDispatchTouchEvent方法
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
  return mDecor.superDispatchTouchEvent(event);
}


private void installDecor() {
   if (mDecor == null) {
     mDecor = generateDecor();
     mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
     mDecor.setIsRootNamespace(true);
   }
   if (mContentParent == null) {
     mContentParent = generateLayout(mDecor);
    }
    //設(shè)置title或actionBar以及其他style樣式的邏輯
    ...
}

接下來(lái),我們需要看一下DecorView內(nèi)部的方法實(shí)現(xiàn)邏輯。如下代碼,它直接又扔給了super的dispatchTouchEvent。

private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
  ...
  public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
  }
 ...
}

這個(gè)super就是FrameLayout。我們都知道DecorView其實(shí)就是一個(gè)FrameLayout,我們?cè)陂_(kāi)發(fā)過(guò)程中寫(xiě)的所以得布局文件,都會(huì)被扔到這個(gè)FrameLayout中來(lái)展示,接下來(lái)我們就開(kāi)始了本文的重中之重,onTouchEvent事件的分析了。

Step3. View內(nèi)部的源碼解析

Step3.1 View的dispatchTouchEvent邏輯

我們首先看一下View內(nèi)部的事件分發(fā)邏輯,相對(duì)于ViewGroup,View的事件分發(fā)更簡(jiǎn)單。

public boolean dispatchTouchEvent(MotionEvent event) {  
  // 焦點(diǎn)處理
  // 滑動(dòng)處理

  if (onFilterTouchEventForSecurity(event)) {    
    ... 
    //View監(jiān)聽(tīng)的處理,如果有onTouchListener,則執(zhí)行onTouch事件,
    //然后,根據(jù)onTouch事件的返回結(jié)果來(lái)判斷是否要執(zhí)行View的onTouchEvent事件。
    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內(nèi)的事件分發(fā)邏輯非常清晰:

  1. OnTouchListener是否存在,如果存在則執(zhí)行onTouch方法
  2. 執(zhí)行onTouchEvent方法。

其他就是一些焦點(diǎn)和滑動(dòng)情況下的特殊處理。本文不做深究。

Step3.2 ViewGroup的dispatchTouchEvent邏輯

接下來(lái),我們來(lái)看下ViewGroup內(nèi)部的事件分發(fā)邏輯

Step3.2.1 ViewGroup內(nèi)部的攔截邏輯
// 事件傳遞隊(duì)列(View的嵌套隊(duì)列)中首個(gè)消耗事件序列的目標(biāo)(View)
private TouchTarget mFirstTouchTarget;

public boolean dispatchTouchEvent(MotionEvent ev) {
  //焦點(diǎn)處理

  //新的點(diǎn)擊序列的初始化
  if (actionMasked == MotionEvent.ACTION_DOWN) {     
    //當(dāng)開(kāi)始一個(gè)新的touch事件序列時(shí),先丟棄之前事件產(chǎn)生的狀態(tài)
    //因?yàn)閍pp的切換,ANR或者其他狀態(tài)的改變會(huì)導(dǎo)致Android丟掉之前手勢(shì)產(chǎn)生的UP及CANCLE事件
    cancelAndClearTouchTargets(ev);    
    resetTouchState();
  }

  //檢查攔截設(shè)置
  final boolean intercepted;
  // 重點(diǎn)1. 
  if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {    
    //重點(diǎn)2. 
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;    
    if (!disallowIntercept) {        
      //根據(jù)onInterceptTouchEvent的返回值確定是否攔截該事件序列
      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;
  }
}

重點(diǎn)1:“或” 的判斷邏輯,前半部分用來(lái)標(biāo)識(shí)事件類型(DOWN,MOVE或者UP),后半部分是用來(lái)判斷之前是否有接收了該事件序列中的View,如果任意為true,則接下來(lái)該ViewGroup的onInterceptTouchEvent方法不會(huì)再被調(diào)用,該事件序列中剩余的事件都交給mFirstTouchTarget處理。

重點(diǎn)2:之前提到過(guò),子View可以通過(guò)requestDisallowInterceptTouchEvent影響父View的事件分發(fā)。當(dāng)FLAG_DISALLOW_INTERCEPT被子View修改以后,父View將只會(huì)攔截DOWN事件,而不會(huì)攔截其他事件。至于原因可以在resetTouchState方法中找到。

Step3.2.2ViewGroup內(nèi)部的事件分發(fā)邏輯
//最近一次處理事件的View的索引
int mLastTouchDownIndex

//子View個(gè)數(shù)
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {    
  //獲取事件坐標(biāo)
  final float x = ev.getX(actionIndex);    
  final float y = ev.getY(actionIndex);    
  // 找到一個(gè)能接受事件的子View
  // 從外向內(nèi)掃描子View,根據(jù)Z軸以及繪制的順序生成的View的list
  final ArrayList<View> preorderedList = buildTouchDispatchChildList();    
  final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled();    
  final View[] children = mChildren;    
  for (int i = childrenCount - 1; i >= 0; i--) {      
    //根據(jù)繪制的順序獲取index并得到View  
    final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);        
    final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);        
    ...    
    //判斷是否可以接收事件
    //1.View可見(jiàn)
    //2.坐標(biāo)在View的區(qū)域內(nèi)
    if (!canViewReceivePointerEvents(child)|| !isTransformedTouchPointInView(x, y, child, null)) {  
        ev.setTargetAccessibilityFocus(false);            
        continue;        
    }        
    ...       
    //重點(diǎn)
    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {            
      // 子View希望接收在它范圍內(nèi)的事件
      mLastTouchDownTime = ev.getDownTime();            
      if (preorderedList != null) {                
        //根據(jù)childindex找到View集合中的原始索引下標(biāo)    
        for (int j = 0; j < childrenCount; j++) {                    
          if (children[childIndex] == mChildren[j]) {
            mLastTouchDownIndex = j;                       
            break;                    
          }                
        }            
    } else {                
      mLastTouchDownIndex = childIndex;            
    }            
    ...    
  }        
  //事件沒(méi)有被能夠獲取到焦點(diǎn)的對(duì)象處理,清除flag,然后分發(fā)到所有的子View
  ev.setTargetAccessibilityFocus(false);    
  }    
//清理預(yù)排序的View集合
if (preorderedList != null) 
  preorderedList.clear();
}

上述代碼標(biāo)注重點(diǎn)的位置,它的具體實(shí)現(xiàn)如下,有子View則調(diào)用子View的dispatchTouchEvent,沒(méi)有則調(diào)用父類的的dispatchTouchEvent方法,如果子View依然是一個(gè)ViewGroup則遞歸,直至最后的View為止。

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {     
  // CANCLE事件的特殊處理
  ...
  // 執(zhí)行必要的轉(zhuǎn)換和分發(fā)
  if (child == null) {    
      handled = super.dispatchTouchEvent(transformedEvent);
  } else {    
      final float offsetX = mScrollX - child.mLeft;    
      final float offsetY = mScrollY - child.mTop;    
      transformedEvent.offsetLocation(offsetX, offsetY);    
      if (! child.hasIdentityMatrix()) {        
          transformedEvent.transform(child.getInverseMatrix());    
      }    
      handled = child.dispatchTouchEvent(transformedEvent);
  }
 ...
}

我們?cè)谝婚_(kāi)始分析了View和ViewGroup的dispatchTouchEvent方法,里面都有調(diào)用onTouchEvent事件的邏輯,根據(jù)該方法的調(diào)用結(jié)果來(lái)確定該事件是否被消耗,一旦被消耗,則dispatchTransformedTouchEvent的返回值為true,此時(shí),ViewGourp的dispatchTouchEvent會(huì)調(diào)出遍歷子view的for循環(huán),進(jìn)入下一事件的分發(fā)過(guò)程。

Step4. onTouchEvent事件的處理邏輯

public boolean onTouchEvent(MotionEvent event) {
  //獲取事件的坐標(biāo)及ACTION
  ...
  if ((viewFlags & ENABLED_MASK) == DISABLED) {    
  //一個(gè)可點(diǎn)擊的disabled的View依然可以消耗事件,只是不會(huì)做出響應(yīng)
  return (((viewFlags & CLICKABLE) == CLICKABLE 
        || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) 
        || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
  }
  //重點(diǎn)1.
  if (((viewFlags & CLICKABLE) == CLICKABLE 
      || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) 
      || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) 
    {    
      switch (action) {        
        case MotionEvent.ACTION_UP:
          //如果View可以獲取焦點(diǎn),則首先讓該View獲取焦點(diǎn)
          //這個(gè)View的按下?tīng)顟B(tài)即將被釋放,修改樣式(selector)及動(dòng)畫(huà)
          if (!post(mPerformClick)) {    
            //重點(diǎn)2,執(zhí)行點(diǎn)擊事件
            performClick();
          }
          break;
        case MotionEvent.ACTION_DOWN:
          ...
          //重點(diǎn)3
          checkForLongClick(0, x, y);
          break;


}

重點(diǎn)1 : 只要View的CLICKABLE或者LONG_CLICKABLE任意一個(gè)屬性為true,那就會(huì)消耗該事件

重點(diǎn)2 : 在onTouchEvent方法會(huì)間接調(diào)用OnClickListener方法的回調(diào)。

public boolean performClick() {    
  final boolean result;    
  final ListenerInfo li = mListenerInfo;    
  if (li != null && li.mOnClickListener != null) {     
    ...      
    //如果執(zhí)行了點(diǎn)擊事件,則表示該事件被消耗了。
    li.mOnClickListener.onClick(this);        
    result = true;    
  } else {        
    result = false;    
  }    
  ...
  return result;
}

重點(diǎn)3 : 在ACTION_DOWN的判斷邏輯里會(huì)進(jìn)行長(zhǎng)按的校驗(yàn),如果判斷不是長(zhǎng)按事件則會(huì)調(diào)用UP分支的performClick方法

事件傳遞所涉及方法的執(zhí)行順序

以下日志只涉及DOWN和UP兩種事件

情景1

多層嵌套,無(wú)任何攔截,子View不處理事件

//activity分發(fā)開(kāi)始
MainActivity---->dispatchTouchEvent start
//各層View嘗試攔截
GrandParentView--->onInterceptTouchEvent
ParentView--->onInterceptTouchEvent
//如果有Listener則先執(zhí)行監(jiān)聽(tīng)的回調(diào)
OnTouchListener---->處理事件
//最終執(zhí)行onTouchEvent
MyImageView--->onTouchEvent
//--------------------------------事件回傳
MyImageView--->dispatchTouchEvent
ParentView--->onTouchEvent
ParentView--->dispatchTouchEvent
GrandParentView--->onTouchEvent
GrandParentView--->dispatchTouchEvent
//回傳到activity然后結(jié)束
MainActivity---->onTouchEvent
MainActivity---->dispatchTouchEvent end
//該事件序列的剩余事件無(wú)需再次層層傳遞,直接在activity中處理
MainActivity---->dispatchTouchEvent start
MainActivity---->onTouchEventfalse
MainActivity---->dispatchTouchEvent end

此情境下情況下,首先從activity開(kāi)始,由外到內(nèi)傳遞給View的onTouchEvent,然后再由內(nèi)到外傳遞給activity的onTouchEvent,最終消耗掉此事件。這里的事件是指DOWN事件,因?yàn)闆](méi)有子View來(lái)處理該DOWN事件,所以最終由activity出面處理,那么接下來(lái)的事件序列也沒(méi)有必要繼續(xù)向內(nèi)傳遞了,可以直接在activity中處理了。

舉個(gè)栗子,CTO吩咐技術(shù)總監(jiān)一個(gè)任務(wù),技術(shù)總監(jiān)交代給你,但是你沒(méi)有完成又扔給總監(jiān),總監(jiān)也沒(méi)完成又扔給CTO,此時(shí)接下來(lái)的任務(wù)CTO就會(huì)自己去處理而不是交給下級(jí)。

情景2

多層嵌套,無(wú)任何攔截,子View處理事件

//activity分發(fā)開(kāi)始
MainActivity---->dispatchTouchEvent start
//各層View嘗試攔截
GrandParentView--->onInterceptTouchEvent
ParentView--->onInterceptTouchEvent
//監(jiān)聽(tīng)處理
OnTouchListener---->處理事件
//事件最終被消耗
MyImageView--->onTouchEvent
//回調(diào)各層級(jí)的dispatchTouchEvent
MyImageView--->dispatchTouchEvent
ParentView--->dispatchTouchEvent
GrandParentView--->dispatchTouchEvent
MainActivity---->dispatchTouchEvent end
//開(kāi)始UP事件的分發(fā)
MainActivity---->dispatchTouchEvent start
GrandParentView--->onInterceptTouchEvent
ParentView--->onInterceptTouchEvent
OnTouchListener---->處理事件
MyImageView--->onTouchEvent
MyImageView--->dispatchTouchEvent
ParentView--->dispatchTouchEvent
GrandParentView--->dispatchTouchEvent
MainActivity---->dispatchTouchEvent end

相較于上種情況,本次的事件傳遞多了UP事件的傳遞。

再舉個(gè)栗子,CTO吩咐技術(shù)總監(jiān)一個(gè)任務(wù),技術(shù)總監(jiān)交代給你,你完美的完成了,接下來(lái)的任務(wù)CTO會(huì)繼續(xù)交托給你執(zhí)行。

情景3

多層嵌套,無(wú)攔截,但是dispatchTouchEvent與onTouchEvent聲明的行為不一致(返回結(jié)果不同)

一般來(lái)說(shuō),我們會(huì)在dispatchTouchEvent方法中聲明說(shuō)事件序列由該View處理,而在onTouchEvent中確定是否真正處理了該事件序列,所以基本來(lái)說(shuō)他們是一致的,但是假如不一致,那么就會(huì)出現(xiàn)如下執(zhí)行順序

3.1

聲明要處理,但是實(shí)際上沒(méi)有處理,則該事件會(huì)被吞噬

dispatch true onTouchEvent false

MainActivity---->dispatchTouchEvent start : 
GrandParentView--->onInterceptTouchEvent
ParentView--->onInterceptTouchEvent
ParentView--->onTouchEvent
ParentView--->dispatchTouchEvent
GrandParentView--->dispatchTouchEvent
MainActivity---->dispatchTouchEvent end : 
3.2

聲明不處理,但是實(shí)際上處理了,則該事件會(huì)當(dāng)做沒(méi)人處理回傳給activity中,并且后續(xù)的事件也不會(huì)再向下傳遞了。

MainActivity---->dispatchTouchEvent start : 
GrandParentView--->onInterceptTouchEvent
ParentView--->onInterceptTouchEvent
ParentView--->onTouchEvent
ParentView--->dispatchTouchEventtrue
GrandParentView--->onTouchEvent
GrandParentView--->dispatchTouchEvent
MainActivity---->onTouchEvent
MainActivity---->dispatchTouchEvent end : 
MainActivity---->dispatchTouchEvent start : 
MainActivity---->onTouchEvent
MainActivity---->dispatchTouchEvent end : 
情景4

多層嵌套,有攔截

4.1 只攔截,不處理
MainActivity---->dispatchTouchEvent start :
GrandParentView--->onInterceptTouchEvent
GrandParentView--->onTouchEvent
GrandParentView--->dispatchTouchEvent
MainActivity---->onTouchEvent
MainActivity---->dispatchTouchEvent end :
MainActivity---->dispatchTouchEvent start : 
MainActivity---->onTouchEvent
MainActivity---->dispatchTouchEvent end : 

只攔截,不處理的話,則事件最終仍會(huì)返回給activity處理

4.1 攔截并處理
MainActivity---->dispatchTouchEvent start : 
GrandParentView--->onInterceptTouchEvent
GrandParentView--->onTouchEvent
GrandParentView--->dispatchTouchEvent
MainActivity---->dispatchTouchEvent end : 
MainActivity---->dispatchTouchEvent start : 
GrandParentView--->onTouchEvent
GrandParentView--->dispatchTouchEvent
MainActivity---->dispatchTouchEvent end : 

此時(shí)和最內(nèi)層View處理事件的邏輯一樣。

情景5

涉及OnClickListener的處理

當(dāng)給一個(gè)View設(shè)置OnClickListener時(shí),onClick回調(diào)方法會(huì)在最后一步執(zhí)行,它的優(yōu)先級(jí)是最低的,此處就不寫(xiě)打印日志了。因?yàn)榇颂幦罩九c情景1類似,只是在最后一步執(zhí)行onClick方法而已,需要注意的是onClick回調(diào)能夠執(zhí)行的前提是,View的dispatchTouchEvent方法返回true,onTouchEvent無(wú)所謂,否則無(wú)法接收點(diǎn)擊事件回調(diào),至于原因在源碼中也可以找到。

至此,Android的事件分發(fā)就已經(jīng)介紹完了。老衲也要苦逼的開(kāi)始找工作了。祝大家工作順利。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容