Android學習一段時間,需求作多了必然會遇到滑動衝突問題,好比在一個ScrollView中要嵌套一個地圖View,這時候觸摸移動地圖或者放大縮小地圖就會變得不太準確甚至沒有反應,這就是遇到了滑動衝突,ScrollView中上下滑動與地圖的觸摸手勢發生衝突。想要解決滑動衝突就不得不提到Android的事件分發機制,只有吃透了事件分發,才能對滑動衝突的解決駕輕就熟。java
Android事件分發機制主要相關方法有如下三個:android
如下是這三個方法在Activity、ViewGroup和View中的存在狀況:api
相關方法 | Activity | ViewGroup | View |
---|---|---|---|
dispatchTouchEvent | yes | yes | yes |
onInterceptTouchEvent | no | yes | no |
onTouchEvent | yes | yes | yes |
這三個方法都返回一個布爾類型,根據返回的不一樣對事件進行不一樣的分發攔截和響應。通常有三種返回true
、false
和super
引用父類對應方法。安全
dispatchTouchEvent 返回true:表示改事件在本層再也不進行分發且已經在事件分發自身中被消費了。
dispatchTouchEvent 返回 false:表示事件在本層再也不繼續進行分發,並交由上層控件的onTouchEvent
方法進行消費。bash
onInterceptTouchEvent 返回true:表示將事件進行攔截,並將攔截到的事件交由本層控件 的onTouchEvent
進行處理。
onInterceptTouchEvent 返回false:表示不對事件進行攔截,事件得以成功分發到子View
。並由子View
的dispatchTouchEvent
進行處理。markdown
onTouchEvent 返回 true:表示onTouchEvent
處理完事件後消費了這次事件。此時事件終結,將不會進行後續的傳遞。
onTouchEvent 返回 false:事件在onTouchEvent
中處理後繼續向上層View傳遞,且有上層View
的onTouchEvent
進行處理。app
除此以外還有一個方法也是常常用到的:ide
它的做用是子View用來通知父View不要攔截事件。下面先寫一個簡單的Demo
來看一下事件分發和傳遞:oop
這裏的代碼只是自定義了兩個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佈局層級關係:
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
方法也不執行了?想弄明白這些問題就只能到源碼中尋找答案了。
在正式看源碼以前先講一個概念:事件序列
咱們常說的事件,通常是指從手指觸摸到屏幕在到離開屏幕這麼一個過程。在這個過程當中其實會產生多個事件,通常是以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
方法開始看起,事件產生時會先調用這個方法:
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
方法:
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
方法。
經過以上這一系列的調用,事件終於從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; } } } 複製代碼
接着看代碼塊3,在這段很長的代碼裏,首先一個if
中判斷了該事件是否知足沒有被攔截和被取消,以後第二個if
判斷了事件類型是否爲DOWN
,知足了沒有被攔截和取消的DOWN
事件,接下來ViewGroup纔會循環其子View找到點擊事件在其內部而且可以接受該事件的子View,再經過調用dispatchTransformedTouchEvent
方法將事件分發給該子View處理,返回true說明子View成功消費事件,因而調用addTouchTarget
方法,方法中經過TouchTarget.obtain
方法得到一個包含這View的TouchTarget
節點並將其添加到鏈表頭,並將已經分發的標記設置爲true
。
接下來看代碼塊4:
// Dispatch to touch targets. //走到這裏說明在循環遍歷全部子View後沒有找到接受該事件或者事件不是DOWN事件或者該事件已被攔截或取消 if (mFirstTouchTarget == null) { //mFirstTouchTarget爲空說明沒有子View響應消費該事件 //全部調用dispatchTransformedTouchEvent方法分發事件 //注意這裏第三個參數傳的是null,方法裏會調用super.dispatchTouchEvent(event)即View.dispatchTouchEvent(event)方法 // No touch targets so treat this as an ordinary view. handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { // mFirstTouchTarget不爲空說明有子View能響應消費該事件,消費過以前的DOWN事件,就將這個事件還分發給這個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; //這裏傳入的是target.child就是以前響應消費的View,把該事件還交給它處理 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; } } 複製代碼
以前在代碼塊3中處理分發了沒被攔截和取消的DOWN
事件,那麼其餘MOVE
、UP
等類型事件怎麼處理呢?還有若是遍歷完子View卻沒有能接受這個事件的View又怎麼處理呢?代碼塊4中就處理分發了這些事件。首先判斷mFirstTouchTarget
是否爲空,爲空說明沒有子View消費該事件,因而就調用dispatchTransformedTouchEvent
方法分發事件,這裏注意dispatchTransformedTouchEvent
方法第三個參數View傳的null
,方法裏會對於這種沒有子View能處理消費事件的狀況,就調用該ViewGroup的super.dispatchTouchEvent
方法,即View的dispatchTouchEvent
,把ViewGroup當成View來處理,把事件交給ViewGroup處理。具體看dispatchTransformedTouchEvent
方法中的這段代碼:
if (child == null) { handled = super.dispatchTouchEvent(event); } else { handled = child.dispatchTouchEvent(event); } 複製代碼
dispatchTransformedTouchEvent
方法中child
即傳入的View爲空則調用super.dispatchTouchEvent
方法分發事件,就是View類的分發方法,不爲空則調用子View方法,即child.dispatchTouchEvent
分發事件,因此歸根結底都是調用了View類的dispatchTouchEvent
方法處理。
至此,ViewGroup中的分發過流程結束,再來總結一下這個過程:首先過濾掉不安全的事件,接着若是事件類型是DOWN
事件認爲是一個新的事件序列開始,就清空TouchTarget
鏈表重置相關標誌位(代碼塊一),而後判斷是否攔截該事件,這裏有兩步判斷:一是若是是DOWN
事件或者不是DOWN
事件可是mFirstTouchTarget
不等於null
(這裏mFirstTouchTarget
若是等於null
說明以前沒有View消費DOWN
事件,在代碼塊三末尾,能夠看到若是有子View消費了DOWN
事件,會調用addTouchTarget
方法,得到一個保存了該子View的TouchTarget
,並將其添加到mFirstTouchTarget
鏈表頭),則進入第二步禁止攔截標記的判斷,不然直接設置爲須要攔截,進入第二步判斷設置過禁止攔截標記爲true
的就不攔截,不然調用ViewGroup的onInterceptTouchEvent
方法根據返回接過來決定是否攔截(代碼塊二)。接下來若是事件沒被攔截也沒被取消並且仍是DOWN
事件,就循環遍歷ViewGroup中的子View找到事件在其範圍內而且能接受事件的子View,經過dispatchTransformedTouchEvent
方法將事件分發給該子View,而後經過addTouchTarget
方法將包含該子View的TouchTarget
插到鏈表頭(代碼塊三)。最後若是沒有找到可以接受該事件的子View又或者是MOVE
、UP
類型事件等再判斷mFirstTouchTarget
是否爲空,爲空說明以前沒有View能接受消費該事件,則調用dispatchTransformedTouchEvent
方法將事件交給自身處理,不爲空則一樣調用dispatchTransformedTouchEvent
方法,可是是將事件分發給該子View處理。
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; } 複製代碼
在ViewGroup的dispatchTouchEvent
中沒設置過禁止攔截的事件默認都會經過onInterceptTouchEvent
方法來決定是否攔截,onInterceptTouchEvent
方法裏能夠看到默認是返回false
,只有在事件源類型是鼠標而且是DOWN
事件是鼠標點擊按鈕和是滾動條的手勢時才返回true
。因此默認通常ViewGroup的onInterceptTouchEvent
方法返回都爲false
,也就是說默認不攔截事件。
ViewGroup中沒有覆蓋onTouchEvent
方法,因此調用ViewGroup的onTouchEvent
方法實際上調用的仍是它的父類View的onTouchEvent
方法。
在ViewGroup中將事件不管是分發給子View的時候仍是本身處理的,最終都會執行默認的View類的dispatchTouchEvent
方法:
public boolean dispatchTouchEvent(MotionEvent event) { ...... boolean result = false; ...... if (onFilterTouchEventForSecurity(event)) { ...... ListenerInfo li = mListenerInfo; if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } if (!result && onTouchEvent(event)) { result = true; } } ...... return result; } 複製代碼
這裏一樣省略一些代碼只看關鍵的,首先一樣和ViewGroup同樣,作了事件安全性的過濾,接着先判斷了mOnTouchListener
是否爲空,不爲空而且該View是ENABLED
可用的,就會調用mOnTouchListener
的onTouch
方法,若是onTouch
方法返回true
說明事件已經被消費了,就將result
標記修改成true
,這樣他就不會走接下來的if
了。若是沒有設置mOnTouchListener
或者onTouch
方法返回false
,則會繼續調用onTouchEvent
方法。這裏能夠發現mOnTouchListener
的onTouch
方法的優先級是在onTouchEvent
以前的,若是在代碼中設置了mOnTouchListener
監聽,而且onTouch
返回true
,則這個事件就被在onTouch
裏消費了,不會在調用onTouchEvent
方法。
//這個mOnTouchListener就是常常在代碼裏設置的View.OnTouchListener mMyView.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { //這裏返回true事件就消費了,不會再調用onTouchEvent方法 return true; } }); 複製代碼
public boolean onTouchEvent(MotionEvent event) { /---------------代碼塊-1------------------------------------------------------------------- final float x = event.getX(); final float y = event.getY(); final int viewFlags = mViewFlags; final int action = event.getAction(); final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; if ((viewFlags & ENABLED_MASK) == DISABLED) { if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; // A disabled view that is clickable still consumes the touch // events, it just doesn't respond to them. return clickable; } /---------------代碼塊-1------完------------------------------------------------------------- /---------------代碼塊-2------------------------------------------------------------------- if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } /---------------代碼塊-2------完------------------------------------------------------------- /---------------代碼塊-3------------------------------------------------------------------- if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { switch (action) { case MotionEvent.ACTION_UP: mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; if ((viewFlags & TOOLTIP) == TOOLTIP) { handleTooltipUp(); } if (!clickable) { removeTapCallback(); removeLongPressCallback(); mInContextButtonPress = false; mHasPerformedLongPress = false; mIgnoreNextUpEvent = false; break; } boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { // take focus if we don't have it already and we should in // touch mode. boolean focusTaken = false; if (isFocusable() && isFocusableInTouchMode() && !isFocused()) { focusTaken = requestFocus(); } if (prepressed) { // The button is being released before we actually // showed it as pressed. Make it show the pressed // state now (before scheduling the click) to ensure // the user sees it. setPressed(true, x, y); } if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { // This is a tap, so remove the longpress check removeLongPressCallback(); // Only perform take click actions if we were in the pressed state if (!focusTaken) { // Use a Runnable and post this rather than calling // performClick directly. This lets other visual state // of the view update before click actions start. if (mPerformClick == null) { mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { //調用了OnClickListener performClick(); } } } if (mUnsetPressedState == null) { mUnsetPressedState = new UnsetPressedState(); } if (prepressed) { postDelayed(mUnsetPressedState, ViewConfiguration.getPressedStateDuration()); } else if (!post(mUnsetPressedState)) { // If the post failed, unpress right now mUnsetPressedState.run(); } removeTapCallback(); } mIgnoreNextUpEvent = false; break; case MotionEvent.ACTION_DOWN: if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) { mPrivateFlags3 |= PFLAG3_FINGER_DOWN; } mHasPerformedLongPress = false; if (!clickable) { checkForLongClick(0, x, y); break; } if (performButtonActionOnTouchDown(event)) { break; } // Walk up the hierarchy to determine if we're inside a scrolling container. boolean isInScrollingContainer = isInScrollingContainer(); // For views inside a scrolling container, delay the pressed feedback for // a short period in case this is a scroll. if (isInScrollingContainer) { mPrivateFlags |= PFLAG_PREPRESSED; if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } mPendingCheckForTap.x = event.getX(); mPendingCheckForTap.y = event.getY(); postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { // Not inside a scrolling container, so show the feedback right away setPressed(true, x, y); checkForLongClick(0, x, y); } break; case MotionEvent.ACTION_CANCEL: if (clickable) { setPressed(false); } removeTapCallback(); removeLongPressCallback(); mInContextButtonPress = false; mHasPerformedLongPress = false; mIgnoreNextUpEvent = false; mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; break; case MotionEvent.ACTION_MOVE: if (clickable) { drawableHotspotChanged(x, y); } // Be lenient about moving outside of buttons if (!pointInView(x, y, mTouchSlop)) { // Outside button // Remove any future long press/tap checks removeTapCallback(); removeLongPressCallback(); if ((mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; } break; } return true; } /---------------代碼塊-3------完------------------------------------------------------------- return false; } 複製代碼
onTouchEvent
方法裏的代碼也很多,不過大部分都是響應事件的一些邏輯,與事件分發流程關係不大。仍是分紅三塊,先看第一個代碼塊:
final float x = event.getX(); final float y = event.getY(); final int viewFlags = mViewFlags; final int action = event.getAction(); //這裏CLICKABLE、CONTEXT_CLICKABLE和CONTEXT_CLICKABLE有一個知足,clickable就爲true final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; //這裏先判斷當前View是否可用,若是是不可用進入if代碼塊 if ((viewFlags & ENABLED_MASK) == DISABLED) { //若是是UP事件而且View處於PRESSED狀態,則調用setPressed設置爲false if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; // A disabled view that is clickable still consumes the touch // events, it just doesn't respond to them. //這裏若是View是不可用狀態,就直接返回clickable狀態,不作任何處理 return clickable; } 複製代碼
代碼塊1中首先得到View是否可點擊clickable
,而後判斷View若是是不可用狀態就直接返回clickable
,可是沒作任何響應。View默認的clickable
爲false
,Enabled
爲ture
,不一樣的View的clickable
默認值也不一樣,Button
默認clickable
爲true
,TextView
默認爲false
。
if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } 複製代碼
代碼塊2中會對一個mTouchDelegate
觸摸代理進行判斷,不爲空會調用代理的onTouchEvent
響應事件而且返回true
。
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { switch (action) { case MotionEvent.ACTION_UP: mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; if ((viewFlags & TOOLTIP) == TOOLTIP) { handleTooltipUp(); } if (!clickable) { removeTapCallback(); removeLongPressCallback(); mInContextButtonPress = false; mHasPerformedLongPress = false; mIgnoreNextUpEvent = false; break; } boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { // take focus if we don't have it already and we should in // touch mode. boolean focusTaken = false; if (isFocusable() && isFocusableInTouchMode() && !isFocused()) { focusTaken = requestFocus(); } if (prepressed) { // The button is being released before we actually // showed it as pressed. Make it show the pressed // state now (before scheduling the click) to ensure // the user sees it. setPressed(true, x, y); } if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { // This is a tap, so remove the longpress check removeLongPressCallback(); // Only perform take click actions if we were in the pressed state if (!focusTaken) { // Use a Runnable and post this rather than calling // performClick directly. This lets other visual state // of the view update before click actions start. if (mPerformClick == null) { mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { //調用了OnClickListener performClick(); } } } if (mUnsetPressedState == null) { mUnsetPressedState = new UnsetPressedState(); } if (prepressed) { postDelayed(mUnsetPressedState, ViewConfiguration.getPressedStateDuration()); } else if (!post(mUnsetPressedState)) { // If the post failed, unpress right now mUnsetPressedState.run(); } removeTapCallback(); } mIgnoreNextUpEvent = false; break; case MotionEvent.ACTION_DOWN: if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) { mPrivateFlags3 |= PFLAG3_FINGER_DOWN; } mHasPerformedLongPress = false; if (!clickable) { checkForLongClick(0, x, y); break; } if (performButtonActionOnTouchDown(event)) { break; } // Walk up the hierarchy to determine if we're inside a scrolling container. boolean isInScrollingContainer = isInScrollingContainer(); // For views inside a scrolling container, delay the pressed feedback for // a short period in case this is a scroll. if (isInScrollingContainer) { mPrivateFlags |= PFLAG_PREPRESSED; if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } mPendingCheckForTap.x = event.getX(); mPendingCheckForTap.y = event.getY(); postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { // Not inside a scrolling container, so show the feedback right away setPressed(true, x, y); checkForLongClick(0, x, y); } break; case MotionEvent.ACTION_CANCEL: if (clickable) { setPressed(false); } removeTapCallback(); removeLongPressCallback(); mInContextButtonPress = false; mHasPerformedLongPress = false; mIgnoreNextUpEvent = false; mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; break; case MotionEvent.ACTION_MOVE: if (clickable) { drawableHotspotChanged(x, y); } // Be lenient about moving outside of buttons if (!pointInView(x, y, mTouchSlop)) { // Outside button // Remove any future long press/tap checks removeTapCallback(); removeLongPressCallback(); if ((mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; } break; } return true; } 複製代碼
代碼塊3中首先判斷了 clickable || (viewFlags & TOOLTIP) == TOOLTIP
知足了這個條件就返回true
消費事件。接下來的switch
中主要對事件四種狀態分別作了處理。這裏稍微看下在UP
事件中會調用一個performClick
方法,方法中調用了OnClickListener
的onClick
方法。
public boolean performClick() { final boolean result; final ListenerInfo li = mListenerInfo; if (li != null && li.mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); li.mOnClickListener.onClick(this); result = true; } else { result = false; } sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); notifyEnterOrExitForAutoFillIfNeeded(true); return result; } 複製代碼
最後看到onTouchEvent
的最後一行默認返回的仍是false
,就是說只有知足上述的條件之一纔會返回ture
。
至此事件分發的相關源碼就梳理完了,我畫了幾張流程圖,能更清晰的理解源碼邏輯。
ViewGroup的dispatchTouchEvent邏輯:
閱讀了源碼以後,先來解決以前提到的三個問題。
ACTION_DOWN
事件有完整的從Activity到ViewGroup再到View的分發攔截和響應的運行日誌,爲何ACTION_MOVE
和ACTION_UP
事件沒有?A1:日誌Demo代碼全部事件傳遞方法都是默認調用super
父類對應方法,因此根據源碼邏輯可知當事件序列中的第一個DOWN
事件來臨時,會按照Activity-->MyViewGroupA-->MyViewGroupB-->MyView
的順序分發,ViewGroup中onInterceptTouchEvent
方法默認返回false
不會攔截事件,最終會找到合適的子View(這裏即MyView)dispatchTransformedTouchEvent
方法,將事件交給子View的dispatchTouchEvent
處理,在dispatchTouchEvent
方法中默認會調用View的onTouchEvent
方法處理事件,這裏由於MyView是繼承View的,因此默認clickable
爲false
,而onTouchEvent
方法中當clickable
爲false
時默認返回的也是false
。最終致使ViewGroup中dispatchTransformedTouchEvent
方法返回爲false
。進而致使mFirstTouchTarget
爲空,因此後續MOVE
、UP
事件到來時,由於mFirstTouchTarget
爲空,事件攔截標記直接設置爲true
事件被攔截,就不會繼續向下分發,最終事件無人消費就返回到Activity的onTouchEvent
方法。因此就會出現這樣的日誌輸出。
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action); } else { intercepted = false; } } else { //mFirstTouchTarget爲空intercepted爲true且不會調用onInterceptTouchEvent方法 intercepted = true; } 複製代碼
clickable="true"
以後ACTION_MOVE
和ACTION_UP
事件就會執行了?A2:如A1中所說,clickable
設置爲true
,View的onTouchEvent
方法的返回就會爲true
,消費了DOWN
事件,就會建立一個TouchTarget
插到單鏈表頭,mFirstTouchTarget
就不會是空了,MOVE
、UP
事件到來時,就會由以前消費了DOWN
事件的View來處理消費MOVE
、UP
事件。
requestDisallowInterceptTouchEvent
方法是怎樣通知父View不攔截事件,爲何連onInterceptTouchEvent
方法也不執行了?A3:源碼閱讀是有看到,requestDisallowInterceptTouchEvent
方法時經過位運算設置標誌位,在調用傳入參數爲true
後,事件在分發時disallowIntercept
會爲true
,!disallowIntercept
即爲false
,致使事件攔截標記intercepted
爲false
,不會進行事件攔截。
View.OnClickListener
的onClick
方法與View.OnTouchListener
的onTouch
執行順序?A4::View.OnClickListener
的onClick
方法是在View的onTouchEvent
中performClick
方法中調用的。 而View.OnTouchListener
的onTouch
方法在View的dispatchTouchEvent
方法中看到是比onTouchEvent
方法優先級高的,而且只要OnTouchListener.Touch
返回爲true
,就只會調用OnTouchListener.onTouch
方法不會再調用onTouchEvent
方法。因此View.OnClickListener
的onClick
方法順序是在View.OnTouchListener
的onTouch
以後的。
關於滑動衝突,在《Android開發藝術探索》中有詳細說明,我這裏把書上的方法結論與具體實例結合起來作一個總結。
不一樣的場景有不一樣的處理規則,例如上面的場景一,規則通常就是當左右滑動時,外部View攔截事件,當上下滑動時要讓內部View攔截事件,這時候處理滑動衝突就能夠根據滑動是水平滑動仍是垂直滑動來判斷誰來攔截事件。場景而這種同個方向上的滑動衝突通常要根據業務邏輯來處理規則,何時要外部View攔截,何時要內部View攔截。場景三就更加複雜了,可是一樣是根據具體業務邏輯,來判斷具體的滑動規則。
外部攔截法是從父View着手,全部事件都要通過父View的分發和攔截,何時父View須要事件,就將其攔截,不須要就不攔截,經過重寫父View的onInterceptTouchEvent
方法來實現攔截規則。
private int mLastXIntercept; private int mLastYIntercept; public boolean onInterceptTouchEvent(MotionEvent event) { boolean intercepted = false; int x = (int)event.getX(); int y = (int)event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: { intercepted = false; break; } case MotionEvent.ACTION_MOVE: { if (知足父容器的攔截要求) { intercepted = true; } else { intercepted = false; } break; } case MotionEvent.ACTION_UP: { intercepted = false; break; } default: break; } mLastXIntercept = x; mLastYIntercept = y; return intercepted; } 複製代碼
按照以上僞代碼,根據不一樣的攔截要求進行修改就能夠解決滑動衝突。
內部攔截法的思想是父View不攔截事件,由子View來決定事件攔截,若是子View須要此事件就直接消耗掉,若是不須要就交給父View處理。這種方法須要配合requestDisallowInterceptTouchEvent
方法來實現。
private int mLastX; private int mLastY; @Override public boolean dispatchTouchEvent(MotionEvent event) { int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: { parent.requestDisallowInterceptTouchEvent(true); break; } case MotionEvent.ACTION_MOVE: { int deltaX = x - mLastX; int deltaY = y - mLastY; if (父容器須要此類點擊事件) { parent.requestDisallowInterceptTouchEvent(false); } break; } case MotionEvent.ACTION_UP: { break; } default: break; } mLastX = x; mLastY = y; return super.dispatchTouchEvent(event); } //父View的onInterceptTouchEvent方法 @Override public boolean onInterceptTouchEvent(MotionEvent event) { int action = event.getAction(); if (action == MotionEvent.ACTION_DOWN) { return false; } else { return true; } } 複製代碼
這裏父View不攔截ACTION_DOWN
方法的緣由,根據以前的源碼閱讀可知若是ACTION_DOWN
事件被攔截,以後的全部事件就都不會再傳遞下去了。
實例一:ScrollView與ListView嵌套
這個實例是同向滑動衝突,先看佈局文件:
<?xml version="1.0" encoding="utf-8"?> <cScrollView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/scrollView1" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".ScrollDemo1Activity"> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical"> <com.example.sy.eventdemo.MyView android:layout_width="match_parent" android:layout_height="350dp" android:background="#27A3F3" android:clickable="true" /> <ListView android:id="@+id/lv" android:layout_width="match_parent" android:background="#E5F327" android:layout_height="300dp"></ListView> <com.example.sy.eventdemo.MyView android:layout_width="match_parent" android:layout_height="500dp" android:background="#0AEC2E" android:clickable="true" /> </LinearLayout> </cScrollView> 複製代碼
這裏MyView就是以前打印日誌的View沒有作任何其餘處理,用於佔位使ScrollView超出一屏能夠滑動。
運行效果:
首先自定義ScrollView,重寫他的onInterceptTouchEvent
方法,攔擊除了DOWN
事件之外的事件。
public class MyScrollView extends ScrollView { public MyScrollView(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onTouchEvent(ev); return false; } return true; } } 複製代碼
這裏沒有攔截DOWN
事件,因此DOWN
事件沒法進入ScrollView的onTouchEvent
事件,又由於ScrollView的滾動須要在onTouchEvent
方法中作一些準備,因此這裏手動調用一次。接着再自定義一個ListView,來決定事件攔截,重寫dispatchTouchEvent
方法。
package com.example.sy.eventdemo; import android.content.Context; import android.os.Build; import android.support.annotation.RequiresApi; import android.util.AttributeSet; import android.view.MotionEvent; import android.widget.ListView; /** * Create by SY on 2019/4/22 */ public class MyListView extends ListView { public MyListView(Context context) { super(context); } public MyListView(Context context, AttributeSet attrs) { super(context, attrs); } public MyListView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } private float lastY; @RequiresApi(api = Build.VERSION_CODES.KITKAT) @Override public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { getParent().getParent().requestDisallowInterceptTouchEvent(true); } else if (ev.getAction() == MotionEvent.ACTION_MOVE) { if (lastY > ev.getY()) { // 這裏判斷是向上滑動,並且不能再向上滑了,說明到頭了,就讓父View處理 if (!canScrollList(1)) { getParent().getParent().requestDisallowInterceptTouchEvent(false); return false; } } else if (ev.getY() > lastY) { // 這裏判斷是向下滑動,並且不能再向下滑了,說明到頭了,一樣讓父View處理 if (!canScrollList(-1)) { getParent().getParent().requestDisallowInterceptTouchEvent(false); return false; } } } lastY = ev.getY(); return super.dispatchTouchEvent(ev); } } 複製代碼
判斷是向上滑動仍是向下滑動,是否滑動到頭了,若是滑到頭了就讓父View攔截事件由父View處理,不然就由本身處理。將佈局文件中的空間更換。
<?xml version="1.0" encoding="utf-8"?> <com.example.sy.eventdemo.MyScrollView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/scrollView1" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".ScrollDemo1Activity"> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical"> <com.example.sy.eventdemo.MyView android:layout_width="match_parent" android:layout_height="350dp" android:background="#27A3F3" android:clickable="true" /> <com.example.sy.eventdemo.MyListView android:id="@+id/lv" android:layout_width="match_parent" android:background="#E5F327" android:layout_height="300dp"></com.example.sy.eventdemo.MyListView> <com.example.sy.eventdemo.MyView android:layout_width="match_parent" android:layout_height="500dp" android:background="#0AEC2E" android:clickable="true" /> </LinearLayout> </com.example.sy.eventdemo.MyScrollView> 複製代碼
運行結果:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".ScrollDemo2Activity"> <com.example.sy.eventdemo.MyViewPager android:id="@+id/viewPager" android:layout_width="match_parent" android:layout_height="match_parent"></com.example.sy.eventdemo.MyViewPager> </LinearLayout> 複製代碼
ViewPager的每一個頁面的佈局也很簡單就是一個ListView:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".ScrollDemo2Activity"> <com.example.sy.eventdemo.MyViewPager android:id="@+id/viewPager" android:layout_width="match_parent" android:layout_height="match_parent"></com.example.sy.eventdemo.MyViewPager> </LinearLayout> 複製代碼
開始沒有處理滑動衝突的運行效果是這樣的:
case MotionEvent.ACTION_MOVE: int gapX = x - lastX; int gapY = y - lastY; //當水平滑動距離大於垂直滑動距離,讓父view攔截事件 if (Math.abs(gapX) > Math.abs(gapY)) { intercept = true; } else { //不然不攔截事件 intercept = false; } break; 複製代碼
onInterceptTouchEvent
中當水平滑動距離大於垂直滑動距離,讓父view攔截事件,反之父View不攔截事件,讓子View處理。
運行結果: