Android事件分發機制原始碼分析下----ViewGroup事件分發分析
ViewGroup事件分發機制
上篇文章從原始碼的角度對View的事件分發進行了分析,這篇文章繼續對事件分發進行介紹,從原始碼的角度分析ViewGroup的事件分發,從繼承關係看ViewGroup也屬於View的一種,但它的內部可以放置View,簡單的結論我就不在文章中利用程式碼進行說明了,預設大家都知道事件是先到ViewGroup,然後再傳遞到View的。
事件到達Activity時,會呼叫Activity#dispatchTouchEvent方法,在這個方法,會把事件傳遞給Window,然後Window把事件傳遞給DecorView,而DecorView是什麼呢?它其實是一個根View,即根佈局,我們所設定的佈局是它的一個子View。最後再從DecorView傳遞給我們的根ViewGroup。
這裡可以說明一下Activity中view的結構:
最外圍是一個window但是從原始碼可以知道window是一個抽象類,具體實現類是一個PhoneWindow,對應一個Surface,最終會利用surfaceFlinger組合各個Surface(layer)繪製到螢幕上,PhoneWindow的內部就是DecorView,DecorView是該視窗的根佈局,它本質上是一個FrameLayout。DecorView有唯一一個子View,它是一個垂直LinearLayout,包含兩個子元素,一個是TitleView(ActionBar的容器),另一個是ContentView(視窗內容的容器)。關於ContentView,它是一個FrameLayout(android.R.id.content),我們平常用的setContentView就是設定它的子View。上圖還表達了每個Activity都與一個Window(具體來說是PhoneWindow)相關聯,使用者介面則由Window所承載。
所以在Activity傳遞事件給ViwGroup的流程是這樣的:Activity->Window->DecorView->ViewGroup
由於ViewGroup繼承自View所以上篇文章所講的函式,ViewGroup都有,同時檢視原始碼可以知道,事件分發過程中ViewGroup還有個重要函式onInterceptTouchEvent,Touch事件攔截函式。
原始碼:
public boolean dispatchTouchEvent(MotionEvent ev) { if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(ev, 1); } // If the event targets the accessibility focused view and this is it, start // normal event dispatch. Maybe a descendant is what will handle the click. if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) { ev.setTargetAccessibilityFocus(false); } boolean handled = false; if (onFilterTouchEventForSecurity(ev)) { final int action = ev.getAction(); final int actionMasked = action & MotionEvent.ACTION_MASK; // Handle an initial down. if (actionMasked == MotionEvent.ACTION_DOWN) { // Throw away all previous state when starting a new touch gesture. // The framework may have dropped the up or cancel event for the previous gesture // due to an app switch, ANR, or some other state change. cancelAndClearTouchTargets(ev); resetTouchState(); } // Check for interception. final boolean intercepted; 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 { // There are no touch targets and this action is not an initial down // so this view group continues to intercept touches. intercepted = true; } // If intercepted, start normal event dispatch. Also if there is already // a view that is handling the gesture, do normal event dispatch. if (intercepted || mFirstTouchTarget != null) { ev.setTargetAccessibilityFocus(false); } // Check for cancelation. final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL; // Update list of touch targets for pointer down, if needed. final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0; TouchTarget newTouchTarget = null; boolean alreadyDispatchedToNewTouchTarget = false; if (!canceled && !intercepted) { // If the event is targeting accessiiblity focus we give it to the // view that has accessibility focus and if it does not handle it // we clear the flag and dispatch the event to all children as usual. // We are looking up the accessibility focused host to avoid keeping // state since these events are very rare. View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus() ? findChildWithAccessibilityFocus() : null; 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 final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) : TouchTarget.ALL_POINTER_IDS; // Clean up earlier touch targets for this pointer id in case they // have become out of sync. removePointersFromTouchTargets(idBitsToAssign); final int childrenCount = mChildrenCount; if (newTouchTarget == null && childrenCount != 0) { final float x = ev.getX(actionIndex); final float y = ev.getY(actionIndex); // Find a child that can receive the event. // Scan children from front to back. final ArrayList<View> preorderedList = buildTouchDispatchChildList(); final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); final View[] children = mChildren; for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = getAndVerifyPreorderedIndex( childrenCount, i, customOrder); final View child = getAndVerifyPreorderedView( preorderedList, children, childIndex); // If there is a view that has accessibility focus we want it // to get the event first and if not handled we will perform a // normal dispatch. We may do a double iteration but this is // safer given the timeframe. if (childWithAccessibilityFocus != null) { if (childWithAccessibilityFocus != child) { continue; } childWithAccessibilityFocus = null; i = childrenCount - 1; } if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; } 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; } resetCancelNextUpFlag(child); 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; } // The accessibility focus didn't handle the event, so clear // the flag and do a normal dispatch to all children. ev.setTargetAccessibilityFocus(false); } if (preorderedList != null) preorderedList.clear(); } if (newTouchTarget == null && mFirstTouchTarget != null) { // Did not find a child to receive the event. // Assign the pointer to the least recently added target. newTouchTarget = mFirstTouchTarget; while (newTouchTarget.next != null) { newTouchTarget = newTouchTarget.next; } newTouchTarget.pointerIdBits |= idBitsToAssign; } } } // Dispatch to touch targets. if (mFirstTouchTarget == null) { // No touch targets so treat this as an ordinary view. handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { // Dispatch to touch targets, excluding the new touch target if we already // dispatched to it. Cancel touch targets if necessary. TouchTarget predecessor = null; TouchTarget target = mFirstTouchTarget; while (target != null) { final TouchTarget next = target.next; if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { handled = true; } else { final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { handled = true; } if (cancelChild) { if (predecessor == null) { mFirstTouchTarget = next; } else { predecessor.next = next; } target.recycle(); target = next; continue; } } predecessor = target; target = next; } } // Update list of touch targets for pointer up or cancel, if needed. if (canceled || actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { resetTouchState(); } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) { final int actionIndex = ev.getActionIndex(); final int idBitsToRemove = 1 << ev.getPointerId(actionIndex); removePointersFromTouchTargets(idBitsToRemove); } } if (!handled && mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1); } return handled; }
程式碼比較長,從分段進行原始碼分析:
1.初始化down事件
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
先判斷事件是否為DOWN事件,如果是則初始化,把mFirstTouchTarget置為null。新的完整事件總是從DOWN開始UP結束,所以如果是DOWN事件,那麼說明是一個新的事件序列,所以需要初始化之前的狀態。這裡的mFirstTouchTarget,當ViewGroup的子元素成功處理事件的時候,mFirstTouchTarget會指向子元素,此時mFirstTouchTarget != null,這個引數後面很重要,特別是涉及到解決事件衝突的內部攔截法。
2檢查ViewGroup是否要攔截事件
final boolean intercepted;
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 {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
以上程式碼主要判斷ViewGroup是否要攔截事件,定義了一個布林值intercept來記錄是否要進行攔截。
首先執行了這個語句:if(actionMasked == MotionEvent.ACTION_DOWN ||mFirstTouchTarget != null),如果事件是DOWN或者mFirstTouchTatget值不為空的時候,才有可能繼續往下執行,否則會直接跳過判斷是否攔截。
為什麼要有這個判斷呢?如果子View消耗了ACTION_DOWN事件,然後這裡可以由ViewGroup繼續判斷是否要攔截接下來的ACTION_MOVE事件之類的;但是如果第一次DOWN事件最終不是由子View消耗掉的,那麼顯然mFirstTouchTarget將為null,所以也就不用判斷了,直接把intercept設定為true,此後的事件都是由這個ViewGroup處理。所以得出一個結論就是如果當前ViewGroup決定攔截該事件,那麼該事件的其他事件序列就不會再走攔截函式,也不會往下傳遞。
disallowIntercept:
// Check for interception.
final boolean intercepted;
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 {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
// If intercepted, start normal event dispatch. Also if there is already
// a view that is handling the gesture, do normal event dispatch.
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
ViewGroup預設不攔截任何事件,但有時特定的ViewGroup會在ACTION_MOVE時攔截掉事件,看原始碼會發現還有一個FLAG_DISALLOW_INTERCEPT標誌位,這個標誌位的作用是禁止ViewGroup攔截除了DOWN之外的事件,一般通過子View的requestDisallowInterceptTouchEvent來設定。重寫子類的onTouchEvent()方法,在裡面呼叫getParent().requestDisallowInterceptTouchEvent(true)方法就不會攔截事件了。當傳入的引數為true時,表示子元件要自己消費這次事件,告訴父元件不要攔截(搶走)這次的事件。
此種情況可以舉個例子,如果在滑動控制元件裡面新增一個點選控制元件,當點選時,裡面的控制元件可以正常處理點選事件,父控制元件也不會攔截,但是如果想點選在子類控制元件位置,滑動子控制元件時,這時父類的控制元件可以滑動,事件傳遞到父類控制元件時可能會被父類消耗掉,以後的事件就不會傳遞到子類了,在子類設定getParent().requestDisallowInterceptTouchEvent(true)可以告訴父類我要自己消耗事件,不要攔截。
如果ViewGroup在onInterceptTouchEvent(ev) ACTION_DOWN裡面直接return true了,那麼子View是捕獲不到事件的設定getParent().requestDisallowInterceptTouchEvent(true)也沒有用,因為不會傳遞到子View的dispatchTouchEvent。只有ViewGroup的onInterceptTouchEvent(ev) ACTION_DOWN時不攔截,在ACTION_MOVE時攔截才能利用getParent().requestDisallowInterceptTouchEvent(true)讓ViewGroup不攔截事件。
3 對ACTION_DWON事件的特殊處理
TouchTarget newTouchTarget = null;
// 1boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
(1)
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
(2) } }
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
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
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
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;
}
resetCancelNextUpFlag(child);
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;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
判斷if(!canceled && !intercepted),表示如果不是取消事件以及ViewGroup不進行攔截則進入(1),接著又是一個判斷if (actionMasked == MotionEvent.ACTION_DOWN …)這表示事件是否是ACTION_DOWN事件,如果是則進入(2),根據以上兩個條件,事件是ACTION_DOWN以及ViewGroup不攔截,那麼(2)內部應該是把事件分發給子View。
4子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
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
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;
}
resetCancelNextUpFlag(child);
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;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
分發事件給子view進行處理。迴圈對所有的子View進行遍歷,由於ViewGroup不對事件進行攔截,在ViewGroup內部對子View進行遍歷,找到能接受事件的子View,從最上層的子View開始往內層遍歷。然後根據方法名字我們得知這個判斷語句是判斷觸控點位置是否在子View的範圍內或者子View是否在播放動畫,如果均不符合則continue,表示這個子View不符合條件,開始遍歷下一個子View。
接著呼叫了dispatchTransformedTouchEvent()方法,交給子view處理事件,然後得到是否子View 消耗了事件。當傳遞進來的的child不為null時,就會呼叫子View的dispatchTouchEvent(event)方法,表示把事件交給子View處理,也即是說,子Viwe符合所有條件的時候,事件就會在這裡傳遞給了子View來處理,完成了ViewGroup到子View的事件傳遞,當事件處理完畢,就會返回一個布林值handled,該值表示子View是否消耗了事件。怎樣判斷一個子View是否消耗了事件呢?如果說子View的onTouchEvent()返回true,那麼就是消耗了事件,如果子View消耗了事件那麼最後便會執行addTouchTarget()方法。
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
如果子View消耗掉了事件,那麼mFirstTouchTarget就會指向子View。在執行完後,直接break了,表示跳出了迴圈,因為已經找到了處理事件的子View,所以無需繼續遍歷了。
但是如果我們沒有點選任何ViewGroup內的控制元件,只是觸摸了自定義的ViewGroup,不會處理任何的子view事件,會去執行ViewGroup的dispatchTouchEvent方法,最終會執行到ViewGroup的onTouch或者onClick事件。
總結:Android事件分發是先傳遞到ViewGroup,再由ViewGroup傳遞到View的。當我們點選viewGroup中的控制元件時,首先執行viewGroup的dispatchTouchEvent方法,內部判斷是否通過onInterceptTouchEvent方法對事件傳遞進行攔截,onInterceptTouchEvent方法返回true代表不允許事件繼續向子View傳遞,返回false代表不對事件進行攔截,預設返回false。如果對傳遞到子View進行攔截,則會執行自定義ViewGroup的dispatchTouchEvent方法,最終處理自定義ViewGroup的onTouch事件。如果事件被攔截,則後面的連續事件不會在進行攔截,也不會傳遞到子View。如果事件沒有被攔截,則遍歷ViewGroup內部的子View找到可以接收事件的view,呼叫view的DispatchTouchEvent方法,處理ontouch事件或者點選事件,終止事件的傳遞。但是如果我們沒有點選任何ViewGroup內的控制元件,只是觸摸了自定義的ViewGroup,不會處理任何的子view事件,會去執行ViewGroup的dispatchTouchEvent方法,最終會執行到ViewGroup的onTouch或者onClick事件。
總之就是ViewGroup預設不攔截任何事件,所以事件能正常分發到子View處(如果子View符合條件的話),如果沒有合適的子View或者子View不消耗ACTION_DOWN事件,那麼接著事件會交由ViewGroup處理,並且同一事件序列之後的事件不會再分發給子View了。如果ViewGroup的onTouchEvent也返回false,即ViewGroup也不消耗事件的話,那麼最後事件會交由Activity處理。即:逐層分發事件下去,如果都沒有處理事件的View,那麼事件會逐層向上返回。