筆記(四)——事件分發機制

  • 2020 年 3 月 27 日
  • 筆記

——》個人平時筆記,看到的同學歡迎指正錯誤,文中多處摘錄於各大部落客精華、書籍

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

image

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

  • 對於ACTION_DOWN事件有以下特性 (1)、dispatchTouchEvent返回 false 的含義應該是事件停止往子View傳遞和分發,並往上層父控制項的onTouchEvent 回溯,之後上層父控制項的onTouchEvent開始從下往上回傳,直到某個更上一層的onTouchEvent return true消費事件而終止傳遞。其中如果是activity的話,dispatchTouchEvent return false | ture都是消費;最內部底層view的dispatchTouchEvent return super.dispatchTouchEvent()則會將事件傳遞給當前view的onTouchEvent 。 (2)、onTouchEvent返回false | super就比較簡單了,它就是不消費事件,並讓事件繼續從下往上向一層父控制項的方向傳遞,直到return true消費掉事件終止傳遞。 (3)、ViewGroup 和View的這些方法的super.xxxx()默認實現就是會讓整個事件按照U形完整走完。中間不做任何改動,不回溯、不終止,每個環節都走到 dispatchTouchEvent—>onInterceptTouchEvent—>dispatchTouchEvent—>onTouchEvent。 (4)、onInterceptTouchEvent攔截方法,類似一個開關,當return true時該事件則被當前控制項消費了,攔截該事件,不再往下傳遞了;而return falsereturn super.xxxxxx()不會攔截事件,繼續往下傳遞事件,保證U形路徑暢通。

down事件流程1

down事件流程2

重要注意: (1)、如果一個View或ViewGroup的onTouchEvent不消耗ACTION_DOWN事件返回了false,那麼它就不會再接收同一事件序列中的ACTION_MOVE、ACTION_UP等事件,並且將整個事件交給它的上一層父元素去處理。如果返回true就消費事件終止傳遞。 (2)、dispatchTouchEvent()與onInterceptTouchEvent()依據上圖情況可以得到相似結果。如果ACTION_DOWN都沒有接收到,同一事件序列的ACTION_MOVE、ACTION_UP就不會再被接收了。但是onInterceptTouchEvent()比較特殊,當攔截事件ACTION_DOWN返回true時,同一事件序列ACTION_MOVE、ACTION_UP並不會再傳遞到onInterceptTouchEvent(),而是直接跳過onInterceptTouchEvent(),直接傳遞到當前view的onTouchEvent()中。反之onInterceptTouchEvent()不攔截事件ACTION_DOWN,後面的ACTION_MOVE、ACTION_UP才能交由它處理。

2、所有ListView、RecycleView、ScrollView等可以滾動的控制項,當一頁顯示不完數據時都會消費掉onTouchEvent,所以父View的onTouchEvent就接收不到事件了。

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

3、《Android開發藝術探索》中提到:

繪製及分發的順序流程: Activity->Window(PhoneWindow實體類)->ViewRoot(ViewRootImpl)->DecorView->ViewGroup——》View(最底部轉折點)——》ViewGroup->DecorView->ViewRoot(ViewRootImpl)->Window(PhoneWindow實體類)->Activity

關於事件傳遞的機制,這裡給出一些結論,根據這些結論可以更好地理解整個傳遞機制,如下所示。

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

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

(3)、某個View一旦決定攔截(攔截ACTION_DOWN返回true),那麼這一個事件序列都只能由它來處理(如果事件序列能夠傳遞給它的話),並且它的onInterceptTouchEvent不會再被調用。這條也很好理解,就是說當一個View決定攔截一個事件後,那麼系統會把同一個事件序列內的其他事件都直接交給它的onTouchEvent()來處理,因此就不用再調用這個View的onInterceptTouchEvent去詢問它是否要攔截了。

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

(5)、如果View不消耗除ACTION_DOWN以外的其他事件,那麼這個點擊事件會消失,此時父元素的onTouchEvent並不會被調用,並且當前View可以持續收到後續的事件,最終這些消失的點擊事件會傳遞給Activity處理。(特別記下)

image

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

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

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

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

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

 源碼: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會發生的前提是當前View是可點擊的,並且它收到了ACTION_DOWN和ACTION_UP的事件。優先順序:onTouchListener > onTouchEvent > OnClickListener

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

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

我們在ViewGroup 2 的onTouchEvent 返回true消費這次事件 紅色的箭頭代表ACTION_DOWN 事件的流向 藍色的箭頭代表ACTION_MOVE 和 ACTION_UP 事件的流向

image

5、解決滑動衝突的方式:外部攔截法和內部攔截法 參考-View的事件分發機制和滑動衝突解決方案

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

ACTION_UP事件,這裡必須要返回false,假設事件交由子元素處理,如果父容器在ACTION_UP時返回了true,就會導致子元素無法接收到ACTION_UP事件,這個時候子元素中的onClick事件就無法觸發,但是父容器比較特殊,一旦它開始攔截任何一個事件,那麼後續的事件都會交給它來處理,而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 (父容器需要當前點擊事件) {                           intercepted = true;                      } else {                           intercepted = false;                      }                    break;            }           case MotionEvent.ACTION_UP: {                   intercepted = false;                   break;           }           default:                   break;           }           mLastXIntercept = x;           mLastYIntercept = y;           return intercepted;        }

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

內部攔截法,子View偽程式碼如下:   public boolean dispatchTouchEvent(MotionEvent event) {           int x = (int) event.getX();           int y = (int) event.getY();             switch (event.getAction()) {              case MotionEvent.ACTION_DOWN: {                    //參數true,使父ViewGroup不攔截                   parent.requestDisallowInterceptTouchEvent(true);                   break;              }              case MotionEvent.ACTION_MOVE: {                   int deltaX = x -mLastX;                   int deltaY = y -mLastY;                   if (父容器需要此類點擊事件)) {                      //參數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) {             //因為子view需要事件,父ViewGroup的down都不攔截,讓這一系列事件往下傳遞             return false;         } else {             return true;         }     }