在Android
開發中,事件分發已經成爲開發者的必備知識,那麼若是面試官讓你講一下對事件分發的理解,你將會怎麼去講呢?面試
咱們都在說的事件分發的本質是什麼呢?bash
咱們都知道事件的傳遞和分發都是在手指觸發屏幕的一瞬間開始,那麼在觸發後,系統會作出什麼反應呢?app
接下來,讓咱們一一揭開這層神祕的面紗。ide
全部的事件發生的起源就在於Touch
事件,咱們的在屏幕上的操做動做,能夠分爲四種:post
動做 | 描述 |
---|---|
MotionEvent.ACTION_DOWN | 在頁面上按下某個位置 |
MotionEvent.ACTION_MOVE | 手指在頁面上進行移動(滑動) |
MotionEvent.ACTION_UP | 手指擡起 |
MotionEvent.ACTION_CANCEL | 結束事件(在ACTION_UP後會執行) |
從用戶角度來講,咱們手指接觸的地方是一個頁面,也就是咱們所說的UI
,UI從上到下依次是Activity
、ViewGroup
、View
。動畫
有可能一個ViewGroup裏還有一個或者多個ViewGroup,這裏咱們只哪最簡單的頁面構造來分析。ui
那麼顯然得知,事件的傳遞機制就是從上到下的,依次就是 Activity
-> ViewGroup
-> View
this
事件的分發涉及到三個方法:spa
dispathTouchEvent
代理
onInterceptTouchEvent
onTouchEvent
它們三者的調用關係能夠用一個經典的例子來講明:
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if(onInterceptTouchEvent(ev)) {
consume = onTouchEvent(ev)
} else {
consume = child.dispatchTouchEvent(ev)
}
return consume;
}
複製代碼
事件的傳遞首先從Activity
開始,直到有一個控件處理了這個事件。咱們來看下源碼中作了些什麼。
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
複製代碼
一、onUserInteraction
是一個空方法。
二、由上邊代碼能夠看出,Activity會發送給依附的Window來處理,若是Window中沒有處理這個事件,就會交給Activity的onTouchEvent去處理。
那麼這個getWindow是什麼呢?
Window是一個抽象類,它的實現類是PhoneWindow
。
PhoneWindow
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
複製代碼
DecorView
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
複製代碼
DecorView是Activity的頂層View,繼承FrameLayout
,也就是說Activity中的點擊事件會傳遞到ViewGroup中,咱們來看下ViewGroup中的分發機制,這也是本篇中比較難的點。
dispatchTouchEvent
// 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;
}
複製代碼
這是dispatchTouchEvent中的一段邏輯,代碼比較多咱們一點點分析。
由上面代碼咱們能夠看到ViewGroup
的攔截首先要知足兩個條件:actionMasked == MotionEvent.ACTION_DOWN
以及mFirstTouchTarget != null
,第一個很好理解,第二個是什麼呢?這裏咱們留一個疑問,接着往下看。
disallowIntercope
中的關鍵在FLAG_DISALLOW_INTERCEPT
標誌位,若是這個標誌位被設置了,那麼將不會被攔截。看下以前的一段代碼:
// 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
的話,就會重置狀態,包括FLAG_DISALLOW_INTERCEPT
,因此設置了FLAG_DISALLOW_INTERCEPT
將不會攔截除了ACTION_DOWN
以外的事件。
接下來咱們看下不攔截以後的處理邏輯:
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;
複製代碼
這段比較長,咱們來找一些重要的邏輯看下,首先遍歷全部的子View,判斷它們是否是處理了這個事件。
判斷的依據有三個:
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
複製代碼
一、當前child是否可得到焦點。
二、當前child是否正在執行動畫。
三、當前事件的座標是否在child的區域內。
若是這些都知足,就會調用dispatchTransformedTouchEvent
方法來將事件分發給child去處理。
dispatchTransformedTouchEvent
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
複製代碼
若是有child處理了事件,則addTouchTarget
,並跳出循環
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
複製代碼
看了addTouchTarget
方法是否是就能夠解答上面的疑問了?
mFirstTouchTarget
是一個單鏈表,指向處理了事件的child,它的賦值與否直接影響了ViewGroup的攔截邏輯。
接着往下看:
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
複製代碼
若是全部的child都沒有處理事件,那麼就會走super.dispatchTouchEvent(event)
.
ViewGroup的super固然就是View了,看看,是否是最終會走到View的處理邏輯裏。
View的處理邏輯就相對來講,簡單多了,咱們來看下:
public boolean dispatchTouchEvent(MotionEvent event) {
....
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
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;
}
複製代碼
看上面的代碼,咱們發現它會先判斷OnTouchListener
,若是設置了OnTouchListener
,就會走裏面的onTouch
方法,不然纔會走onTouchEvent
咱們來看下onTouchEvent
方法:
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; } if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } 複製代碼
首先會判斷是否是不可用狀態,若是是不可用狀態也是會消費事件的,其次的代理的判斷。
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)) { performClickInternal(); } } } ... 複製代碼
只要是點擊狀態是CLICKABLE
或者LONG_CLICKABLE
都會消費事件,最終會走到performClickInternal
, performClickInternal
中實際上是調用的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);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
複製代碼
在這個方法中會調用咱們熟知的OnClickListener
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
複製代碼
咱們每一個View的默認LONG_CLICKABLE
都是false的,CLICKABLE
大都也是false的,看上面代碼可知,在 setOnClickListener
會將CLICKABLE
設置爲true。
到這裏,事件分發的機制就瞭解完了,你是否是對剛開始提出的問題有了答案了呢?