筆記(四)——事件分發(fā)機(jī)制

——》個人平時筆記,看到的同學(xué)歡迎指正錯誤,文中多處摘錄于各大博主精華、書籍

1、事件分發(fā)機(jī)制:整個事件分發(fā)是一個U形傳遞的,遞歸傳遞。圖解 Android 事件分發(fā)機(jī)制

image

一個事件是指:一個ACTION_DOWN事件或ACTION_MOVE事件或ACTION_UP事件等。它們合稱同一個事件序列。事件是分開執(zhí)行傳遞的,在順利的條件下ACTION_DOWN、ACTION_MOVE、ACTION_UP事件分別按這個順序,依次跑完U行事件流程。

image.png
image.png

image.png
image.png
  • 對于ACTION_DOWN事件有以下特性
    (1)、dispatchTouchEvent返回 false 的含義應(yīng)該是事件停止往子View傳遞和分發(fā),并往上層父控件的onTouchEvent 回溯,之后上層父控件的onTouchEvent開始從下往上回傳,直到某個更上一層的onTouchEvent return true消費(fèi)事件而終止傳遞。其中如果是activity的話,dispatchTouchEvent return false | ture都是消費(fèi);最內(nèi)部底層view的dispatchTouchEvent return super.dispatchTouchEvent()則會將事件傳遞給當(dāng)前view的onTouchEvent 。
    (2)、onTouchEvent返回false | super就比較簡單了,它就是不消費(fèi)事件,并讓事件繼續(xù)從下往上向上一層父控件的方向傳遞,直到return true消費(fèi)掉事件終止傳遞。
    (3)、ViewGroup 和View的這些方法的super.xxxx()默認(rèn)實(shí)現(xiàn)就是會讓整個事件按照U形完整走完。中間不做任何改動,不回溯、不終止,每個環(huán)節(jié)都走到 dispatchTouchEvent--->onInterceptTouchEvent--->dispatchTouchEvent--->onTouchEvent。
    (4)、onInterceptTouchEvent攔截方法,類似一個開關(guān),當(dāng)return true時該事件則被當(dāng)前控件消費(fèi)了,攔截該事件,不再往下傳遞了;而return falsereturn super.xxxxxx()不會攔截事件,繼續(xù)往下傳遞事件,保證U形路徑暢通。
    down事件流程1
down事件流程2

重要注意:

(1)、如果一個View或ViewGroup的onTouchEvent不消耗ACTION_DOWN事件返回了false,那么它就不會再接收同一事件序列中的ACTION_MOVE、ACTION_UP等事件,并且將整個事件交給它的上一層父元素去處理。如果返回true就消費(fèi)事件終止傳遞。

(2)、dispatchTouchEvent()與onInterceptTouchEvent()依據(jù)上圖情況可以得到相似結(jié)果。如果ACTION_DOWN都沒有接收到,同一事件序列的ACTION_MOVE、ACTION_UP就不會再被接收了。但是onInterceptTouchEvent()比較特殊,當(dāng)攔截事件ACTION_DOWN返回true時,同一事件序列ACTION_MOVE、ACTION_UP并不會再傳遞到onInterceptTouchEvent(),而是直接跳過onInterceptTouchEvent(),直接傳遞到當(dāng)前view的onTouchEvent()中。反之onInterceptTouchEvent()不攔截事件ACTION_DOWN,后面的ACTION_MOVE、ACTION_UP才能交由它處理。

2、所有ListView、RecycleView、ScrollView等可以滾動的控件,當(dāng)一頁顯示不完數(shù)據(jù)時都會消費(fèi)掉onTouchEvent,所以父View的onTouchEvent就接收不到事件了。

小結(jié):dispatchTouchEvent(),onInterceptTouchEvent(),onTouchEvent()所示最后打印輸出所知Down、Move、UP都是一個一個對應(yīng)執(zhí)行。但是要是ACTION_DOWN都沒有接收到事件,后面的ACTION_MOVE、ACTION_UP事件序列就也不能接收到。

3、《Android開發(fā)藝術(shù)探索》中提到:

繪制及分發(fā)的順序流程:

Activity->Window(PhoneWindow實(shí)體類)->ViewRoot(ViewRootImpl)->DecorView->ViewGroup——》View(最底部轉(zhuǎn)折點(diǎn))——》ViewGroup->DecorView->ViewRoot(ViewRootImpl)->Window(PhoneWindow實(shí)體類)->Activity

關(guān)于事件傳遞的機(jī)制,這里給出一些結(jié)論,根據(jù)這些結(jié)論可以更好地理解整個傳遞機(jī)制,如下所示。

(1)、同一個事件序列是指從手指接觸屏幕的那一刻起,到手指離開屏幕的那一刻結(jié)束,在這個過程中所產(chǎn)生的一系列事件,這個事件序列以down事件開始,中間含有數(shù)量不定的move事件,最終以up事件結(jié)束。

(2)、正常情況下,一個事件序列只能被一個View攔截且消耗。這一條的原因可以參考(3),因?yàn)橐坏┮粋€元素?cái)r截了某此事件,那么同一個事件序列內(nèi)的所有事件都會直接交給它處理,因此同一個事件序列中的事件不能分別被兩個View同時處理,但是通過特殊手段可以做到,比如:一個view1將本該自己處理的事件通過onTouchEvent()強(qiáng)行傳遞給view2的onTouchEvent()的處理。

(3)、某個View一旦決定攔截(攔截ACTION_DOWN返回true),那么這一個事件序列都只能由它來處理(如果事件序列能夠傳遞給它的話),并且它的onInterceptTouchEvent不會再被調(diào)用。這條也很好理解,就是說當(dāng)一個View決定攔截一個事件后,那么系統(tǒng)會把同一個事件序列內(nèi)的其他事件都直接交給它的onTouchEvent()來處理,因此就不用再調(diào)用這個View的onInterceptTouchEvent去詢問它是否要攔截ACTION_MOVE和ACTION_UP了。

(4)、某個View一旦開始處理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一事件序列中的其他事件(ACTION_MOVE和ACTION_UP)都不會再交給它來處理,并且事件將重新交由它的父元素去處理,即父元素的onTouchEvent會被調(diào)用。意思就是事件一旦交給一個View處理,那么它就必須消耗掉,否則同一事件序列中剩下的事件就不再交給它來處理了,這就好比上級交給程序員一件事,如果這件事沒有處理好,短期內(nèi)上級就不敢再把事情交給這個程序員做了,二者是類似的道理。

(5)、如果View不消耗除ACTION_DOWN以外的其他事件,那么這個點(diǎn)擊事件會消失,此時父元素的onTouchEvent并不會被調(diào)用,并且當(dāng)前View可以持續(xù)收到后續(xù)的事件,最終這些消失的點(diǎn)擊事件會傳遞給Activity處理。(特別記下)

image

(6)、ViewGroup默認(rèn)不攔截任何事件。Android源碼中ViewGroup的onInterceptTouchEvent方法默認(rèn)返回false。

(7)、View沒有onInterceptTouchEvent方法,一旦有點(diǎn)擊事件傳遞給它,那么它的onTouchEvent方法就會被調(diào)用。

(8)、View的onTouchEvent默認(rèn)都會消耗事件(返回true),除非它是不可點(diǎn)擊的(clickable 和longClickable同時為false)。View的longClickable屬性默認(rèn)都為false,clickable屬性要分情況,比如Button的clickable屬性默認(rèn)為true,而TextView的clickable屬性默認(rèn)為false。

(9)、View的enable屬性不影響onTouchEvent的默認(rèn)返回值。哪怕一個View是disable狀態(tài)的,只要它的clickable或者longClickable有一個為true,那么它的onTouchEvent就返回true,就會消費(fèi)事件。

比如Button是可點(diǎn)擊的,TextView是不可點(diǎn)擊的。通過setClickable和setLongClickable可以分別改變View的CLICKABLE和LONG_CLICKABLE屬性。另外,setOnClickListener會自動將View的CLICKABLE設(shè)為true,setOnLongClickListener則會自動將View的LONG_CLICKABLE設(shè)為true,這一點(diǎn)從源碼中可以看出來。

 源碼:public void setOnClickListener(OnClickListener l) {
         if (!isClickable()) {
                 setClickable(true);
         }
         getListenerInfo().mOnClickListener = l;
     }
 
     public void setOnLongClickListener(OnLongClickListener l) {
         if (!isLongClickable()) {
                 setLongClickable(true);
         }
        getListenerInfo().mOnLongClickListener = l;
 }

(10)、onClick會發(fā)生的前提是當(dāng)前View是可點(diǎn)擊的,并且它收到了ACTION_DOWN和ACTION_UP的事件。優(yōu)先級:onTouchListener > onTouchEvent > OnClickListener

(11)、事件傳遞過程是由外向內(nèi)的,即事件總是先傳遞給父元素,然后再由父元素分發(fā)給子View,通過requestDisallowInterceptTouchEvent方法可以在子元素中干預(yù)父元素的事件分發(fā)過程,但是ACTION_DOWN事件除外。(若要干預(yù)ACTION_DOW的話也只能改父viewGroup為攔截狀態(tài)返回true,如果這樣的話子view本身就接收不到任何事件序列了,就更談不上requestDisallowInterceptTouchEvent能夠干預(yù)父元素事件了)(有解釋說因?yàn)锳CTION_DOWN事件方法里,會清除所有的標(biāo)志位——View的事件分發(fā)機(jī)制和滑動沖突解決方案

4、下圖理解(圖解 Android 事件分發(fā)機(jī)制一文中):事件為U型傳遞,ViewGroup2在onTouchEvent消費(fèi)事件,事件序列都返回true,事件分發(fā)到此為止;ViewGroup2既然能消費(fèi)事件,則它的下層子View的onTouchEvent的ACTION_DOWN必定是不消費(fèi)返回false的,而返回了false則后面子View的ACTION_MOVE和ACTION_UP等事件序列就不再被接收了,直接分發(fā)至父ViewGroup2,所以才有這樣的藍(lán)色箭頭走向

我們在ViewGroup 2 的onTouchEvent 返回true消費(fèi)這次事件

紅色的箭頭代表ACTION_DOWN 事件的流向

藍(lán)色的箭頭代表ACTION_MOVE 和 ACTION_UP 事件的流向

image

5、解決滑動沖突的方式:外部攔截法和內(nèi)部攔截法 參考-View的事件分發(fā)機(jī)制和滑動沖突解決方案

外部攔截法:是指點(diǎn)擊事情都先經(jīng)過父容器的攔截處理,如果父容器需要此事件就攔截,如果不需要此事件就不攔截,這樣就可以解決滑動沖突的問題,這種方法比較符合點(diǎn)擊事件的分發(fā)機(jī)制。外部攔截法需要重寫父容器的onInterceptTouchEvent方法,在方法內(nèi)做相應(yīng)的攔截即可。

ACTION_UP事件,這里必須要返回false,假設(shè)事件交由子元素處理,如果父容器在ACTION_UP時返回了true,就會導(dǎo)致子元素?zé)o法接收到ACTION_UP事件,這個時候子元素中的onClick事件就無法觸發(fā),但是父容器比較特殊,一旦它開始攔截任何一個事件,那么后續(xù)的事件都會交給它來處理,而ACTION_UP作為最后一個事件也必定可以傳遞給父容器,即便父容器的onInterceptTouchEvent方法在ACTION_UP時返回了false。

外部攔截法,父ViewGroup偽代碼如下: 
 public boolean onInterceptTouchEvent(MotionEvent event) {
         boolean intercepted = false;
         int x = (int) event.getX();
         int y = (int) event.getY();
 
         switch (event.getAction()) {
          case MotionEvent.ACTION_DOWN: {
                 intercepted = false;
                 break;
          }
          case MotionEvent.ACTION_MOVE: {
                    if (父容器需要當(dāng)前點(diǎn)擊事件) {
                         intercepted = true;
                    } else {
                         intercepted = false;
                    }
                  break;
          }
         case MotionEvent.ACTION_UP: {
                 intercepted = false;
                 break;
         }
         default:
                 break;
         }
         mLastXIntercept = x;
         mLastYIntercept = y;
         return intercepted;
      }

內(nèi)部攔截法:是指父容器不攔截任何事件,所有的事件都傳遞給子元素,如果子元素需要此事件就直接消耗掉,否則就交由父容器進(jìn)行處理,這種方法和Android中的事件分發(fā)機(jī)制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作。

內(nèi)部攔截法,子View偽代碼如下: 
 public boolean dispatchTouchEvent(MotionEvent event) { 
         int x = (int) event.getX();
         int y = (int) event.getY();

         switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                  //參數(shù)true,使父ViewGroup不攔截
                 parent.requestDisallowInterceptTouchEvent(true);
                 break;
            }
            case MotionEvent.ACTION_MOVE: {
                 int deltaX = x -mLastX;
                 int deltaY = y -mLastY;
                 if (父容器需要此類點(diǎn)擊事件)) {
                    //參數(shù)false,讓父ViewGroup攔截事件
                         parent.requestDisallowInterceptTouchEvent(false);
                 }
                 break;
             }
             case MotionEvent.ACTION_UP: {
                 break;
             }
            default:
                break;
          }
         mLastX = x;
         mLastY = y;
         return super.dispatchTouchEvent(event);
     }
 
 父ViewGroup偽代碼:
 
 @Override
   public boolean onInterceptTouchEvent(MotionEvent event) {
       int action = event.getAction();
       if (action == MotionEvent.ACTION_DOWN) {
           //因?yàn)樽觱iew需要事件,父ViewGroup的down都不攔截,讓這一系列事件往下傳遞
           return false;
       } else {
           return true;
       }
   }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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