探索View的事件分發機制
可能你遇到過這樣的情形,從github上down下來一個開源專案的demo跑到好好的,可是一用到自己專案中就出現各種問題,例如滑動衝突問題,可是不知道從何下手解決?瞭解View的分發機制也許可以幫助你。在自定義有互動View中,事件分發處於一個比較重的地位,也是面試的常客。
在開始之前呢先囉嗦一點題外話,我們在平時學習工作中經常會遇到一些問題,特別是作為開發人員,通常的做法是google、baidu一些資料,看看別人有沒有遇到過類似的問題,借鑑他們的處理方案。這的確是一個有效的方法,可是我們都堅信一點,那就是不管在生活上還是在工作上,總有的時候沒有人可以給你參考,你需要獨立思考並作出選擇,所以養成獨立思考的習慣也很重要。
通常來說要去探究一件事,總要有一些線索才行,你比如說警察破案,他要勘察案發現場,蒐集一些證據,然後沿著線索一步步偵破案件。那我們現在要分析View的事件分發機制,如何去找這個所謂的線索,事實上程式的“作案”過程會被完整地記錄了,那就是方法棧。你是不是聯想到了平時程式異常時打出來的異常棧,就是它,現在就案情重演一次,看看它的“作案”過程:
btn.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
//throw new RuntimeException("Touch");
Thread.dumpStack();
return false;
}
});
這裡選擇給一個Button設定OnTouchListener,然後在onTouch方法中通過Thread.dumpStack()列印出方法的呼叫棧,當然你也可以丟擲一個異常,或者進入debug模式來檢視。點一下這個Button列印出了下面這一段內容:
這一段內容記雖然比較長,但是不復雜。它展示了點選事件在整個Android系統中的傳遞過程,從ZygoteInit.main方法到OnTouchListener.onTouch方法。從嚴格意義上來講,這不算是完整的過程,為啥?通常點選事件是從點選螢幕開始,當點選手機螢幕後,驅動程式是如何處理並把這個事件交給Android系統這個過程是沒有體現的。
這次我並不打算分析完整個過程,因為其實在應用層,事件的輸入通常是在應用的介面上,而我們編寫介面基本是開始於Activity,所以從Activity開始分析往下分析,就是下面這一段:
是不是看起來內容少了很多,心理負擔一下子就輕了不少。可以看到事件的傳遞過程是Activity->PhoneWindow->DecorView->ViewGroup->View。下面就沿著這條線索摸索摸索,首先是Activity的dispatchTouchEvent方法:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
//這個方法是空的實現,用法可以看看它的註釋
onUserInteraction();
}
//將事件傳遞到Window中
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
//如果子View都不消耗該事件,那Activity的onTouchEvent()方法就會被呼叫
return onTouchEvent(ev);
}
可以看到,Activity將事件傳遞給了Window,如果Window的superDispatchTouchEvent()方法返回true,那Activity的onTouchEvent就不會被呼叫了。
接著就到Window了,這個Window是個抽象類,我們要找的是它的實現類,通過前面的方法呼叫棧知道它是PhoneWindow,看看它的superDispatchTouchEvent方法:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
Window又將事件傳遞給了DecorView,可見這個Window充當了Activity和DecorView之間的連線紐帶。
//DecorView#superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
DecorView的superDispatchTouchEvent方法呼叫了父類的dispatchTouchEvent,而DecorView的直接父類是FrameLayout,而FrameLayout的dispatchTouchEvent方法是從ViewGroup繼承下來的,所以事件就傳遞到了ViewGroup中,這點從方法棧也可以看出來。
ViewGroup的dispatchTouchEvent方法程式碼是比較多的,也是View事件分發的核心,搞清楚它基本也就搞清楚了View的事件分發過程,下面分段來看看這個方法的實現:
// 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();
}
從這段程式碼實現可以看到在DOWN事件的時候會重置一些狀態資訊。
// 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 {
//從這個else條件可以看出,只要不是DOWN事件或者mFirstTouchTarget為null, intercepted直接賦值為true,也就是預設攔截。這個DOWN很好理解,但是這個mFirstTouchTarget是什麼現在還不知道
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
從這一段程式碼可以看到在DOWN事件的時候,會呼叫onInterceptTouchEvent()方法來詢問是否要攔截該事件,並通過intercepted來標記,後面的程式碼會根據這個標記來選擇不同的處理方式,首先看看intercepted為false,即事件會向下傳遞給子View:
// 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;
//遍歷所有的子View
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
//判斷子View是否可以接收點選事件,點選事件的x,y座標是否在子View內部
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
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);
}
這段程式碼也比較簡單,遍歷所有的子View,首先判斷子View是否能接收點選事件(View的可見性為VISIBLE或者view.getAnimation() != null),接著判斷事件的x,y座標是否在View的內部。如果能滿足這兩個條件,那事件就可以傳給該子View處理了。其中dispatchTransformedTouchEvent()方法實際上是呼叫了子View的dispatchTouchEvent()方法,如下:
View#dispatchTouchEvent
// 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());
}
//這裡傳入的child不是null,所以走這個分支
handled = child.dispatchTouchEvent(transformedEvent);
}
這裡呼叫dispatchTransformedTouchEvent()傳入的child不是null,所以走的是else分支。注意,如果子View的dispatchTouchEvent()方法返回true, 那麼 mFirstTouchTarget 就會被賦值並跳出遍歷子View的迴圈,如下:
//addTouchTarget內部會對 mFirstTouchTarget進行賦值操作
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
addTouchTarget()內部會為mFirstTouchTarget賦值
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
還記的前面關於攔截是的條件嗎?
if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null)
這裡就解答了前面的疑問,ViewGroup不攔截DOWN事件,且子View的dispatchTouchEvent()方法返回true時mFirstTouchTarget被賦值。前面我們也提到DOWN事件時會重置一些狀態資訊,這個mFirstTouchTarget會被置為null。也就是說一旦子View的dispatchTouchEvent()方法返回true,那同一個事件序列中,mFirstTouchTarget != null這個條件就都成立。
如果遍歷完所有子View,mFirstTouchTarget為null(有兩種情況,子View的dispatchTouchEvent()方法都返回false;或者事件被攔截了,即intercepted為true),則會走下面的邏輯:
// 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);
}
注意這裡的dispatchTransformedTouchEvent()方法的child是null。
// Perform any necessary transformations and dispatch.
if (child == null) {
//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);
}
可以看到當child為null是呼叫的super.dispatchTouchEvent()方法,這個super就是View。看到這你可能會問,前面為什麼不直接往下分析View的dispatchTouchEvent()方法的實現?當然可以,只是這樣方法棧就會變深,而我們記憶是有限的,一味地深入會讓自己無法自拔,這點在閱讀原始碼的時候要注意。現在大局觀已經明確,再去分析它的實現就比較清晰了。
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
...
if (onFilterTouchEventForSecurity(event)) {
//點選ScrollBar
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
//li.mOnTouchListener就是通過setOnTouchListener設定的OnTouchListener
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//如果 mOnTouchListener.onTouch()返回true,onTouchEvent()就不會再被呼叫了
if (!result && onTouchEvent(event)) {
result = true;
}
}
...
return result;
}
從View的dispatchTouchEvent()的實現可以看到,如果有呼叫setOnTouchListener()設定了OnTouchListener的話,就會先呼叫OnTouchListener的onTouch()方法,若onTouch()返回false,再呼叫View的onTouchEvent()方法;若返回true,則直接返回了,這將導致View的onTouchEvent()不會被呼叫。另外在onTouchEvent()內部會呼叫OnClickListener.onClick()方法:
public boolean onTouchEvent(MotionEvent event) {
...
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
...
if (!post(mPerformClick)) {
//這個方法的實現會呼叫OnClickListener.onClick()方法
performClick();
}
...
}
}
}
在UP事件時呼叫performClick()內部會呼叫OnClickListener.onClick(),所以父View不能攔截UP事件,否則點選事件就不會被呼叫。
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
//onClick()被呼叫
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
從前面的分析可以看到我們平時常用的onClick()方法的優先順序是最低的,呼叫順序為:OnTouchListener.onTouch()->onTouchEvent()->OnClickListener.onClick(),而且onTouch()返回true會中斷後續的呼叫。
前面的分析過程中發現,主要的邏輯是下面三個方法:
public boolean dispatchTouchEvent(MotionEvent ev)
用來進行事件分發,當事件傳遞到當前面View,dispatchTouchEvent()方法會被呼叫,它的返回值受onTouchEvent()方法的返回值和子View的dispatchTouchEvent()返回值的影響,表示是否消耗事件。
public boolean onInterceptTouchEvent(MotionEvent ev)
這個方法的返回值表示是否攔截事件,如果返回true,同一個事件序列中不會再被呼叫,同一個事件序列指的是down->move...move->up。
public boolean onTouchEvent(MotionEvent event)
在dispatchTouchEvent()內部呼叫,用於處理事件,返回值表示是否消耗該事件。如果返回false,同一事件序列中,當前View(不包括ViewGroup)將無法再次接收到。
這三個方法的關係大致可以理解為下面的虛擬碼:
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume;
if (onInterceptTouchEvent(ev)) {
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
大致的意思是當事件傳遞給ViewGroup時,ViewGroup的dispatchTouchEvent()方法會被呼叫,接著呼叫它的onInterceptTouchEvent()方法詢問是否攔截該事件,如攔截則該事件交給這個ViewGroup處理,即它的onTouchEvent()方法被呼叫;如不攔截,則傳遞給子View,如此反覆直到事件被處理掉。下面給出一些前面分析得到的結論:
(1)當一個View決定攔截某個事件,即onInterceptTouchEvent()返回true,那麼同一個事件序列的所有事件都直接交給它處理而不會再呼叫onInterceptTouchEvent()來詢問是否攔截;換句話說就是一個事件序列只能被一個View攔截並消耗。
(2)某個View一旦開始處理事件,如果它不消耗調DOWN事件,即onTouchEvent()返回false,那同一個事件序列的其他事件就不會再交給它處理,事件將重新交由它的父View處理,即父View的onTouchEvent()會被呼叫。
(3)如果不消耗DOWN以外的事件,同一個事件序列還是會傳給當前View,沒有消耗的事件會交給Activity的onTouchEvent()來處理。
(4)View的onTouchEvent()預設都消耗事件,即返回true,除非它是不可點選的,即下面的clickable為false。
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
可以看到只要是CLICKABLE、LONG_CLICKABLE、CONTEXT_CLICKABLE這三個有一個為true它就是可點選的,而與ennable無關,換句話說就是如果View的enable為true也是會消耗事件的。
(5)事件總是先傳給父View,然後在傳給子View,在子View中可以通過呼叫父View的requestDisallowInterceptTouchEvent()來干預事件的攔截過程,在處理滑動衝突可以利用這一點。
@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);
}
}
在這個方法內部修改了mGroupFlags的值,現在再回頭看看ViewGroup攔截事件的邏輯:
ViewGroup#dispatchTouchEvent
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//這個值會受到干預,進而影響是intercepted的值,也就是干預攔截
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 {
intercepted = true;
}
可以看到如果disallowIntercept為true,intercepted直接置為false。另外DOWN事件是不能干預的,因為在DONW事件時呼叫會resetTouchState()來重置狀態資訊,mGroupFlags會被重置。
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
這些結論看似輕描淡寫,要真正理解就必須要仔細看看原始碼才行,如果你能為這些結論說出有力的論據,那說明你掌握的也差不多了。後面我會運用這些論據來自定義一個具體的ViewGroup,讓這些結論變得有用。
相關文章
- View事件分發機制View事件
- 《Android開發藝術探索》——View事件分發機制AndroidView事件
- View的事件分發機制分析View事件
- View事件分發機制分析View事件
- 《Android藝術開發探索》學習筆記之View的事件體系(View的事件分發機制)Android筆記View事件
- Android View 的事件體系 -- 事件分發機制AndroidView事件
- 【Android原始碼】View的事件分發機制Android原始碼View事件
- 10分鐘理解 Android View 事件分發機制AndroidView事件
- Android事件分發:從原始碼角度分析View事件分發機制Android事件原始碼View
- Android 事件分發機制原始碼解析-view層Android事件原始碼View
- 2018.03.08、View的事件分發機制筆記View事件筆記
- ViewGroup/View的事件分發機制(1)(Touch,down,move,up)View事件
- ViewGroup/View的事件分發機制(2)(Touch,down,move,up)View事件
- ViewGroup/View的事件分發機制(3)(Touch,down,move,up)View事件
- View 事件傳遞體系知識梳理(1) 事件分發機制View事件
- Android自定義View之事件分發機制總結AndroidView事件
- 基於原始碼分析 Android View 事件分發機制原始碼AndroidView事件
- Android從原始碼角度剖析View事件分發機制Android原始碼View事件
- View事件機制分析View事件
- 學習總結 -- View 事件分發機制和滑動衝突View事件
- Android事件分發機制Android事件
- 這可能是最"俗"的View的事件分發機制(含Demo講解)View事件
- Android的MotionEvent事件分發機制Android事件
- Android 事件分發機制的理解Android事件
- android View的事件分發AndroidView事件
- Android事件分發機制三:事件分發工作流程Android事件
- 事件分發機制(一):解惑篇事件
- cocos EventDispatcher事件分發機制事件
- TouchEvent事件分發機制全解析事件
- Android事件分發機制探究Android事件
- Android事件分發機制解析Android事件
- 事件分發之View事件處理事件View
- 通俗理解Android中View的事件分發機制及滑動衝突處理AndroidView事件
- Android 控制元件框架、View的分發機制和自定義ViewAndroid控制元件框架View
- android事件分發機制詳解Android事件
- 事件分發機制(二):原始碼篇事件原始碼
- iOS事件分發機制與實踐iOS事件
- 完全理解android事件分發機制Android事件