1、前言
Android學習一段時間,需求做多了必然會遇到滑動衝突問題,比如在一個ScrollView中要嵌套一個地圖View,這時候觸摸移動地圖或者放大縮小地圖就會變得不太準確甚至沒有反應,這就是遇到了滑動衝突,ScrollView中上下滑動與地圖的觸摸手勢發生衝突。想要解決滑動衝突就不得不提到Android的事件分發機制,只有吃透了事件分發,才能對滑動衝突的解決得心應手。
2、事件分發機制相關方法
Android事件分發機制主要相關方法有以下三個:
- 事件分發:public boolean dispatchTouchEvent(MotionEvent ev)
- 事件攔截:public boolean onInterceptTouchEvent(MotionEvent ev)
- 事件響應:public boolean onTouchEvent(MotionEvent ev)
以下是這三個方法在Activity、ViewGroup和View中的存在情況:
相關方法 | Activity | ViewGroup | View |
---|---|---|---|
dispatchTouchEvent | yes | yes | yes |
onInterceptTouchEvent | no | yes | no |
onTouchEvent | yes | yes | yes |
這三個方法都返回一個布爾類型,根據返回的不同對事件進行不同的分發攔截和響應。一般有三種返回true
、false
和super
引用父類對應方法。
dispatchTouchEvent 返回true:表示改事件在本層不再進行分發且已經在事件分發自身中被消費了。
dispatchTouchEvent 返回 false:表示事件在本層不再繼續進行分發,並交由上層控件的onTouchEvent
方法進行消費。
onInterceptTouchEvent 返回true:表示將事件進行攔截,並將攔截到的事件交由本層控件 的onTouchEvent
進行處理。
onInterceptTouchEvent 返回false:表示不對事件進行攔截,事件得以成功分發到子View
。並由子View
的dispatchTouchEvent
進行處理。
onTouchEvent 返回 true:表示onTouchEvent
處理完事件後消費了此次事件。此時事件終結,將不會進行後續的傳遞。
onTouchEvent 返回 false:事件在onTouchEvent
中處理後繼續向上層View傳遞,且有上層View
的onTouchEvent
進行處理。
除此之外還有一個方法也是經常用到的:
- public void requestDisallowInterceptTouchEvent(boolean disallowIntercept)
它的作用是子View用來通知父View不要攔截事件。下面先寫一個簡單的Demo
來看一下事件分發和傳遞:
簡單的日誌的Demo:
這裏的代碼只是自定義了兩個ViewGroup
和一個View
,在其對應事件分發傳遞方法中打印日誌,來查看調用順序情況,所有相關分發傳遞方法返回皆是super
父類方法。
例如: MyViewGroupA.java:
public class MyViewGroupA extends RelativeLayout {
public MyViewGroupA(Context context) {
super(context);
}
public MyViewGroupA(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyViewGroupA(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
Log.d(getClass().getSimpleName(),"dispatchTouchEvent:ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.d(getClass().getSimpleName(),"dispatchTouchEvent:ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.d(getClass().getSimpleName(),"dispatchTouchEvent:ACTION_UP");
break;
} return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_UP");
break;
} return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_UP");
break;
} return super.onTouchEvent(event);
}
}
其他的代碼都是類似的,這裏再貼一下Acitivity裏的佈局:
<?xml version="1.0" encoding="utf-8"?>
<com.example.sy.eventdemo.MyViewGroupA xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/viewGroupA"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorPrimary"
tools:context=".MainActivity">
<com.example.sy.eventdemo.MyViewGroupB
android:id="@+id/viewGroupB"
android:layout_width="300dp"
android:layout_height="300dp"
android:layout_centerInParent="true"
android:background="@android:color/white">
<com.example.sy.eventdemo.MyView
android:id="@+id/myView"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_centerInParent="true"
android:background="@android:color/holo_orange_light" />
</com.example.sy.eventdemo.MyViewGroupB>
</com.example.sy.eventdemo.MyViewGroupA>
Demo中的Activity佈局層級關係:
除去外層Activity和Window的層級,從MyViewGroup開始是自己定義的打印日誌View。接下來運行Demo查看日誌:
D/MainActivity: dispatchTouchEvent:ACTION_DOWN
D/MyViewGroupA: dispatchTouchEvent:ACTION_DOWN
D/MyViewGroupA: onInterceptTouchEvent:ACTION_DOWN
D/MyViewGroupB: dispatchTouchEvent:ACTION_DOWN
D/MyViewGroupB: onInterceptTouchEvent:ACTION_DOWN
D/MyView: dispatchTouchEvent:ACTION_DOWN
D/MyView: onTouchEvent:ACTION_DOWN
D/MyViewGroupB: onTouchEvent:ACTION_DOWN
D/MyViewGroupA: onTouchEvent:ACTION_DOWN
D/MainActivity: onTouchEvent:ACTION_DOWN
D/MainActivity: dispatchTouchEvent:ACTION_MOVE
D/MainActivity: onTouchEvent:ACTION_MOVE
D/MainActivity: dispatchTouchEvent:ACTION_UP
D/MainActivity: onTouchEvent:ACTION_UP
結合日誌可以大概看出(先只看ACTION_DOWN
事件):
事件的分發順序:Activity-->MyViewGroupA-->MyViewGroupB-->MyView
自頂向下分發
事件的響應順序:MyView-->MyViewGroupB-->MyViewGroupA-->Activity
自底向上響應消費
同時這裏通過日誌也發現一個問題:
-
問題一:爲什麼這裏只有
ACTION_DOWN
事件有完整的從Activity到ViewGroup再到View的分發攔截和響應的運行日誌,爲什麼ACTION_MOVE
和ACTION_UP
事件沒有?
接着再測試一下之前提的requestDisallowInterceptTouchEvent
方法的使用。現在佈局文件中將MyView添加一個屬性android:clickable="true"
。此時在運行點擊打印日誌是這樣的:
/-------------------ACTION_DOWN事件------------------
D/MainActivity: dispatchTouchEvent:ACTION_DOWN
D/MyViewGroupA: dispatchTouchEvent:ACTION_DOWN
D/MyViewGroupA: onInterceptTouchEvent:ACTION_DOWN
D/MyViewGroupB: dispatchTouchEvent:ACTION_DOWN
D/MyViewGroupB: onInterceptTouchEvent:ACTION_DOWN
D/MyView: dispatchTouchEvent:ACTION_DOWN
D/MyView: onTouchEvent:ACTION_DOWN
/-------------------ACTION_MOVE事件------------------
D/MainActivity: dispatchTouchEvent:ACTION_MOVE
D/MyViewGroupA: dispatchTouchEvent:ACTION_MOVE
D/MyViewGroupA: onInterceptTouchEvent:ACTION_MOVE
D/MyViewGroupB: dispatchTouchEvent:ACTION_MOVE
D/MyViewGroupB: onInterceptTouchEvent:ACTION_MOVE
D/MyView: dispatchTouchEvent:ACTION_MOVE
D/MyView: onTouchEvent:ACTION_MOVE
/-------------------ACTION_UP事件------------------
D/MainActivity: dispatchTouchEvent:ACTION_UP
D/MyViewGroupA: dispatchTouchEvent:ACTION_UP
D/MyViewGroupA: onInterceptTouchEvent:ACTION_UP
D/MyViewGroupB: dispatchTouchEvent:ACTION_UP
D/MyViewGroupB: onInterceptTouchEvent:ACTION_UP
D/MyView: dispatchTouchEvent:ACTION_UP
D/MyView: onTouchEvent:ACTION_UP
這下ACTION_MOVE
和ACTION_UP
事件也有日誌了。接下來在MyViewGroupB的onInterceptTouchEvent
的方法中修改代碼如下:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_DOWN");
return false;
case MotionEvent.ACTION_MOVE:
Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_MOVE");
return true;
case MotionEvent.ACTION_UP:
Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_UP");
return true;
}
return false;
}
也就是攔截下ACTION_MOVE
和ACTION_UP
事件不攔截下ACTION_DOWN
事件,然後在運行查看日誌:
/------------------ACTION_DOWN事件------------------------------
D/MainActivity: dispatchTouchEvent:ACTION_DOWN
D/MyViewGroupA: dispatchTouchEvent:ACTION_DOWN
D/MyViewGroupA: onInterceptTouchEvent:ACTION_DOWN
D/MyViewGroupB: dispatchTouchEvent:ACTION_DOWN
D/MyViewGroupB: onInterceptTouchEvent:ACTION_DOWN
D/MyView: dispatchTouchEvent:ACTION_DOWN
D/MyView: onTouchEvent:ACTION_DOWN
/------------------ACTION_MOVE事件-----------------------------
D/MainActivity: dispatchTouchEvent:ACTION_MOVE
D/MyViewGroupA: dispatchTouchEvent:ACTION_MOVE
D/MyViewGroupA: onInterceptTouchEvent:ACTION_MOVE
D/MyViewGroupB: dispatchTouchEvent:ACTION_MOVE
D/MyViewGroupB: onInterceptTouchEvent:ACTION_MOVE
/------------------ACTION_UP事件-------------------------------
D/MainActivity: dispatchTouchEvent:ACTION_UP
D/MyViewGroupA: dispatchTouchEvent:ACTION_UP
D/MyViewGroupA: onInterceptTouchEvent:ACTION_UP
D/MyViewGroupB: dispatchTouchEvent:ACTION_UP
D/MyViewGroupB: onTouchEvent:ACTION_UP
D/MainActivity: onTouchEvent:ACTION_UP
根據日誌可知ACTION_MOVE
和ACTION_UP
事件傳遞到MyViewGroupB就沒有再向MyView傳遞了。接着在MyView的onTouchEvent方法中調用requestDisallowInterceptTouchEvent
方法通知父容器不要攔截事件。
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_UP");
break;
}
return super.onTouchEvent(event);
}
再次運行查看日誌:
/------------------ACTION_DOWN事件------------------------------
D/MainActivity: dispatchTouchEvent:ACTION_DOWN
D/MyViewGroupA: dispatchTouchEvent:ACTION_DOWN
D/MyViewGroupA: onInterceptTouchEvent:ACTION_DOWN
D/MyViewGroupB: dispatchTouchEvent:ACTION_DOWN
D/MyViewGroupB: onInterceptTouchEvent:ACTION_DOWN
D/MyView: dispatchTouchEvent:ACTION_DOWN
D/MyView: onTouchEvent:ACTION_DOWN
/------------------ACTION_MOVE事件-----------------------------
D/MainActivity: dispatchTouchEvent:ACTION_MOVE
D/MyViewGroupA: dispatchTouchEvent:ACTION_MOVE
D/MyViewGroupB: dispatchTouchEvent:ACTION_MOVE
D/MyView: dispatchTouchEvent:ACTION_MOVE
D/MyView: onTouchEvent:ACTION_MOVE
/------------------ACTION_UP事件-------------------------------
D/MainActivity: dispatchTouchEvent:ACTION_UP
D/MyViewGroupA: dispatchTouchEvent:ACTION_UP
D/MyViewGroupB: dispatchTouchEvent:ACTION_UP
D/MyView: dispatchTouchEvent:ACTION_UP
D/MyView: onTouchEvent:ACTION_UP
這時可以發現ACTION_MOVE
和ACTION_UP
事件又傳遞到了MyView中並且兩個ViewGroup中都沒有執行onInterceptTouchEvent
方法。 明顯是requestDisallowInterceptTouchEvent
方法起了作用。但是又出現了兩個新問題。
- 問題二:爲什麼將設置
clickable="true"
之後ACTION_MOVE
和ACTION_UP
事件就會執行了? - 問題三:
requestDisallowInterceptTouchEvent
方法是怎樣通知父View不攔截事件,爲什麼連onInterceptTouchEvent
方法也不執行了?
想弄明白這些問題就只能到源碼中尋找答案了。
3、事件分發機制源碼
在正式看源碼之前先講一個概念:事件序列
我們常說的事件,一般是指從手指觸摸到屏幕在到離開屏幕這麼一個過程。在這個過程中其實會產生多個事件,一般是以ACTION_DOWN
作爲開始,中間存在多個ACTION_MOVE
,最後以ACTION_UP
結束。我們稱一次ACTION_DOWN-->ACTION_MOVE-->ACTION_UP
過程稱爲一個事件序列。
ViewGroup中有一個內部類TouchTarget,這個類將消費事件的View封裝成一個節點,使得可以將一個事件序列的DOWN
、MOVE
、UP
事件構成一個單鏈表保存。ViewGroup中也有個TouchTarget
類型的成員mFirstTouchTarget
用來指向這個單鏈表頭。在每次DOWN
事件開始時清空這個鏈表,成功消費事件後通過TouchTarget.obtain
方法獲得一個TouchTarget
,將消費事件的View傳入,然後插到單鏈表頭。後續MOVE
、UP
事件可以通過判斷mFirstTouchTarget
來知道之前是否有能夠消費事件的View。
TouchTarget的源碼:
private static final class TouchTarget {
private static final int MAX_RECYCLED = 32;
private static final Object sRecycleLock = new Object[0];
private static TouchTarget sRecycleBin;
private static int sRecycledCount;
public static final int ALL_POINTER_IDS = -1; // all ones
// The touched child view.
//接受事件的View
public View child;
// The combined bit mask of pointer ids for all pointers captured by the target.
public int pointerIdBits;
// The next target in the target list.
//下一個TouchTarget的地址
public TouchTarget next;
private TouchTarget() {
}
public static TouchTarget obtain(@NonNull View child, int pointerIdBits) {
if (child == null) {
throw new IllegalArgumentException("child must be non-null");
}
final TouchTarget target;
synchronized (sRecycleLock) {
if (sRecycleBin == null) {
target = new TouchTarget();
} else {
target = sRecycleBin;
sRecycleBin = target.next;
sRecycledCount--;
target.next = null;
}
}
target.child = child;
target.pointerIdBits = pointerIdBits;
return target;
}
public void recycle() {
if (child == null) {
throw new IllegalStateException("already recycled once");
}
synchronized (sRecycleLock) {
if (sRecycledCount < MAX_RECYCLED) {
next = sRecycleBin;
sRecycleBin = this;
sRecycledCount += 1;
} else {
next = null;
}
child = null;
}
}
}
Activity中的dispatchTouchEvent方法:
接下來正式按照分發流程來閱讀源碼,從Activity的dispatchTouchEvent
方法開始看起,事件產生時會先調用這個方法:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
方法中先判斷事件類型是ACTION_DOWN
事件會執行onUserInteraction
方法,onUserInteraction
方法在Activity中是一個空實現,在當前Activity下按下Home或者Back鍵時會調用此方法,這裏不是重點,這裏重點是關注下ACTION_DOWN
事件,ACTION_DOWN
類型事件的判斷,在事件傳遞的邏輯中非常重要,因爲每次點擊事件都是以ACTION_DOWN
事件開頭,所以ACTION_DOWN
事件又作爲一次新的點擊事件的標記。
緊接着看,在第二個if
判斷中根據getWindow().superDispatchTouchEvent(ev)
的返回值決定了整個方法的返回。
如果getWindow().superDispatchTouchEvent(ev)
方法返回爲true
則dispatchTouchEvent
方法返回true
,否則則根據Activity中的onTouchEvent
方法的返回值返回。
Activity中的onTouchEvent方法:
先來看Activity中的onTouchEvent
方法:
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
onTouchEvent
方法中根據window的shouldCloseOnTouch
方法決定返回的結果和是否finish當前Activity。進入抽象類Window
查看shouldCloseOnTouch
方法:
/** @hide */
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN
&& isOutOfBounds(context, event) && peekDecorView() != null) {
return true;
}
return false;
}
這是個hide
方法,判斷當前事件Event是否是ACTION_DOWN
類型,當前事件點擊座標是否在範圍外等標誌位,如果爲true
就會返回到onTouchEvent
方法關閉當前Activity。
看完再回到dispatchTouchEvent
方法中,只剩下getWindow().superDispatchTouchEvent(ev)
方法,來看他啥時候返回true
啥時候返回false
。這裏的getWindow
獲取到Activity中的Window對象,調用Widnow
的superDispatchTouchEvent(ev)
方法,這個方法不在抽象類Window
當中,這裏要去查看他的實現類PhoneWindow
。
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
superDispatchTouchEvent
方法中又調用了mDecor.superDispatchTouchEvent
方法,這裏的mDecor就是外層的DecorView
,superDispatchTouchEvent
方法:
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
方法中又調用了父類的dispatchTouchEvent
方法,DecorView繼承自FrameLayout,而FrameLayout沒有重寫dispatchTouchEvent
方法所以也就是調用了其父類ViewGroup的dispatchTouchEvent
方法。
ViewGroup的dispatchTouchEvent方法:
通過以上這一系列的調用,事件終於從Activity到PhoneWindow再到DecorView最終走到了ViewGroup的dispatchTouchEvent
方法中,接下來進入ViewGroup查看它的dispatchTouchEvent
方法。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
......
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
//-----------------代碼塊-1----------------------------------------------------------------
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();
}
//------------------代碼塊-1--完------------------------------------------------------------
//------------------代碼塊-2----------------------------------------------------------------
// 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;
}
//------------------代碼塊-2--完----------------------------------------------------------
// 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;
//------------------代碼塊-3--------------------------------------------------------------
if (!canceled && !intercepted) {
......
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;
}
}
}
//------------------代碼塊-3--完----------------------------------------------------------
//------------------代碼塊-4--------------------------------------------------------------
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
//mFirstTouchTarget爲空說明沒有子View響應消費該事件
// 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;
}
}
//------------------代碼塊-4--完----------------------------------------------------------
......
return handled;
}
ViewGroup的dispatchTouchEvent
方法比較長,雖然已經省略了一部分代碼但代碼還是非常多,並且代碼中存在很多if-else
判斷,容易看着看着就迷失在if
與else
之間。所以這裏把他分成了四塊代碼來看。不過在看這四塊代碼之前先看dispatchTouchEvent
方法中第一個if
判斷:
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)){
......
}
這裏初始化的handled
就是dispatchTouchEvent
方法最後的返回值,onFilterTouchEventForSecurity
這個方法過濾了認爲不安全的事件,方法裏主要是判斷了view和window是否被遮擋,dispatchTouchEvent
方法中所有的分發邏輯都要在onFilterTouchEventForSecurity
返回爲true
的前提之下,否則直接返回handled
即爲false
。
接下來看第一段代碼:
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();
}
第一段比較少比較簡單,開始首先判斷事件類型ACTION_DOWN
事件被認爲是一個新的事件序列開始,所以重置touch狀態,將mFirstTouchTarget
鏈表置空。這裏可以進resetTouchState
方法看下,方法中除了重置了一些狀態還調用了clearTouchTargets
方法清空鏈表。
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
/**
* Clears all touch targets.
*/
private void clearTouchTargets() {
TouchTarget target = mFirstTouchTarget;
if (target != null) {
do {
TouchTarget next = target.next;
target.recycle();
target = next;
} while (target != null);
mFirstTouchTarget = null;
}
}
接着看到代碼塊2:
// Check for interception.
//檢查是否攔截事件
final boolean intercepted;
//是ACTION_DOWN事件或者mFirstTouchTarget不爲空進入if
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//繼續判斷是否在調用了requestDisallowInterceptTouchEvent(true)設置了禁止攔截標記
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
//設置禁止攔截設標記disallowIntercept爲true,!disallowIntercept即爲false
if (!disallowIntercept) {
//根據ViewGroup的nInterceptTouchEvent(ev)方法返回是否攔截
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.
//不是ACTION_DOWN事件或者mFirstTouchTarget=null,就攔截
intercepted = true;
}
這段代碼中主要是判斷是否對事件進行攔截,intercepted
是攔截標記,true
代表攔截,false
表示不攔截。這裏首先判斷是事件類型是DOWN
或者mFirstTouchTarget
不等於空(不等於空說明有子View消費了之前的DOWN
事件),滿足這個條件,就進入if進一步判斷,否則直接設置intercepted
爲false
不攔截。在if中判斷FLAG_DISALLOW_INTERCEPT
這個標記位,這個標記位就是在requestDisallowInterceptTouchEvent()
方法中設置的。這裏跳到requestDisallowInterceptTouchEvent(true)
方法來看一下:
@Override
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
方法里根據disallowIntercept
進行不同位運算,mGroupFlags
默認爲0,FLAG_DISALLOW_INTERCEPT
爲0x80000
,如果傳入設置爲true
,則進行或運算,mGroupFlags
結果爲0x80000
,再回到代碼塊2裏和FLAG_DISALLOW_INTERCEPT
做與運算結果仍爲0x80000
,此時不等於0。反之傳入false
,最終位運算結果爲0。也就是說調用requestDisallowInterceptTouchEvent
方法傳入true
導致disallowIntercep
爲true
,進而導致if
條件不滿足,使得intercepted
爲false
此時對事件進行攔截。反之,則進入if
代碼塊調用onInterceptTouchEvent(ev)
方法,根據返回值來決定是否攔截。
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;
//再判斷事件類型是DOWN事件繼續執行if代碼塊,這裏的三個標記分別對應單點觸摸DOWN多點觸摸DOWN和鼠標移動事件
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);
//這裏拿到子VIew個數
final int childrenCount = mChildrenCount;
//循環子View找到可以響應事件的子View將事件分發
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;
}
//這個子View無法接受這個事件或者事件點擊不在這個子View內就跳過這次循環
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
//到這裏說明這個子View可以處理該事件,就到TochTarget鏈表裏去找對應的TochTarget,沒找到返回null
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.
//不爲空說明view已經處理過這個事件,說明是多點觸摸,就再加一個指針
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
//調用dispatchTransformedTouchEvent方法將事件分發給子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();
//dispatchTransformedTouchEvent返回true說明子View響應消費了這個事件
//於是調用addTouchTarget方法獲得包含這個View的TouchTarget節點並將其添加到鏈表頭
newTouchTarget = addTouchTarget(child, idBitsToAssign);
//將已經分發的標記設置爲true
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();
}
//如果newTouchTarget爲null且mFirstTouchTarget不爲null,說明沒找到子View來響應消費該事件,但是TouchTarget鏈表不爲空
//則將newTouchTarget賦爲TouchTarget鏈表中mFirstTouchTarget.next
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;
}
}
}