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,那麼事件會逐層向上傳回。