筆記:事件分發機制(二):ViewGroup的事件分發
前言
前面我根據郭大神的部落格做了View的事件分發的筆記
筆記:事件分發機制(一):View的事件分發
對View的事件分發有了一個比較深入的瞭解。
本篇還是就郭大神的部落格
Android事件分發機制完全解析,帶你從原始碼的角度徹底理解(下)
做一下筆記。從原始碼角度深入分析和理解一下ViewGroup的事件分發。
ViewGroup
ViewGroup是View 的子類,一般作為容器,盛放其他View和ViewGroup。是Android佈局控制元件的直接或間接父類,像LinearLayout、FrameLayout、RelativeLayout等都屬於ViewGroup的子類。ViewGroup與View相比,多了可以包含子View和定義佈局引數的功能。ViewGroup的繼承關係如下:
從ViewGroup的子類中可以找到平常經常用到的佈局控制元件。
ViewGroup的事件分發流程
demo
簡單瞭解了一下ViewGroup
,接下來用demo探索ViewGroup
的事件分發流程。
首先,自定義一個佈局命名為MyLayout
繼承自LinearLayout
,如下所示:
public class MyLayout extends LinearLayout {
public MyLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
}
然後開啟佈局檔案 activity_view_group.xml
,用MyLayout
作為根佈局,設定屬性,新增控制元件。如下:
<?xml version="1.0" encoding="utf-8"?>
<com.wzhy.dispatcheventdemo.MyLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation ="vertical">
<Button
android:id="@+id/button1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button1"
/>
<Button
android:id="@+id/button2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button2"
/>
</com.wzhy.dispatcheventdemo.MyLayout>
在MyLayout
佈局中新增兩個按鈕,接著在Activity
中為MyLayout
和兩個按鈕新增監聽事件:
mLayout.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.i("TAG", " MyLayout: onTouch === action: " + event.getAction());
return false;
}
});
mButton1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i("TAG", "Button1: onClick Button1");
}
});
mButton2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i("TAG", "Button2: onClick Button2");
}
});
設定好監聽事件,執行一下:
分別點選Button1、Button2和空白區域,列印結果如下:
根據log列印可以看出,當點選按鈕時,MyLayout
註冊的觸控監聽(OnTouchEvent
)的onTouch
方法並沒有執行,只有點選空白區域時才會執行onTouch
方法。此時,可以先理解為Button的onClick
方法將事件消費掉了,因此事件不再向下傳遞。
那麼事件的傳遞流程到底是怎樣的呢?難道是事件先經過子View再到ViewGroup
嗎?欲知答案,繼續跟進…
原始碼分析
onInterceptTouchEvent
事件攔截
在事件分發機制中,ViewGroup
有一個View
不具備的方法–onInterceptTouchEvent
/**
* Implement this method to intercept all touch screen motion events. This
* allows you to watch events as they are dispatched to your children, and
* take ownership of the current gesture at any point.
*
* <p>Using this function takes some care, as it has a fairly complicated
* interaction with {@link View#onTouchEvent(MotionEvent)
* View.onTouchEvent(MotionEvent)}, and using it requires implementing
* that method as well as this one in the correct way. Events will be
* received in the following order:
*
* <ol>
* <li> You will receive the down event here.
* <li> The down event will be handled either by a child of this view
* group, or given to your own onTouchEvent() method to handle; this means
* you should implement onTouchEvent() to return true, so you will
* continue to see the rest of the gesture (instead of looking for
* a parent view to handle it). Also, by returning true from
* onTouchEvent(), you will not receive any following
* events in onInterceptTouchEvent() and all touch processing must
* happen in onTouchEvent() like normal.
* <li> For as long as you return false from this function, each following
* event (up to and including the final up) will be delivered first here
* and then to the target's onTouchEvent().
* <li> If you return true from here, you will not receive any
* following events: the target view will receive the same event but
* with the action {@link MotionEvent#ACTION_CANCEL}, and all further
* events will be delivered to your onTouchEvent() method and no longer
* appear here.
* </ol>
*
* @param ev The motion event being dispatched down the hierarchy.
* @return Return true to steal motion events from the children and have
* them dispatched to this ViewGroup through onTouchEvent().
* The current target will receive an ACTION_CANCEL event, and no further
* messages will be delivered here.
*/
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;
}
原始碼註釋很多,程式碼很簡單,返回值是boolean型別,既然如此,可以重寫這個方法,返回一個true試試。
public class MyLayout extends LinearLayout {
public MyLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
super.onInterceptTouchEvent(ev);
return true;
}
}
再次執行,分別點選Button1、Button2和空白區域,log列印如下:
發現不管在哪裡點選,只會觸發MyLayout的touch事件,按鈕的點選事件被遮蔽了!這是為什麼?如果touch事件先傳遞到子控制元件後傳遞到ViewGroup
,那麼MyLayout怎麼可能遮蔽掉Button的點選事件呢?明明之前點選MyLayout中的Button,只觸發了按鈕的點選事件,沒有觸發MyLayout的touch事件。
只能從原始碼中找答案,才能解決心中的疑惑。事先說明,Android中的touch事件的傳遞,絕對是先傳遞到ViewGroup,後傳遞到View的。
ViewGroup
的事件分發dispatchTouchEvent
記得在上一篇(筆記:事件分發機制(一):View的事件分發)中分析過,當觸控一個控制元件,都會先呼叫該控制元件的dispatchTouchEvent()
方法。說法沒錯,但並不完整。實際情況是當你觸控某個控制元件,首先呼叫該控制元件所在佈局的dispatchTouchEvent()
方法,然後在佈局的dispatchTouchEvent()
方法中找到相應的被觸控控制元件,然後在呼叫該空間的dispatchTouchEvent()
方法。所以,當我們點選了MyLayout的按鈕,首先會呼叫MyLayout的dispatchTouchEvent()
方法,這時會發現MyLayout和它的父類LinearLayout
中沒有這個方法,繼續往上找在ViewGroup
中發現了dispatchTouchEvent()
方法。
既然在ViewGroup
找到了dispatchTouchEvent()
方法,那麼就看一下這裡的dispatchTouchEvent()
方法:
@Override
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.
//當啟動一個新的觸控手勢,摒棄所有之前的狀態。
//由於App切換、ANR(應用無響應),或一些其他狀態改變,框架可能會放棄先前手勢的up或cancel事件。
cancelAndClearTouchTargets(ev);//取消和清除TouchTarget
resetTouchState();//重置觸控狀態,會將mFirstTouchTarget置空,FLAG_DISALLOW_INTERCEPT重置
}
// Check for interception.//檢查攔截
final boolean intercepted;
//如果是按下或者mFirstTouchTarget不為空(初始按下時,會尋找觸控目標),當事件由ViewGroup的子元素
//成功處理時,mFirstTouchTarget會被賦值並指向子元素,也就是
//ViewGroup不攔截事件並將事件交由子元素處理時mFirstTouchTarget!=null
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//FLAG_DISALLOW_INTERCEPT,可通過子View通過requestDisallowInterceptTouchEvent方法設定
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {//如果允許攔截
intercepted = onInterceptTouchEvent(ev);//呼叫ViewGroup自己的事件攔截
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.
//沒有觸控目標(mFirstTouchTarget == null)且當前動作(action)不是初始按下動作
//那麼當前ViewGroup繼續攔截觸控事件。也就是點選位置的ViewGroup內沒有找到目標View,
//事件由這個ViewGroup攔截處理。比如,點選ViewGroup內空白處。
intercepted = true;
}
//如果已攔截或已經有一個View正在處理手勢,正常事件分發。
// 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.
//如果需要,更新手指按下後一些列touch目標。
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
//新的touch目標
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
//動作(手勢)沒有取消,且事件沒有被當前ViewGroup攔截,
//事件會向下分發,交由它的子類處理。
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);
//遍歷ViewGroup所有子元素,然後判斷子元素是否能夠接收到事件。
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. 從前往後掃描ViewGroup的子控制元件。
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
//遍歷ViewGroup的所有子View。
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;
}
//重要:觸控點位置是否在子View的範圍內,或者子View是否可見或在播放動畫;
//如果均不符合,則continue, 表示子View不符合條件,開始遍歷下一個子View
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);
//分發轉換過的觸控事件,獲取最後按下的子View的index。
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();
//為子View新增一個TouchTarget,並把這個TouchTarget賦值給mFirstTouchTarget
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;
}
}
}
//如果找不到子View來處理事件,最後交由ViewGroup來處理
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
//沒有觸控的目標,所以把當前ViewGroup當做普通View(,去處理觸控事件)。
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
//mFirstTouchTarget不為空,表示已找到一個子View來消耗事件
// 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.
//如果需要,更新觸控點擡起(up)和取消(cancel)的觸控目標集合
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;
}
從上面的原始碼可知,當ViewGroup
不作事件攔截,會遍歷ViewGroup
的所有子元素,然後判斷子元素是否能夠接收到事件。是否能夠接收到事件主要由兩點判斷:子元素是否在播放動畫和點選事件是否落在子元素的邊界範圍內。如果某個子元素滿足這兩個條件,那麼事件就會傳遞給這個子控制元件來處理。可以看到,dispatchTransformedTouchEvent
實際上呼叫的是子元素的dispatchEvent
方法,其內部有一段程式碼:
if(child == null) {
handled = super.dispatchTouchEvent(event);
} else {
...
handled = child.dispatchTouchEvent(transformedEvent);
}
如果找到了能夠接收事件的子View
,就會由這個View
分發處理事件,否則將當前ViewGroup
作為View
(ViewGroup
的super
是View
)分發處理該事件。
如果子元素的dispatchTouchEvent
返回true,我們就暫時不用考慮事件是怎樣在子View中分發的。那麼mFirstTouchTarget
就會被賦值同時跳出for迴圈,如下所示:
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
這幾行程式碼完成了mFirstTouchTarget
的賦值並終止對子元素的遍歷。如果子元素的dispatchTouchEvent
返回false,ViewGroup會繼續遍歷,把事件分發給下一個符合條件的子元素(如果有的話)。
其實mFirstTouchTarget
的賦值是在addTouchTarget
方法中,從下面addTouchTarget
方法的原始碼中可以看出,mFirstTouchTarget
其實是一種單鏈表結構。mFirstTouchTarget
是否被賦值,將直接影響到ViewGroup對事件的攔截策略,如果mFirstTouchTarget
為null,那麼ViewGroup就預設攔截接下來的同一序列中(down-up、down-move…-up等)的所有觸控事件。
/**
* Adds a touch target for specified child to the beginning of the list.
* Assumes the target child is not already present.
*/
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
如果遍歷了所有子元素後,事件沒有被合理地處理。這包含兩種情況:一,ViewGroup沒有子元素;二,子元素處理了事件,但在dispatchTouchEvent
中返回了false,這一般是因為子元素在onTouchEvent
中返回了false。在這兩種情況下,ViewGroup會自己處理點選事件。如下:
// 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 {
......
}
注意dispatchTransformedTouchEvent
的第三個引數child為null,從dispatchTransformedTouchEvent
方法的分析可知,它會呼叫super.dispatchTouchEvent(event);
,而這裡的super是一個View(ViewGroup的super是View),也就是ViewGroup作為View去處理事件,這就轉到了View的事件分發。
下面是dispatchTransformedTouchEvent
的原始碼:
//把一個動作事件轉換到一個特定子View的座標空間中,過濾掉不相干的
//觸控點ids,並且在必要時覆蓋它的動作(事件)。如果子控制元件為空,
//動作事件(MotionEvent)將會分發給當前ViewGroup.
/**
* Transforms a motion event into the coordinate space of a particular child view,
* filters out irrelevant pointer ids, and overrides its action if necessary.
* If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
*/
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
//取消動作時一個特殊情況。我們不需要執行任何轉換或者過濾。
//它重要的部分是動作(action),不是內容。
final int oldAction = event.getAction();//獲取動作
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
//計算要分發的觸控點數量。過濾掉不相干的觸控點id。
// Calculate the number of pointers to deliver.
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
// If for some reason we ended up in an inconsistent state where it looks like we
// might produce a motion event with no pointers in it, then drop the event.
//如果由於一些原因,我們最後處於一個不合邏輯的狀態,就好像我們作出一個沒有觸控點的動作事件,那麼停止事件。
if (newPointerIdBits == 0) {
return false;
}
//動作事件複用
// If the number of pointers is the same and we don't need to perform any fancy
// irreversible transformations, then we can reuse the motion event for this
// dispatch as long as we are careful to revert any changes we make.
// Otherwise we need to make a copy.
//如果兩個觸控點的數值一樣,並且我們不需要執行任何不可逆的變換,
//那麼我們可以複用動作事件(motion event)來進行分發,只要
//我們注意恢復我們做出的改變。
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
transformedEvent = MotionEvent.obtain(event);
} else {
transformedEvent = event.split(newPointerIdBits);
}
//執行任何必要的轉換和事件分發。
// Perform any necessary transformations and dispatch.
if (child == null) {
//如果沒找到接收事件的子類,就由ViewGroup(ViewGroup的super是View)自己作為View分發處理事件。
handled = super.dispatchTouchEvent(transformedEvent);
} else {如果有接收事件的子View,座標空間轉換後,事件由子View分發處理。
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);
}
// Done.
transformedEvent.recycle();
return handled;
}
Activity對事件的分發過程
點選事件用MotionEvent
來表示,當一個點選操作發生時,事件首先傳遞給當前Activity,由Activity的dispatchTouchEvent
來進行事件分發,具體工作是由Activity內部的Window來完成的。Window會將事件傳遞給DecorView,DecorView一般是當前介面的底層容器(也就是setContentView所設定View的父容器),通過activity.getWindow().getDecorView()
可以獲得。先從Activity的dispatchTouchEvent
開始分析:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
分析一下原始碼,發現事件首先被交給Activity所附屬的Window進行分發,如果返回true,整個事件分發過程結束,如果返回false,意味著事件沒有控制元件處理,那麼Activity的onTouchEvent
就會被呼叫。
接下來看Window是如何將事件傳遞給ViewGroup的。通過原始碼我們知道,Window是一個抽象類,而Window的superDispatchTouchEvent
是個抽象方法,因此必須找到Window的實現類才行。
Window的實現類到底是誰?其實是PhoneWindow,這一點從Window的原始碼可以知道,在Window原始碼的類註釋中有這麼一段話:
Abstract base class for a top-level window look and behavior policy. An
instance of this class should be used as the top-level view added to the
window manager. It provides standard UI policies such as a background, title
area, default key processing, etc.
<p>The only existing implementation of this abstract class is
android.view.PhoneWindow, which you should instantiate when needing a
Window.
大概意思是:Window類可以控制頂層View的外觀和行為策略。它的唯一實現是android.policy.PhoneWindow,當你想要例項化這個Window類時,你並不 知道它的細節,因為這個類會被重構,只有一個工廠方法可以使用。
由於Window的唯一實現是PhoneWindow,那麼就看一下PhoneWindow是怎樣處理事件的。也就是看一下superDispatchTouchEvent
方法:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
到這裡就很清楚了,PhoneWindow將事件傳遞給了DecorView,這個DecorView是什麼呢?看下面:
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
@Override
public final View getDecorView() {
if (mDecor == null || mForceDecorInstall) {
installDecor();
}
return mDecor;
}
我們可以通過getWindow().getDecorView().findViewById(R.id.content).getChildAt(0)
獲取Activity通過所設定的View。這個mDecor就是getWindow().getDecorView()
返回的View,而我們通過setContentView
設定的View是它的一個子View。現在事件傳遞到了DecorView這裡,由於DecorView繼承自FrameLayout,且是父View,所以最終事件會傳遞給setContentView
設定View。從這裡開始,事件就傳遞到頂級View了,也就是Activity中通過setContentView
設定View,另外頂級View也叫根View,頂級View一般都是ViewGroup。
這樣就與上面ViewGroup的事件分發連線上了。
借用何俊林大神(逆流的魚yuiop)的一張圖片,來展示ViewGroup的事件分發流程。
ViewGroup事件分發試驗
code
與部落格最開始一樣,自定義一個MyLayout
,繼承LinearLayout
。重寫三個方法:onInterceptTouchEvent
、onTouchEvent
、dispatchTouchEvent
,程式碼如下:
public class MyLayout extends LinearLayout {
public MyLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.i("TAG", "MyLayout: onInterceptTouchEvent === " + getActionString(ev.getAction()));
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("TAG", "MyLayout: onTouchEvent === " + getActionString(event.getAction()));
return super.onTouchEvent(event);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.i("TAG", "MyLayout: dispatchTouchEvent === " + getActionString(ev.getAction()));
return super.dispatchTouchEvent(ev);
}
private String getActionString(int action){
switch (action) {
case MotionEvent.ACTION_DOWN:
return "ACTION_DOWN";
case MotionEvent.ACTION_MOVE:
return "ACTION_MOVE";
case MotionEvent.ACTION_UP:
return "ACTION_UP";
case MotionEvent.ACTION_CANCEL:
return "ACTION_CANCEL";
default: return "ACTION_OTHER";
}
}
}
然後,自定義一個MButton
,繼承Button
,重寫兩個方法onTouchEvent
、dispatchTouchEvent
,程式碼如下:
public class MButton extends android.support.v7.widget.AppCompatButton {
public MButton(Context context) {
super(context);
}
public MButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i("TAG", "MButton: dispatchTouchEvent === " + getActionString(event.getAction()));
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("TAG", "MButton: onTouchEvent === " + getActionString(event.getAction()));
return super.onTouchEvent(event);
}
private String getActionString(int action){
switch (action) {
case MotionEvent.ACTION_DOWN:
return "ACTION_DOWN";
case MotionEvent.ACTION_MOVE:
return "ACTION_MOVE";
case MotionEvent.ACTION_UP:
return "ACTION_UP";
case MotionEvent.ACTION_CANCEL:
return "ACTION_CANCEL";
default: return "ACTION_OTHER";
}
}
}
在佈局檔案activity_view_group.xml
中引用MyLayout和MButton,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<com.wzhy.dispatcheventdemo.MyLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/my_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.wzhy.dispatcheventdemo.MButton
android:id="@+id/m_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="MButton"
/>
</com.wzhy.dispatcheventdemo.MyLayout>
最後,在ActivityViewGroup
中為MyLayout
和MButton
設定onTouchListener
,重寫Activity的onTouchEvent
、dispatchTouchEvent
。程式碼如下:
public class ActivityViewGroup extends AppCompatActivity {
private MyLayout mLayout;
private MButton mButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_view_group);
mLayout = (MyLayout) findViewById(R.id.my_layout);
mButton = (MButton) findViewById(R.id.m_button);
mLayout.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.i("TAG", "MyLayout: onTouch === " + getActionString(event.getAction()));
return false;
}
});
mButton.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.i("TAG", "MButton: onTouch === " + getActionString(event.getAction()));
return false;
}
});
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
String actionString = getActionString(ev.getAction());
Log.i("TAG", "Activity: dispatchTouchEvent === " + actionString);
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
String actionString = getActionString(event.getAction());
Log.i("TAG", "Activity: onTouchEvent ===