Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 詳解Android事件的分發、攔截和執行

詳解Android事件的分發、攔截和執行

編輯:關於Android編程

在平常的開發中,我們經常會遇到點擊,滑動之類的事件。有時候不同的view之間也存在各種滑動沖突。比如布局的內外兩層都能滑動的話,那麼就會出現沖突了。這個時候我們就需要了解Android的事件分發機制。
Android的觸摸事件分發過程由三個很重要的方法來共同完成:dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent。我先將這三個方法大體的介紹一下。

 •public boolean dispatchTouchEvent(MotionEvent ev) 

用來進行事件的分發。如果事件能夠傳遞給當前View,那麼此方法一定會被調用,返回結果受當前View的onTouchEvent和下級View的dispatchTouchEvent方法的影響,表示是否消耗當前事件。ACTION_DOWN的dispatchTouchEvent()返回true,後續事件(ACTION_MOVE、ACTION_UP)會再傳遞,如果返回false,dispatchTouchEvent()就接收不到ACTION_UP、ACTION_MOVE。簡單的說,就是當dispatchTouchEvent在進行事件分發的時候,只有前一個action返回true,才會觸發後一個action。

 •public boolean onInterceptTouchEvent(MotionEvent event) 

這個方法是在dispatchTouchEvent方法中調用的,用來攔截某個事件的。如果當前View攔截了某個事件,那麼在同一個事件序列中,此方法不會被再次調用,返回的結果表示是否攔截當前事件。它是ViewGroup提供的方法,默認返回false。

 •public boolean onTouchEvent(MotionEvent event) 

在dispatchTouchEvent方法中調用,用來處理點擊事件,返回結果表示是否消耗掉當前事件(true表示消耗,false表示不消耗),如果不消耗,則在同一個事件序列中,當前View無法再次接收到事件。View和ViewGroup都有該方法,View默認返回true,表示消費了這個事件。

View裡,有兩個回調函數 :

public boolean dispatchTouchEvent(MotionEvent ev);   
public boolean onTouchEvent(MotionEvent ev);

ViewGroup裡,有三個回調函數 :

public boolean dispatchTouchEvent(MotionEvent ev);   
public boolean onInterceptTouchEvent(MotionEvent ev);   
public boolean onTouchEvent(MotionEvent ev);

上述三個方法中有什麼區別和關系呢?下面用一段偽代碼表示:

public boolean dispatchTouchEvent(MotionEvent ev) { 
 boolean consume = false; 
 if(onInterceptTouchEvent(ev)){ 
  consume = onTouchEvent(ev); 
 } else { 
  consume = child.dispatchTouchEvent(ev); 
 } 
 return consume; 
} 

 通過上面的偽代碼大家可能對點擊事件的傳遞規則有了更清楚的認識,即:對於一個根ViewGroup來說,點擊事件產生後,首先會傳遞給它,這時它的dispatchTouchEvent就會被調用,如果這個ViewGroup的onInterceptTouchEvent方法返回true表示它要攔截此事件,接著這個事件就會交給這個ViewGroup處理,即它的onTouchEvent方法就會被調用;如果這個ViewGroup的onInterceptTouchEvent方法返回false,就表示它不攔截此事件,這是當前事件就會繼續傳遞給它的子元素,接著子元素的dispatchTouchEvent方法就會被調用,如此反復直到事件被最終處理。

下面的幾張圖參考自[eoe]:

 •圖一:ACTION_DOWN都沒被消費

 

•圖二(一):ACTION_DOWN被View消費了


•圖二(二):後續ACTION_MOVE和UP在不被攔截的情況下都會去找VIEW


•圖三:後續的被攔截了


•圖四:ACTION_DOWN一開始就被攔截

View事件分發源碼分析:
 •dispatchTouchEvent方法: 

public boolean dispatchTouchEvent(MotionEvent event) { 
 if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event)) { 
  return true; 
 } 
 return onTouchEvent(event); 
}

 如果mOnTouchListener != null,(mViewFlags&ENABLED_MASK)==ENABLED和mOnTouchListener.onTouch(this, event)這三個條件都為真,就返回true,否則就去執行onTouchEvent(event)方法並返回。

總結下來onTouch能夠得到執行需要兩個前提條件(都滿足):
 1.設置了OnTouchListener
 2.控件是enable狀態

 而onTouchEvent能夠得到執行滿足以下三個條件任意一個即可:
 1.沒有設置OnTouchListener
 2.控件不是enable狀態
 3.onTouch返回false

 再來看一下dispatchTouchEvent的返回值,它其實受onTouch和onTouchEvent函數的返回值控制,也就是說touch事件被成功消費返回true,它也就返回true,說明分發成功,此後的事件序列也會在此被分發,而如果返回false,則認為分發失敗,此後的事件序列就不再分發下去了。
 •onTouchEvent方法:

 if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
  ...
  return true;
 }

View的onTouchEvent默認都會消耗掉事件(該方法返回true),除非它是不可點擊的(clickable和longClickable同時為false)。並且View的longClickable默認為false,clickable屬性要分情況,比如Button默認為true,TextView、ImageView默認為false。

public boolean performClick() { 
 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); 
 if (mOnClickListener != null) { 
  playSoundEffect(SoundEffectConstants.CLICK); 
  mOnClickListener.onClick(this); 
  return true; 
 } 
 return false; 
}

 這不就是我們熟悉的OnClickListener嗎,它原來是在onTouchEvent中被調用的。只要mOnClickListener不是null,就會去調用它的onClick方法。

總結下來onClick能夠得到執行需要兩個前提條件(都滿足):
 1.可以執行到onTouchEvent
 2.設置了OnClickListener

 整個View的事件轉發流程是:
dispatchEvent->setOnTouchListener->onTouchEvent->setOnClickListener

最後還有一個問題,setOnLongClickListener和setOnClickListener是否只能執行一個?
答:不是的,只要setOnLongClickListener中的onClick返回false,則兩個都會執行;返回true則會屏蔽setOnClickListener。

ViewGroup事件分發源碼分析:
 •dispatchTouchEvent方法:

 ...
   if (disallowIntercept || !onInterceptTouchEvent(ev)) { 
    ev.setAction(MotionEvent.ACTION_DOWN); 
    final int scrolledXInt = (int) scrolledXFloat; 
    final int scrolledYInt = (int) scrolledYFloat; 
    final View[] children = mChildren; 
    final int count = mChildrenCount; 

    for (int i = count - 1; i >= 0; i--) { 
     final View child = children[i]; 
     if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE 
       || child.getAnimation() != null) { 
      child.getHitRect(frame); 
      if (frame.contains(scrolledXInt, scrolledYInt)) { 
       final float xc = scrolledXFloat - child.mLeft; 
       final float yc = scrolledYFloat - child.mTop; 
       ev.setLocation(xc, yc); 
       child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; 
       if (child.dispatchTouchEvent(ev)) { 
        // Event handled, we have a target now. 
        mMotionTarget = child; 
        return true; 
       } 
      } 
     } 
    } 
   }

 兩種可能會進入if代碼段(即事件被分發給子View):
1、當前不允許攔截,即disallowIntercept = true.
2、當前沒有攔截,即onInterceptTouchEvent(ev)返回false.

 注:disallowIntercept是指是否禁用掉事件攔截的功能,默認是false,可以通過ViewGroup.requestDisallowInterceptTouchEvent(boolean)進行設置;而onInterceptTouchEvent(ev)可以進行復寫。

進入if代碼段後,通過一個for循環,遍歷當前ViewGroup下的所有子View,判斷當前遍歷的View是不是正在點擊的View,如果是的話就會調用該View的dispatchTouchEvent,就進入了View的事件分發流程了,上面有講。當child.dispatchTouchEvent(ev)返回true,則為mMotionTarget=child;然後return true,說明ViewGroup的dispatchTouchEvent返回值受childView的dispatchTouchEvent返回值影響,子view事件分發成功,ViewGroup的事件分發才成功,此後的事件序列也會在此分發(從上面知:子view的clickable或longClickable為true都能分發成功),而如果ViewGroup事件分發失敗或者沒有找到子View(點擊空白位置),則會走到它的onTouchEvent,以後的事件序列也不會分發下去,直接走onTouchEvent。

整個ViewGroup的事件轉發流程是:
dispatchEvent->onInterceptTouchEvent->child.dispatchEvent->(setOnTouchListener->onTouchEvent)

上面的總結都是基於:如果沒有攔截;那麼如何攔截呢?
 •onInterceptTouchEvent

 public boolean onInterceptTouchEvent(MotionEvent ev) { 
 return false; 
}

 代碼很簡單,只有一句,即返回false,ViewGroup默認是不攔截的。如果你需要攔截,只要return true就行了,這樣該事件就不會往子View傳遞了,並且如果你在DOWN return true ,則DOWN,MOVE,UP子View都不會捕獲到事件;如果你在MOVE return true , 則子View在MOVE和UP都不會捕獲到事件。

如何不被攔截:
如果ViewGroup的onInterceptTouchEvent(ev) 當ACTION_MOVE時return true ,即攔截了子View的MOVE以及UP事件;此時子View希望依然能夠響應MOVE和UP時該咋辦呢?
答:onInterceptTouchEvent是定義在ViewGroup中的,子View無法修改。Android給我們提供了一個方法:requestDisallowInterceptTouchEvent(boolean) 用於設置是否允許攔截,我們在子View的dispatchTouchEvent中直接這麼寫:

 @Override 
  public boolean dispatchTouchEvent(MotionEvent event) 
  { 
   getParent().requestDisallowInterceptTouchEvent(true); 
   int action = event.getAction();  
   switch (action) { 
   case MotionEvent.ACTION_DOWN: 
    Log.e(TAG, "dispatchTouchEvent ACTION_DOWN"); 
    break; 
   case MotionEvent.ACTION_MOVE: 
    Log.e(TAG, "dispatchTouchEvent ACTION_MOVE"); 
    break; 
   case MotionEvent.ACTION_UP: 
    Log.e(TAG, "dispatchTouchEvent ACTION_UP"); 
    break;  
   default: 
    break; 
   } 
   return super.dispatchTouchEvent(event); 
  } 

 getParent().requestDisallowInterceptTouchEvent(true); 這樣即使ViewGroup在MOVE的時候return true,子View依然可以捕獲到MOVE以及UP事件。
注:如果ViewGroup在onInterceptTouchEvent(ev) ACTION_DOWN裡面直接return true了,那麼子View是沒有辦法的捕獲事件的!

總結
關於代碼流程上面已經總結過了~
1、如果ViewGroup找到了能夠處理該事件的View,則直接交給子View處理,自己的onTouchEvent不會被觸發;
2、可以通過復寫onInterceptTouchEvent(ev)方法,攔截子View的事件(即return true),把事件交給自己處理,則會執行自己對應的onTouchEvent方法
3、子View可以通過調用getParent().requestDisallowInterceptTouchEvent(true); 阻止ViewGroup對其MOVE或者UP事件進行攔截;
好了,那麼實際應用中能解決哪些問題呢?
比如你在ScrollView中嵌套了一個EditText,當EditText中文字內容太多超出范圍時,你想上下滑動使EditText中文字滾動出來,卻發現滾動的是ScrollView。這時我們設置EditText的onTouch事件,在onTouch中設置不讓ScrollView攔截我的事件,最後在UP時把狀態改回去。

@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
  if ((view.getId() == R.id.tousuContentEditText && canVerticalScroll(tousuContentEditText))) {
   view.getParent().requestDisallowInterceptTouchEvent(true);
   if (motionEvent.getAction() == MotionEvent.ACTION_UP) {
    view.getParent().requestDisallowInterceptTouchEvent(false);
   }
  }
  return false;
 }

private boolean canVerticalScroll(EditText editText) {
  int scrollY = editText.getScrollY();
  int scrollRange = editText.getLayout().getHeight();
  int scrollExtent = editText.getHeight() - editText.getCompoundPaddingTop() - editText.getCompoundPaddingBottom();
  int scrollDifference = scrollRange - scrollExtent;
  if (scrollDifference == 0) {
   return false;
  }
  return (scrollY > 0) || (scrollY < scrollDifference - 1);
 }

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持本站。

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