Android事件分發機制的本質
将點選事件向某個View進行傳遞并且最終得到處理,即當一個點選事件發生後,系統需要将這個事件傳遞給一個具體的View處理,這個事件的傳遞過程就是事件分發過程
事件在那些對象傳遞
Activity、ViewGroup、View分發流程:Activity(Window)-> ViewGroup -> Viewsuper:調用父類方法true:處理事件,事件不在繼續往下傳遞false:不處理事件,事件也不繼續傳遞,交給父控件的onTouchEvent()處理傳遞:
Activity -> ViewGroup -> View 從上往下調用dispatchTouchEvent()
View -> ViewGroup -> Activity 從下往上調用onTouchEvent()
1. Activity的事件分發
當一個點選事件發生時,事件最先到達Activity的dispatchTouchEvent()進行事件分發
public boolean dispatchTouchEvent(MotionEvent ev) {
//一個事件的開始總是從DOWN開始
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
//預設空方法,每當按鍵、觸摸、trackBall事件分發到目前的Activity就會被調用,
//如果想在Activity運作的時候能夠感覺使用者正在與裝置互動,重寫此方法
onUserInteraction();
}
//getWindow()=Window抽象類,唯一的實作類PhoneWindow
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
PhoneWindow
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
//mDecor=DecorView,是視圖頂層View,繼承FrameLayout,所有界面的父類
return mDecor.superDispatchTouchEvent(event);
}
DecorView
public boolean superDispatchTouchEvent(MotionEvent event) {
//由于DecorView繼承自FrameLayout,FrameLayout繼承自ViewGroup
//super.dispatchTouchEvent為ViewGroup 的dispatchTouchEvent
return super.dispatchTouchEvent(event);
}
即getWindow().superDispatchTouchEvent(ev)就是執行了ViewGroup.dispatchTouchEvent(event)
說明事件就是從Activity傳遞到ViewGroup中
總結:
1. 事件最先傳遞到Activity的dispatchTouchEvent()進行事件分發
2. 調用Window唯一實作類PhoneWindow的 superDispatchTouchEvent()
3. 調用DecorView的superDispatchTouchEvent()
4. 最終調用DecorView父類FrameLayout的dispatchTouchEvent()即ViewGroup的dispatchTouchEvent()
2.View事件分發
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
...
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
ListenerInfo li = mListenerInfo;
//分析(1)
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
...
//如果分析(1)中onTouch()傳回false(onTouch()未消費事件),
return result;
}
分析(1):
第一個條件:li != null,經過跟蹤發現在View中發現了 getListenerInfo() 那麼這個方法又是什麼時候調用的呢?
ListenerInfo getListenerInfo() {
if (mListenerInfo != null) {
return mListenerInfo;
}
mListenerInfo = new ListenerInfo();
return mListenerInfo;
}
繼續跟蹤發現,getListenerInfo()方法在多個地方使用到,比如setOnClickListener(),setOnLongClickListener(),setOnTouchListener()等等,這裡也正好找到了li.mOnTouchListener != null指派地方setOnTouchListener()
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
if (!isLongClickable()) {
setLongClickable(true);
}
getListenerInfo().mOnLongClickListener = l;
}
public void setOnTouchListener(OnTouchListener l) {
getListenerInfo().mOnTouchListener = l;
}
第三個條件:(mViewFlags & ENABLED_MASK) == ENABLED,即判斷目前控件是否是enable,一般控件都是預設enable
第四個條件:li.mOnTouchListener.onTouch(this, event),其實這個方法就是回調控件注冊的touch事件的onTouch()方法,也就是如果我們在onTouch()裡傳回true,這四個條件也就滿足了,進而讓整個方法直接傳回了true,如果我們在onTouch()方法傳回false,就會執行onTouchEvent()方法。
根據上面的研究,我們可以得出結論:onTouch()方法優先于onClick()執行,如果onTouch()方法傳回true(事件被消費)因而事件不會繼續向下傳遞,onClick()方法也将不在執行
我們發現如果條件不通過,則會執行onTouchEvent()方法,可想而知onClick()方法也必然在onTouchEvent()方法中,接着看onTouchEvent源碼:
public boolean onTouchEvent(MotionEvent event) {
...
final int action = event.getAction();
//隻要控件是CLICKABLE(點選)、LONG_CLICKABLE(長按)有一個為true,onTouchEvent就一定消費事件
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
...
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)) {
//分析(1)
performClickInternal();
}
}
}
...
}
return true;
}
return false;
}
分析(1):跟蹤performClickInternal()方法
private boolean performClickInternal() {
// Must notify autofill manager before performing the click actions to avoid scenarios where
// the app has a click listener that changes the state of views the autofill service might
// be interested on.
notifyAutofillManagerOnClick();
return performClick();
}
public boolean performClick() {
// We still need to call this method to handle the cases where performClick() was called
// externally, instead of through performClickInternal()
notifyAutofillManagerOnClick();
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
//如果注冊了OnClickListener事件就會回調onClick()方法
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
view的事件分發源碼解析到此結束,上總結:
1. onTouch()優先于onClick()先執行
2. 控件被點選時:
(1) 如果onTouch傳回false,執行onTouchEvent()方法,進而執行onClick()方法
(2) 如果onTouch傳回true,則事件被onTouch()消費,onClick()就不會執行了。
3.ViewGroup事件分發
測試用例:(1)隻點選Button (2)點選ViewGroupB
測試結果:當點選Button的時候,執行Button 的Onclick()方法,但是ViewGroupB 的onTouch()不會執行,隻有點選了ViewGroupB時才會觸發其onTouchEvent()
測試結論:Button的onClick()将事件消費了,是以事件不會繼續向下傳遞
測試用例:(1)重寫Button的onTouch事件傳回true,點選Button
測試結果:Button的onTouch消費了事件,onClick()收不到事件
測試用例:(1)單獨點選ViewGroupB (2)單獨ViewGroupA (3)重寫B的onTouch事件傳回true
測試結果:(1)當單獨點選ViewGroupB的時候A/B 的onTouch()都會被觸發;(2)單獨單擊ViewGroupA時觸發了A的onTouch()
(3)單獨點選ViewGroupB時A的onTouch()沒有被觸發了,因為已經被 B消費了
源碼分析:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Handle an initial down.初始化
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
//恢複mFirstTouchTarget==null
resetTouchState();
}
// Check for interception.
final boolean intercepted;
//按下并且首次
//mFirstTouchTarget:判斷目前的ViewGroup是否攔截了事件,如果攔截了mFirstTouchTarget=null,如果沒有攔截
//并交由子view處理,則mFirstTouchTarget!=null
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {//分析(1)
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {//分析(2)
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;
}
}
...
return handled;
}
分析(1):首先必須是ACTION_DOWN狀态,一次完整的事件序列應該是從DOWN開始UP結束,mFirstTouchTarget的意義是:目前ViewGroup是否攔截了事件,如果攔截了,mFirstTouchTarget=null,如果沒有攔截交由子View來處理,則mFirstTouchTarget!=null。假設目前的ViewGroup攔截了事件(disallowIntercept=false),mFirstTouchTarget!=null為false,如果這時觸發ACTION_DOWN事件,則會執行onInterceptTouchEvent()方法;如果觸發的是ACTION_MOVE、ACTION_UP事件,則不再執行onInterceptTouchEvent()方法,而是直接設定了intercepted=true,此後的一個事件序列均由這個ViewGroup處理。
分析(2):FLAG_DISALLOW_INTERCEPT标志主要是禁止ViewGroup攔截除了DOWN之外的事件,一般通過子View的requestDisallowInterceptTouchEvent來設定。
總結:當ViewGroup要攔截事件的時候,那麼後續的事件都要由他處理,而不再調用onInterceptTouchEvent()方法,其中onInterceptTouchEvent預設false,如果想要ViewGroup攔截則重寫方法傳回true。
繼續往下面分析:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
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;
//倒序周遊子元素是否能夠接收點選事件(從最上層View開始往内層周遊)
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
//判斷觸摸點是否在子view的範圍或者子view是否正在播放動畫,如果不符合要求則continue
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)) {//分析(1)
// 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();
}}}
...
}
分析(1):dispatchTransformedTouchEvent()事件分發實際的處理方法
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
//如果沒有子view
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
...
return handled;
}
如果存在子View,則調用View的dispatchTouchEvent()方法,如果ViewGroup沒有子View,則調用super.dispatchTouchEvent()方法即View的dispatchTouchEvent()方法就回到了View的分發上面了。
4. 點選事件分發的傳遞規則
從上面分析得出,僞代碼
fun dispatchTouchEvent(ev: MotionEvent): Boolean {
var result = false
if (onInterceptTouchEvent(ev)) {
result = super.onTouchEvent(ev)
} else {
result = child.dispatchTouchEvent(ev)
}
return result
}
onInterceptTouchEvent()方法和onTouchEvent()方法都是在dispatchTouchEvent()方法中調用的。
我們知道他們的傳遞規則是由上而下,如果到最底層的View一直還沒有消耗事件則又從下而上傳遞
如下圖:(網絡配圖侵權删)
舉個栗子:(摘抄自劉望舒的《Android進階之光》)
在金庸的《倚天屠龍記》中,武當派實力強勁,按照身份和實力區分,分别是武當掌門張
三豐、武當七俠、武當弟子。這時突然有一個敵人來犯,這個消息首先會彙報給武當掌門張三豐。張三豐
當然不會親自出馬,是以他就将應戰的任務交給武當七俠之一的宋遠橋(onInterceptTouchEvent()傳回
false);宋遠橋威名遠揚,也不會應戰,是以他就把應戰的任務交給武當弟子宋青書
(onInterceptTouchEvent()傳回 false);武當弟子宋青書沒有手下,他隻能自己應戰。在這裡我們把武當
掌門張三豐比作頂層 ViewGroup,将武當七俠之一的宋遠橋比作中層ViewGroup,武當弟子宋青書比作底層
View。那麼事件的傳遞流程就是:武當掌門張三豐(頂層ViewGroup)→武當七俠宋遠橋(中層
ViewGroup)→武當弟子宋青書(底層View)。是以得出結論,事件由上而下傳遞傳回值的規則如下:為
true,則攔截,不繼續向下傳遞;為false,則不攔截,繼續向下傳遞。
接下來講解點選事件由下而上的傳遞。當點選事件傳給底層的 View 時,如果其onTouchEvent()方法
傳回true,則事件由底層的View消耗并處理;如果傳回false則表示該View不做處理,則傳遞給父View的
onTouchEvent()處理;如果父View的onTouchEvent()仍舊傳回false,則繼續傳遞給該父View的父View
處理,如此反複下去。
再傳回上面武俠的例子。武當弟子宋青書發現來犯的敵人是混元霹靂手成昆,他打不過成昆
(onTouchEvent()傳回 false),于是就跑去找宋遠橋,宋遠橋來了,發現自己也打不過成昆
(onTouchEvent()傳回 false),就去找武當掌門張三豐,張三豐用太極拳很輕松地打敗了成昆
(onTouchEvent()傳回true)。是以得出結論,事件由下而上傳遞傳回值的規則如下:為true,則處理
了,不繼續向上傳遞;為false,則不處理,繼續向上傳遞。
5.總結
-
onToutch()和onTouchEvent()的差別
這兩個方法都是在View的dispatchTouchEvent()中調用的;但onTouch優先于onTouchEvent()執行,如果在onTouch()中傳回true,onTouchEvent() 将不再執行。
另外,onTouch能夠得到執行隻需兩個前提條件,第一mOnTouchListener的值不能為空,第二目前點選的控件必須是enable,因為如果是非enable,那麼注冊的onTouch事件永遠無法得到執行,對于這一類控件,重寫onTouchEvent方法來監聽touch事件
-
Touch事件的後續事件層級傳遞
當dispatchTouchEvent()在進行事件分發的時候,隻有前一個事件傳回true,才會收到後一個事件(比如:在執行ACTION_DOWN時傳回了false,後面一系列的ACTION_MOVE、ACTION_UP事件都不會執行了)
dispatchTouchEvent()和onTouchEvent()消費事件、終結事件傳遞傳回 true,那麼收到ACTION_DOWN,也會收到ACTION_MOVE、ACTION_UP
如果在某個對象(Activity、ViewGroup、View)的onTouchEvent()消費事件(傳回true),那麼ACTION_MOVE和ACTION_UP事件從上往下到這個View後就不在往下傳遞了,而是直接傳給自己的onTouchEvent()并結束本次事件傳遞的過程。
完結散花。。