Android
事件傳遞流程在網上能夠找到不少資料,FrameWork
層輸入事件和消費事件,能夠參考:android
這篇blog闡述了底層是如何處理屏幕輸,並往上傳遞的。Touch
事件傳遞到Activity
的DecorView
時,往下走就是ViewGroup
和子View
之間的事件傳遞,能夠參考郭神的這兩篇博客緩存
郭神的兩篇博客清楚明白地說明了View
之間事件傳遞的大方向,可是具體的一些晦暗的細節闡述較少,本文主要是總結這兩篇博客的同時,側重於兩點:ide
view
到底如何和父View
搶事件,父View
又是如何攔截事件不發送給子View
,以及若是咱們須要處理這種混亂的關係才能讓二者和諧相處?。要明白View
的事件傳遞,頗有必要先說一下Touch
事件是如何在Android
系統中抽象的,這主要使用的就是MotionEvent
。這個類經歷了幾回重大的修改,一次是在2.x版本支持多點觸摸,一次是4.x將大部分代碼甩給native
層處理。函數
咱們先舉個栗子來講明一次完整的事件,用戶觸屏 滑動 到手機離開屏幕,這認爲是一次完整動做序列(movement traces
)。一個動做序列中包含不少動做Action
,好比在用戶按下時,會封裝一個MotionEvent
,分發給視圖樹,咱們能夠經過motionevent.getAction
拿到這個動做是ACTION_DOWN
。一樣,在手指擡起時,咱們能夠接收到Action
類型是Action_UP
的MotionEvent
。對於滑動(MOVE
)這個操做,Android
爲了從效率出發,會將多個MOVE
動做打包到一個MotionEvent
中。經過getX getY
能夠獲取當前的座標,若是要訪問打包的緩存數據,能夠經過getHistorical**()
函數來獲取。佈局
對於單點的操做來看,MotionEvent
顯得比較簡單,可是考慮引入多點觸摸呢?咱們定義一個接觸點爲(Pointer
)。咱們從onTouch
接受到一個MotionEvent
,怎麼拿到多個觸碰點的信息?爲了解開筆者剛開始學習這部分知識時的困惑,咱們首先樹立起一種概念:一個MotionEvent
只容許有一個Action
(動做),並且這個Action
會包含觸發此次Action
的觸碰點信息,對於MOVE
操做來講,必定是當前全部觸碰點都在動。只有ACTION_POINTER_DOWN
這類事件事件會在Action
裏面指定是哪個POINTER
按下。學習
在MotionEvent
的底層實現中,是經過一個16位來存儲Action
和Pointer
信息(PointerIndex
)。低8位表示Action
,理論上能夠表示255種動做類型;高8位表示觸發這個Action
的PointerIndex
,理論上Android
最多能夠支持255點同時觸摸,可是在上層代碼使用的時候,默認多點最多存在32個,否則事件在分發的時候會有問題。ui
MotionEvent
中多個手指的操做API
大部分都是經過pointerindex
來進行的,如:獲取不一樣Pointer
的觸碰位置,getX(int pointerIndex)
;獲取PointerId
等等。大部分狀況下,pointerid == pointeridex
。this
ACTION_DOWN
OR ACTION_POINTER_DOWN
:spa
這兩個按下操做的區別是ACTION_DOWN
是一個系列動做的開始,而ACTION_POINTER_DOWN
是在一個系列動做中間有另一個觸碰點觸碰到屏幕。.net
這部分詳細的描述,請參考:
android觸控,先了解MotionEvent
到這裏,鋪墊終於結束了,咱們開始直奔主題。
Android
的Touch
事件傳遞到Activity
頂層的DecorView
(一個FrameLayout
)以後,會經過ViewGroup
一層層往視圖樹的上面傳遞,最終將事件傳遞給實際接收的View
。下面給出一些重要的方法。若是你對這個流程比較熟悉的話,能夠跳過這裏,直接進入第二部分。
事件傳遞到一個ViewGroup
上面時,會調用dispatchTouchEvent
。代碼有刪減
public boolean dispatchTouchEvent(MotionEvent ev) { boolean handled = false; if (onFilterTouchEventForSecurity(ev)) { final int action = ev.getAction(); final int actionMasked = action & MotionEvent.ACTION_MASK; // Attention 1 :在按下時候清除一些狀態 if (actionMasked == MotionEvent.ACTION_DOWN) { cancelAndClearTouchTargets(ev); //注意這個方法 resetTouchState(); } // Attention 2:檢查是否須要攔截 final boolean intercepted; //若是剛剛按下 或者 已經有子View來處理 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 { // 不是一個動做序列的開始 同時也沒有子View來處理,直接攔截 intercepted = true; } //事件沒有取消 同時沒有被當前ViewGroup攔截,去找是否有子View接盤 if (!canceled && !intercepted) { //若是這是一系列動做的開始 或者有一個新的Pointer按下 咱們須要去找可以處理這個Pointer的子View if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { final int actionIndex = ev.getActionIndex(); // always 0 for down //上面說的觸碰點32的限制就是這裏致使 final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) : TouchTarget.ALL_POINTER_IDS; final int childrenCount = mChildrenCount; if (newTouchTarget == null && childrenCount != 0) { final float x = ev.getX(actionIndex); final float y = ev.getY(actionIndex); //對當前ViewGroup的全部子View進行排序,在上層的放在開始 final ArrayList<View> preorderedList = buildOrderedChildList(); final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); final View[] children = mChildren; for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i; final View child = (preorderedList == null) ? children[childIndex] : preorderedList.get(childIndex); // canViewReceivePointerEvents visible的View均可以接受事件 // isTransformedTouchPointInView 計算是否落在點擊區域上 if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; } //可以處理這個Pointer的View是否已經處理以前的Pointer,那麼把 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; } } //Attention 3 : 直接發給子View 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; } } } } } // 前面已經找到了接收事件的子View,若是爲NULL,表示沒有子View來接手,當前ViewGroup須要來處理 if (mFirstTouchTarget == null) { // ViewGroup處理 handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { if(alreadyDispatchedToNewTouchTarget) { //ignore some code if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { handled = true; } } } return handled; }
上面代碼中的Attention
在後面部分將會涉及,重點注意。
這裏須要指出一點的是,一系列動做中的不一樣Pointer
能夠分配給不一樣的View
去響應。ViewGroup會維護一個PointerId
和處理View
的列表TouchTarget
,一個TouchTarget
表明一個能夠處理Pointer
的子View
,固然一個View
能夠處理多個Pointer
,好比兩根手指都在某一個子View
區域。TouchTarget
內部使用一個int
來存儲它能處理的PointerId
,一個int
32位,這也就是上層爲啥最多隻能容許同時最多32點觸碰。
看一下Attention 3
處的代碼,咱們常常說view
的dispatchTouchEvent
若是返回false,那麼它就不能系列動做後面的動做,這是爲啥呢?由於Attention 3
處若是返回false
,那麼它不會被記錄到TouchTarget
中,ViewGroup認爲你沒有能力處理這個事件。
這裏能夠看到,ViewGroup
真正處理事件是在dispatchTransformedTouchEvent
裏面,跟進去看看:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { //沒有子類處理,那麼交給viewgroup處理 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); } return handled; }
能夠看到這裏無論怎麼樣,都會調用View
的dispatchTouchEvent
,這是真正處理這一次點擊事件的地方。
public boolean dispatchTouchEvent(MotionEvent event) { if (onFilterTouchEventForSecurity(event)) { //先走View的onTouch事件,若是onTouch返回True 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; } } return result; }
咱們給View
設置的onTouch
事件處在一個較高的優先級,若是onTouch
執行返回true
,那麼就不會去走view
的onTouchEvent
,而咱們一些點擊事件都是在onTouchEvent
中處理的,這也是爲何onTouch
中返回true,view
的點擊相關事件不會被處理。
ViewGroup
在接受到上級傳下來的事件時,若是是一系列Touch
事件的開始(ACTION_DOWN
),ViewGroup
會先看看本身需不須要攔截這個事件(onInterceptTouchEvent
,ViewGroup
的默認實現直接返回false
表示不攔截),接着ViewGroup
遍歷本身全部的View
。找到當前點擊的那個View
,立刻調用目標View
的dispatchTouchEvent
。若是目標View
的dispatchTouchEvent
返回false,那麼認爲目標View
只是在那個位置而已,它並不想接受這個事件,只想安安靜靜的作一個View
(我靜靜地看着大家裝*)。此時,ViewGroup
還會去走一下本身dispatchTouchEvent,Done!
終於來到本文的重要環節,子View和父佈局(ViewGroup
)是如何撕逼的。咱們常常遇到這樣的問題:在ListView
中放一個ViewPager
不能滑動的問題,其實這裏就會涉及到子View和佈局之間的協商,事件處理到底你上仍是我上。
首先須要明確一點的是,一個事件確定是由ViewGroup
傳遞給本身的子View
的,因此ViewGroup
具備絕對的權威來禁止事件往下傳,這就是onInterceptTouchEvent
方法。能夠看上面ViewGroup中的dispatchTouchEvent
的Attention 1
和Attention 2
。
先看Attetion2
:
進行判斷有有兩個條件:1,若是是一次新的事件 or 在一次事件中可是已經有子View來處理這個事件,那麼父類須要去看看是否攔截此次事件。不然,直接攔截(此時處於一系列動做的中間,並且沒有子view來接盤,那麼ViewGroup就直接攔下來)。
決定是否攔截有兩個步驟,
disallowIntercept
是否駁回攔截,默認false
。注意這個值是子View
和撕*的關鍵,由於ViewGroup
開放了給這個標記賦值的接口requestDisallowInterceptTouchEvent()
,並且這個方法直接往上遞歸,這個ViewGroup
的各級父容器都會設置駁回攔截。onInterceptTouchEvent
雖然ViewGroup
中默認返回false,可是在不少有滑動功能的ViewGroup
裏面(如scrollview ListView
等)會處理各類狀況,決定是否攔截這個事件,因此就會出現以前說的ListView
中的Viewpager
不能滑動的問題,緣由是事件被父View攔截了。在Attetion1
的位置若是是一次新的ACTION_DOWN
,那麼會把以前事件傳遞設置的各類狀態清除。
對於一個須要攔截事件的ViewGroup
,它一般都有一些特殊的操做,好比ScrollView
,好比ViewPager
,它重寫onInterceptTouchEvent
是很是關鍵的,這也是能和子View
和諧相處的關鍵。舉個例子,我本身定義了一個ViewGroup
:
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { if(ev.getActionMasked() == MotionEvent.ACTION_DOWN) { return true; } return super.onInterceptTouchEvent(ev); }
這樣會發生什麼?
全部位於MyViewGroup中的子View收不到任何的事件,緣由能夠看一下Attention2
的代碼,判斷是否攔截是在系列動做按下時會進行判斷,若是此時攔截,那麼直接不會去查找相應處理的子View,因此touchtarget
爲空,那麼接下來的動做都直接被ViewGroup
笑納。
因此哪怕再強勢的ViewGroup
,通常都是在Down
的時候給子類機會去掉用requestDisallowInterceptTouchEvent
,如設置駁回攔截,那麼在ViewGroup分發事件的時候,會跳過onInterceptTouchEvent
的執行。
對於子View來講,在合適的時機調用requestDisallowInterceptTouchEvent
便可。固然啥時候合適?對於一個View
來講,那就是在dispatchTouchEvent
或者onTouchEvent
來調用。
對於ViewGroup
來講,一般咱們會在onInterceptTouchEvent
進行判斷。好比咱們常常會遇到在ListView
裏面套了ViewPager
致使ViewPager
不能滑動的問題,一般的處理方式:
@Override public boolean onInterceptTouchEvent(MotionEvent event) { if (absListView != null) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mDownX = event.getX(); mDownY = event.getY(); //ACTION_DOWN的時候,趕忙把事件hold住 getParent().requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_MOVE: if(Math.abs(event.getX() - mDownX)>Math.abs(event.getY()-mDownY)) { getParent().requestDisallowInterceptTouchEvent(true); }else { //發現不是本身處理,還給父類 getParent().requestDisallowInterceptTouchEvent(false); } break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: //其實這裏是多餘的 getParent().requestDisallowInterceptTouchEvent(false); } } return super.onInterceptTouchEvent(event); }
原本打算寫一個短篇的,結果一個不當心,弄成了長篇大論。
最後須要注意一點的是,全部咱們上述討論的內容都是在一層層遞歸中進行,並且requestDisallowInterceptTouchEvent
這個函數也是遞歸調用的。
咱們能夠認爲ViewGroup
是一個具備絕對話語權可是從不專政的霸道總裁,它本身能夠攔截處理某些事件,好比Viewpager
的橫滑,可是它也能夠給子View足夠的空間去要求這個事件給本身處理。做爲一名開發者,一方面在本身定義ViewGroup
時須要考慮可以給子View足夠空間中斷本身的攔截;一方面本身定義View時,咱們須要在合適的時候跟父View索要事件。ViewPager(新版)
做爲容器來講,它須要攔截橫滑事件,同時,本身具有了和父View
爭搶事件的能力,因此無論把ViewPager
放到什麼佈局中,它都能正確處理。看看它的onInterceptTouchEvent
怎麼寫的吧,完美的體現了這一思想。