PS一句:最終還是選擇CSDN來整理發表這幾年的知識點,該文章平行遷移到CSDN。因為CSDN也支持MarkDown語法了,牛逼啊!
【工匠若水?[http://blog.csdn.net/yanbober](http://blog.csdn.net/yanbober)】
該篇承接上一篇[《Android觸摸屏事件派發機制詳解與源碼分析一(View篇)》](http://blog.csdn.net/yanbober/article/details/45887547),閱讀本篇之前建議先閱讀。當然,閱讀完這一篇之后可以閱讀繼續進階的下一篇[《Android觸摸屏事件派發機制詳解與源碼分析三(Activity篇)》](http://blog.csdn.net/yanbober/article/details/45932123)。
### 1 背景
還記得前一篇[《Android觸摸屏事件派發機制詳解與源碼分析一(View篇)》](http://blog.csdn.net/yanbober/article/details/45887547)中關于透過源碼繼續進階實例驗證模塊中存在的點擊Button卻觸發了LinearLayout的事件疑惑嗎?當時說了,在那一篇咱們只討論View的觸摸事件派發機制,這個疑惑留在了這一篇解釋,也就是ViewGroup的事件派發機制。
PS:閱讀本篇前建議先查看前一篇[《Android觸摸屏事件派發機制詳解與源碼分析一(View篇)》](http://blog.csdn.net/yanbober/article/details/45887547),這一篇承接上一篇。
關于View與ViewGroup的區別在前一篇的Android 5.1.1(API 22) View觸摸屏事件傳遞源碼分析部分的寫在前面的話里面有詳細介紹。其實你只要記住類似Button這種控件都是View的子類,類似布局這種控件都是ViewGroup的子類,而ViewGroup又是View的子類而已。具體查閱[《Android觸摸屏事件派發機制詳解與源碼分析一(View篇)》](http://blog.csdn.net/yanbober/article/details/45887547)。
### 2 基礎實例現象
#### 2-1 例子
這個例子布局等還和上一篇的例子相似,只是重寫了Button和LinearLayout而已,所以效果圖不在提供,具體參見上一篇。
首先我們簡單的自定義一個Button(View的子類),再自定義一個LinearLayout(ViewGroup的子類),其實沒有自定義任何屬性,只是重寫部分方法(添加了打印,方便查看)而已,如下:
~~~
public class TestButton extends Button {
public TestButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i(null, "TestButton dispatchTouchEvent-- action=" + event.getAction());
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i(null, "TestButton onTouchEvent-- action=" + event.getAction());
return super.onTouchEvent(event);
}
}
~~~
~~~
public class TestLinearLayout extends LinearLayout {
public TestLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.i(null, "TestLinearLayout onInterceptTouchEvent-- action=" + ev.getAction());
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i(null, "TestLinearLayout dispatchTouchEvent-- action=" + event.getAction());
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i(null, "TestLinearLayout onTouchEvent-- action=" + event.getAction());
return super.onTouchEvent(event);
}
}
~~~
如上兩個控件很簡單吧,不解釋,繼續看其他代碼:
~~~
<?xml version="1.0" encoding="utf-8"?>
<com.zzci.light.TestLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:gravity="center"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:id="@+id/mylayout">
<com.zzci.light.TestButton
android:id="@+id/my_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="click test"/>
</com.zzci.light.TestLinearLayout>
~~~
~~~
public class ListenerActivity extends Activity implements View.OnTouchListener, View.OnClickListener {
private TestLinearLayout mLayout;
private TestButton mButton;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mLayout = (TestLinearLayout) this.findViewById(R.id.mylayout);
mButton = (TestButton) this.findViewById(R.id.my_btn);
mLayout.setOnTouchListener(this);
mButton.setOnTouchListener(this);
mLayout.setOnClickListener(this);
mButton.setOnClickListener(this);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.i(null, "OnTouchListener--onTouch-- action="+event.getAction()+" --"+v);
return false;
}
@Override
public void onClick(View v) {
Log.i(null, "OnClickListener--onClick--"+v);
}
}
~~~
到此基礎示例的代碼編寫完成。沒有啥難度,很簡單易懂,不多解釋了。
#### 2-2 運行現象
當直接點擊Button時打印現象如下:
~~~
TestLinearLayout dispatchTouchEvent-- action=0
TestLinearLayout onInterceptTouchEvent-- action=0
TestButton dispatchTouchEvent-- action=0
OnTouchListener--onTouch-- action=0 --com.zzci.light.TestButton
TestButton onTouchEvent-- action=0
TestLinearLayout dispatchTouchEvent-- action=1
TestLinearLayout onInterceptTouchEvent-- action=1
TestButton dispatchTouchEvent-- action=1
OnTouchListener--onTouch-- action=1 --com.zzci.light.TestButton
TestButton onTouchEvent-- action=1
OnClickListener--onClick--com.zzci.light.TestButton
~~~
分析:你會發現這個結果好驚訝吧,點擊了Button卻先執行了TestLinearLayout(ViewGroup)的dispatchTouchEvent,接著執行
TestLinearLayout(ViewGroup)的onInterceptTouchEvent,接著執行TestButton(TestLinearLayout包含的成員View)的dispatchTouchEvent
,接著就是View觸摸事件的分發流程,上一篇已經講過了。也就是說當點擊View時事件派發每一個down,up的action順序是先觸發最父級控件
(這里為LinearLayout)的dispatchTouchEvent->onInterceptTouchEvent->然后向前一級傳遞(這里就是傳遞到Button View)。
那么繼續看,當直接點擊除Button以外的其他部分時打印如下:
~~~
TestLinearLayout dispatchTouchEvent-- action=0
TestLinearLayout onInterceptTouchEvent-- action=0
OnTouchListener--onTouch-- action=0 --com.zzci.light.TestLinearLayout
TestLinearLayout onTouchEvent-- action=0
TestLinearLayout dispatchTouchEvent-- action=1
OnTouchListener--onTouch-- action=1 --com.zzci.light.TestLinearLayout
TestLinearLayout onTouchEvent-- action=1
OnClickListener--onClick--com.zzci.light.TestLinearLayout
~~~
分析:你會發現一個奇怪的現象,派發ACTION_DOWN(action=0)事件時順序為dispatchTouchEvent->onInterceptTouchEvent->onTouch
->onTouchEvent,而接著派發ACTION_UP(action=1)事件時與上面順序不同的時竟然沒觸發onInterceptTouchEvent方法。這是為啥呢?
我也納悶,那就留著下面分析源碼再找答案吧,先記住這個問題。
有了上面這個例子你是不是發現包含ViewGroup與View的事件觸發有些相似又有很大差異吧(PS:在Android中繼承View實現的控件已經是最小單位了,也即在XML布局等操作中不能再包含子項了,而繼承ViewGroup實現的控件通常不是最小單位,可以包含不確定數目的子項)。具體差異是啥呢?咱們類似上篇一樣,帶著這個實例疑惑去看源碼找答案吧。
### 3 Android 5.1.1(API 22) ViewGroup觸摸屏事件傳遞源碼分析
通過上面例子的打印我們可以確定分析源碼的順序,那就開始分析唄。
#### 3-1 從ViewGroup的dispatchTouchEvent方法說起
前一篇的3-2小節說在Android中你只要觸摸控件首先都會觸發控件的dispatchTouchEvent方法(其實這個方法一般都沒在具體的控件類中,而在他的父類View中)。這其實是思維單單局限在View的角度去看待的,這里通過上面的例子你是否發現觸摸控件會先從他的父級dispatchTouchEvent方法開始派發呢?是的,所以咱們先從ViewGroup的dispatchTouchEvent方法說起,如下:
~~~
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 = 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);
// 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;
}
~~~
我勒個去!!!這比View的dispatchTouchEvent方法長很多啊,那就只關注重點分析吧。
第一步,17-24行,對ACTION_DOWN進行處理。
因為ACTION_DOWN是一系列事件的開端,當是ACTION_DOWN時進行一些初始化操作,從上面源碼中注釋也可以看出來,清除以往的Touch狀態然后開始新的手勢。在這里你會發現cancelAndClearTouchTargets(ev)方法中有一個非常重要的操作就是將mFirstTouchTarget設置為了null(剛開始分析大眼瞄一眼沒留意,結果越往下看越迷糊,所以這個是分析ViewGroup的dispatchTouchEvent方法第一步中重點要記住的一個地方),接著在resetTouchState()方法中重置Touch狀態標識。
第二步,26-47行,檢查是否要攔截。
在dispatchTouchEvent(MotionEvent ev)這段代碼中使用變量intercepted來標記ViewGroup是否攔截Touch事件的傳遞,該變量類似第一步的mFirstTouchTarget變量,在后續代碼中起著很重要的作用。`if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)`這一條判斷語句說明當事件為ACTION_DOWN或者mFirstTouchTarget不為null(即已經找到能夠接收touch事件的目標組件)時if成立,否則if不成立,然后將intercepted設置為true,也即攔截事件。當當事件為ACTION_DOWN或者mFirstTouchTarget不為null時判斷disallowIntercept(禁止攔截)標志位,而這個標記在ViewGroup中提供了public的設置方法,如下:
~~~
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
~~~
所以你可以在其他地方調用requestDisallowInterceptTouchEvent(boolean disallowIntercept)方法,從而禁止執行是否需要攔截的判斷。
當disallowIntercept為true(禁止攔截判斷)時則intercepted直接設置為false,否則調用onInterceptTouchEvent(ev)方法,然后將結果賦值給
intercepted。那就來看下ViewGroup與眾不同與View特有的onInterceptTouchEvent方法,如下:
~~~
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
~~~
看見了吧,默認的onInterceptTouchEvent方法只是返回了一個false,也即intercepted=false。所以可以說明上面例子的部分打印
(dispatchTouchEvent->onInterceptTouchEvent->onTouchEvent),這里很明顯表明在ViewGroup的dispatchTouchEvent()中默認
(不在其他地方調運requestDisallowInterceptTouchEvent方法設置禁止攔截標記)首先調用了onInterceptTouchEvent()方法。
第三步,49-51行,檢查cancel。
通過標記和action檢查cancel,然后將結果賦值給局部boolean變量canceled。
第四步,53-函數結束,事件分發。
54行首先可以看見獲取一個boolean變量標記split來標記,默認是true,作用是是否把事件分發給多個子View,這個同樣在ViewGroup中提供了public的方法設置,如下:
~~~
public void setMotionEventSplittingEnabled(boolean split) {
// TODO Applications really shouldn't change this setting mid-touch event,
// but perhaps this should handle that case and send ACTION_CANCELs to any child views
// with gestures in progress when this is changed.
if (split) {
mGroupFlags |= FLAG_SPLIT_MOTION_EVENTS;
} else {
mGroupFlags &= ~FLAG_SPLIT_MOTION_EVENTS;
}
}
~~~
接著57行`if (!canceled && !intercepted)`判斷表明,事件不是ACTION_CANCEL并且ViewGroup的攔截標志位intercepted為false(不攔截)則會進入
其中。事件分發步驟中關于ACTION_DOWN的特殊處理
接著67行這個很大的if語句`if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) ||?`
`actionMasked == MotionEvent.ACTION_HOVER_MOVE)`處理ACTION_DOWN事件,這個環節比較繁瑣,也比較重要,如下具體分析。
在79行判斷了childrenCount個數是否不為0,然后接著在84行拿到了子View的list集合preorderedList;接著在88行通過一個for循環i從childrenCount - 1開始遍歷到0,倒序遍歷所有的子view,這是因為preorderedList中的順序是按照addView或者XML布局文件中的順序來的,后addView添加的子View,會因為Android的UI后刷新機制顯示在上層;假如點擊的地方有兩個子View都包含的點擊的坐標,那么后被添加到布局中的那個子view會先響應事件;這樣其實也是符合人的思維方式的,因為后被添加的子view會浮在上層,所以我們去點擊的時候一般都會希望點擊最上層的那個組件先去響應事件。
接著在106到112行通過getTouchTarget去查找當前子View是否在mFirstTouchTarget.next這條target鏈中的某一個targe中,如果在則返回這個target,否則返回null。在這段代碼的if判斷通過說明找到了接收Touch事件的子View,即newTouchTarget,那么,既然已經找到了,所以執行break跳出for循環。如果沒有break則繼續向下執行走到115行開始到134行,這里你可以看見一段if判斷的代碼`if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign))`,這個被if的大括弧括起來的一段代碼很重要,具體解釋如下:
調用方法dispatchTransformedTouchEvent()將Touch事件傳遞給特定的子View。該方法十分重要,在該方法中為一個遞歸調用,會遞歸調用dispatchTouchEvent()方法。在dispatchTouchEvent()中如果子View為ViewGroup并且Touch沒有被攔截那么遞歸調用dispatchTouchEvent(),如果子View為View那么就會調用其onTouchEvent()。dispatchTransformedTouchEvent方法如果返回true則表示子View消費掉該事件,同時進入
該if判斷。滿足if語句后重要的操作有:
- 給newTouchTarget賦值;
- 給alreadyDispatchedToNewTouchTarget賦值為true;
- 執行break,因為該for循環遍歷子View判斷哪個子View接受Touch事件,既然已經找到了就跳出該外層for循環;
如果115行if判斷中的dispatchTransformedTouchEvent()方法返回false,即子View的onTouchEvent返回false(即Touch事件未被消費),那么就不滿足該if條件,也就無法執行addTouchTarget(),從而導致mFirstTouchTarget為null(沒法對mFirstTouchTarget賦值,因為上面分析了mFirstTouchTarget一進來是ACTION_DOWN就置位為null了),那么該子View就無法繼續處理ACTION_MOVE事件和ACTION_UP事件(28行的判斷為false,也即intercepted=true了,所以之后一系列判斷無法通過)。
如果115行if判斷中的dispatchTransformedTouchEvent()方法返回true,即子View的onTouchEvent返回true(即Touch事件被消費),那么就滿足該if條件,從而mFirstTouchTarget不為null。
繼續看143行的判斷`if (newTouchTarget == null && mFirstTouchTarget != null)`。該if表示經過前面的for循環沒有找到子View接收Touch事件并且之前的mFirstTouchTarget不為空則為真,然后newTouchTarget指向了最初的TouchTarget。
通過上面67到157行關于事件分發步驟中ACTION_DOWN的特殊處理可以發現,對于此處ACTION_DOWN的處理具體體現在dispatchTransformedTouchEvent()方法,該方法返回值具備如下特征:
| return | description | set |
|-----|-----|-----|
| true | 事件被消費 | mFirstTouchTarget!=null |
| false | 事件未被消費 | mFirstTouchTarget==null |
因為在dispatchTransformedTouchEvent()會調用遞歸調用dispatchTouchEvent()和onTouchEvent(),所以dispatchTransformedTouchEvent()的返回值實際上是由onTouchEvent()決定的。簡單地說onTouchEvent()是否消費了Touch事件的返回值決定了dispatchTransformedTouchEvent()的返回值,從而決定mFirstTouchTarget是否為null,進一步決定了ViewGroup是否處理Touch事件,這一點在160行開始的代碼中有體現。如下分析事件分發步驟中關于ACTION_DOWN處理之后的其他處理邏輯,也即160行開始剩余的邏輯。
事件分發步驟中關于ACTION_DOWN處理之后的其他處理邏輯
可以看到,如果派發的事件不是ACTION_DOWN就不會經過上面的流程,而是直接從此處開始執行。上面說了,經過上面對于ACTION_DOWN的處理后mFirstTouchTarget可能為null或者不為null。所以可以看見161行代碼`if (mFirstTouchTarget == null)與else`判斷了mFirstTouchTarget值是否為null的情況,完全符合如上分析。那我們分情況繼續分析一下:
當161行if判斷的mFirstTouchTarget為null時,也就是說Touch事件未被消費,即沒有找到能夠消費touch事件的子組件或Touch事件被攔截了,則調用ViewGroup的dispatchTransformedTouchEvent()方法處理Touch事件(和普通View一樣),即子View沒有消費Touch事件,那么子View的上層ViewGroup才會調用其onTouchEvent()處理Touch事件。具體就是在調用dispatchTransformedTouchEvent()時第三個參數為null,關于dispatchTransformedTouchEvent方法下面會分析,暫時先記住就行。
這下再回想上面例子,點擊Button時為啥觸發了Button的一系列touch方法而沒有觸發父級LinearLayout的touch方法的疑惑?明白了吧?
子view對于Touch事件處理返回true那么其上層的ViewGroup就無法處理Touch事件了,子view對于Touch事件處理返回false那么其上層的ViewGroup才可以處理Touch事件。
當161行if判斷的mFirstTouchTarget不為null時,也就是說找到了可以消費Touch事件的子View且后續Touch事件可以傳遞到該子View。可以看見在源碼的else中對于非ACTION_DOWN事件繼續傳遞給目標子組件進行處理,依然是遞歸調用dispatchTransformedTouchEvent()方法來實現的處理。
到此ViewGroup的dispatchTouchEvent方法分析完畢。
上面說了ViewGroup的dispatchTouchEvent方法詳細情況,也知道在其中可能會執行onInterceptTouchEvent方法,所以接下來咱們先簡單分析一下這個方法。
#### 3-2 說說ViewGroup的dispatchTouchEvent中可能執行的onInterceptTouchEvent方法
如下系統源碼:
~~~
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
~~~
看到了吧,這個方法算是ViewGroup不同于View特有的一個事件派發調運方法。在源碼中可以看到這個方法實現很簡單,但是有一堆注釋。其實上面
分析了,如果ViewGroup的onInterceptTouchEvent返回false就不阻止事件繼續傳遞派發,否則阻止傳遞派發。
對了,還記得在dispatchTouchEvent方法中除過可能執行的onInterceptTouchEvent以外在后面派發事件時執行的dispatchTransformedTouchEvent方法嗎?上面分析dispatchTouchEvent時說了下面會仔細分析,那么現在就來繼續看看這個方法吧。
#### 3-3 繼續說說ViewGroup的dispatchTouchEvent中執行的dispatchTransformedTouchEvent方法
ViewGroup的dispatchTransformedTouchEvent方法系統源碼如下:
~~~
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.
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;
}
// 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.
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) {
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);
}
// Done.
transformedEvent.recycle();
return handled;
}
~~~
看到了吧,這個方法也算是ViewGroup不同于View特有的一個事件派發調運方法,而且奇葩的就是這個方法也很長。那也繼續分析吧。。。
上面分析了,在dispatchTouchEvent()中調用dispatchTransformedTouchEvent()將事件分發給子View處理。在此我們需要重點分析該方法的第三個參數(View child)。在dispatchTouchEvent()中多次調用了dispatchTransformedTouchEvent()方法,而且有時候第三個參數為null,有時又不是,他們到底有啥區別呢?這段源碼中很明顯展示了結果。在dispatchTransformedTouchEvent()源碼中可以發現多次對于child是否為null的判斷,并且均做出如下類似的操作。其中,當child == null時會將Touch事件傳遞給該ViewGroup自身的dispatchTouchEvent()處理,即super.dispatchTouchEvent(event)(也就是View的這個方法,因為ViewGroup的父類是View);當child != null時會調用該子view(當然該view可能是一個View也可能是一個ViewGroup)的dispatchTouchEvent(event)處理,即child.dispatchTouchEvent(event)。別的代碼幾乎沒啥需要具體注意分析的。
所以,到此你也會發現ViewGroup沒有重寫View的onTouchEvent(MotionEvent event) 方法,也就是說接下來的調運關系就是上一篇分析的流程了,這里不在多說。
好了,到此你是不是即明白了上面實例演示的代碼結果,也明白了上一篇最后升級實例驗證模塊留下的點擊Button觸發了LinearLayout的一些疑惑呢?答案自然是必須的!
### 4 Android 5.1.1(API 22) ViewGroup觸摸屏事件傳遞總結
如上就是所有ViewGroup關于觸摸屏事件的傳遞機制源碼分析與實例演示。具體總結如下:
1. Android事件派發是先傳遞到最頂級的ViewGroup,再由ViewGroup遞歸傳遞到View的。
1. 在ViewGroup中可以通過onInterceptTouchEvent方法對事件傳遞進行攔截,onInterceptTouchEvent方法返回true代表不允許事件繼續向子View傳遞,返回false代表不對事件進行攔截,默認返回false。
1. 子View中如果將傳遞的事件消費掉,ViewGroup中將無法接收到任何事件。
【工匠若水?[http://blog.csdn.net/yanbober](http://blog.csdn.net/yanbober)】
好了,至此整個View與ViewGroup的觸摸屏事件派發機制分析完畢。關于他們的事件是哪派發來的可以繼續進階的閱讀下一篇[《Android觸摸屏事件派發機制詳解與源碼分析三(Activity篇)》](http://blog.csdn.net/yanbober/article/details/45932123)
- 前言
- Android觸摸屏事件派發機制詳解與源碼分析一(View篇)
- Android觸摸屏事件派發機制詳解與源碼分析二(ViewGroup篇)
- Android觸摸屏事件派發機制詳解與源碼分析三(Activity篇)
- Android應用setContentView與LayoutInflater加載解析機制源碼分析
- Android應用Context詳解及源碼解析
- Android異步消息處理機制詳解及源碼分析
- Android應用Activity、Dialog、PopWindow、Toast窗口添加機制及源碼分析
- Android ListView工作原理完全解析,帶你從源碼的角度徹底理解
- Activity啟動過程全解析
- Android應用AsyncTask處理機制詳解及源碼分析
- 說說 PendingIntent 的內部機制
- Android Activity.startActivity流程簡介
- Activity界面顯示全解析
- 框架層理解Activity生命周期(APP啟動過程)
- APK安裝過程及原理詳解
- Android構建過程簡述
- Android應用層View繪制流程與源碼分析