3.4 View的事件分發(fā)機制(一)

1. 事件分發(fā)最重要的三個方法

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    return super.dispatchTouchEvent(ev);
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    return super.onInterceptTouchEvent(ev);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    return super.onTouchEvent(event);
}
  • dispatchTouchEvent(MotionEvent ev)
    用來進行事件分發(fā),如果事件能到達當前View,那么此方法一定會被調(diào)用,而且是先調(diào)用。返回值表示是否消耗當前事件。
  • onInterceptTouchEvent(MotionEvent ev)
    在dispatchTouchEvent方法內(nèi)部調(diào)用,用來判斷是否攔截某個事件,如果當前View攔截了某個事件,那么在同一個事件序列當中,此方法不會再被調(diào)用。返回值表示是否攔截當前事件。
  • onTouchEvent(MotionEvent event)
    在dispatchTouchEvent方法中調(diào)用,用來處理點擊事件,返回結(jié)果表示是否消耗當前事件。
    三者關系用偽代碼說明:
public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean consume = false;
    if (onInterceptTouchEvent(ev)) {
        consume = onTouchEvent(ev);
    } else {
        consume = child.dispatchTouchEvent(ev);
    }
    return consume;
}

2. 一些結(jié)論

  1. 同一個事件序列是指從手指接觸屏幕的那一刻開始,到手指離開屏幕的那一刻結(jié)束,在這個過程中所產(chǎn)生的一系列事件。這個事件以down事件開始,中間含有數(shù)量不定的move事件,最終以up事件結(jié)束。
  2. 正常情況下,一個事件序列只能被一個View攔截且消耗。因為一旦一個元素攔截了某事件(down事件),那么同一事件序列內(nèi)的所有事件都會交給它處理。但是可以通過其他特殊手段,比如一個View將本該自己處理的事件通過onTouchEvent強行傳遞給其他View處理。
  3. 某個View一旦決定攔截(onInterceptTouchEvent),那么一個事件序列都只能由它來處理,并且它的oninterceptTouchEvent不會再被調(diào)用。
  4. 某個View一旦開始處理(onTouchEvent)事件,如果不消耗ACTION_DOWN事件,那么同一事件序列的其他事件都不會再交給它來處理,并且事件將重新交由它的父元素處理,即父元素的onTouchEvent方法會被調(diào)用。
  5. 如果View不消耗ACTION_DOWN以外的其他事件,那么這個點擊事件會消失,此時父元素的onTouchEvent并不會被調(diào)用,并且當前View可以持續(xù)收到后續(xù)事件,最終這些消失的點擊事件會傳遞給Activity處理。
  6. ViewGroup默認不攔截任何事件,源碼中ViewGroup的onInterceptTouchEvent方法默認返回false。
  7. View沒有onInterceptTouchEvent方法,一旦有點擊事件傳遞給它,那么它的onTouchEvent方法就會被調(diào)用。
  8. View的onTouchEvent默認都會消耗事件(返回true),除非它是不可點擊的(clickable和longClickable同時為false)。View的longClickable屬性默認都為false,clickable屬性要分情況,比如button的clickable屬性默認為true,而TextView的clickable屬性默認為false。
  9. View的enable屬性不影響onTouchEvent的默認返回值。哪怕一個View是disable狀態(tài),只要它的clickable或longClickable有一個為true,那么它的onTouchEvent返回的就是true。
  10. onClick會發(fā)生的前提是當前View是可點擊的,并且它收到了down和up事件。
  11. 事件傳遞過程是由外向內(nèi)的,即事件總是先傳遞給父元素,然后再由父元素分發(fā)給子元素,子元素可以通過requestDisallowInterceptTouchEvent方法可以在子元素中干預父元素的事件分發(fā)過程,但是ACTION_DOWN事件除外。

3. Activity對點擊事件的分發(fā)過程

點擊事件用MotionEvent來表示,當一個點擊操作發(fā)生時,事件最先傳遞給當前Activity,又Activity的dispatchTouchEvent來進行分發(fā),具體工作由Activity內(nèi)部的Windwo來完成。Window會將事件傳遞給decor view,decor view一般就是當前界面的底層容器(即setContentView所設置的View的父容器),通過Activity.getWindow().getDecorView()獲得。

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

window是個抽象類,window的唯一實現(xiàn)是PhoneWindow,看PhoneWindow的分發(fā)事件方法

@Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}
DecorView extends FrameLayout implements RootViewSurfaceTaker{}

我們在activity中可以獲取DecorView

getWindow().getDecorView()

我們通過setContentView方法設置的Veiw是DecorView的子View?,F(xiàn)在事件已經(jīng)到ViewGroup了,繼續(xù)看ViewGroup的分發(fā)。

4. 頂級View對點擊事件的分發(fā)過程

事件到達頂級View后,肯定會進入dispatchTouchEvent方法中,該方法中首先判斷是否攔截,攔截則當前Veiw自己處理,處理方式要先看是否有onTouchListener,有則執(zhí)行onTouchListener并根據(jù)其返回值看是否執(zhí)行OnTouchEvent。不攔截則找到當前點擊位置的子View繼續(xù)分發(fā)。View中dispatchTouchEvent方法的源碼:

public boolean dispatchTouchEvent(MotionEvent event) {
    if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
            mOnTouchListener.onTouch(this, event)) {
        return true;
    }
    return onTouchEvent(event);
}

繼續(xù)看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;
}

ViewGroup在兩個條件下會判斷是否攔截當前事件,ACTION_DOWN事件時或者mFirstTouchTarget不為空時。mFirstTouchTarget會在事件由ViewGroup子元素成功處理時,被賦予子元素的值。
也就是事件被子元素處理了,mFirstTouchTarget有值,沒被子元素處理,也就是被當前ViewGroup攔截了,則mFirstTouchTarget就沒有值,就不滿足條件了。
假如down已經(jīng)被當前viewGroup攔截,當move和up事件到來時,mFirstTouchTarget是空,所以會直接執(zhí)行intercepted=true,也就是直接攔截move和up事件都交給當前View處理。否則intercepted的值是onInterceptTouchEvent方法的返回值。
判斷了上面兩個條件,下面還有一個判斷:

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;
}

這里有一個標志位FLAG_DISALLOW_INTERCEPT,在子view中可以通過下面方法設置:

 getParent().requestDisallowInterceptTouchEvent(true);

一旦設置后,ViewGroup將無法攔截除了ACTION_DOWN以外的其他點擊事件。
為什么除了ACTION_DOWN呢?因為在執(zhí)行上面的代碼前,還有一些代碼

// 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,會重置標記位。由此我們知道兩點:

  • 第一點,onInterceptTouchEvent不是每次事件都會被調(diào)用,如果我們想提前處理所有的點擊事件,要選擇dispatchTouchEvent方法,只有這個方法能確保每次都調(diào)用。
  • 另外一點,F(xiàn)LAG_DISALLOW_INTERCEPT標記位能幫我們解決滑動沖突。

攔截或者不攔截由intercepted決定,上面的條件判斷最后都會給intercepted賦值。然后看攔截和不攔截的代碼如下:

if (!canceled && !intercepted) {
    // 不攔截
}
if (mFirstTouchTarget == null) {
    // 攔截注意這里的dispatchTransformedTouchEvent方法.
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
}

然后繼續(xù)看ViewGroup在不攔截時的詳細代碼。

for (int i = childrenCount - 1; i >= 0; i--) {
    ...

    if (!canViewReceivePointerEvents(child)
            || !isTransformedTouchPointInView(x, y, child, null)) {
        ev.setTargetAccessibilityFocus(false);
        continue;
    }

    ...
    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
        ...
        newTouchTarget = addTouchTarget(child, idBitsToAssign);
        alreadyDispatchedToNewTouchTarget = true;
        break;
    }
    ...
}

遍歷所有元素,判斷子元素是否能夠接受到點擊事件。下面兩個方法就是判斷標準,第一個表示是否可見以及是否有動畫。第二個表示點擊事件是否落在子元素的區(qū)域內(nèi)。

private static boolean canViewReceivePointerEvents(View child) {
    return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
            || child.getAnimation() != null;
}
protected boolean isTransformedTouchPointInView(float x, float y, View child,
            PointF outLocalPoint) {
        final float[] point = getTempPoint();
        point[0] = x;
        point[1] = y;
        transformPointToViewLocal(point, child);
        final boolean isInView = child.pointInView(point[0], point[1]);
        if (isInView && outLocalPoint != null) {
            outLocalPoint.set(point[0], point[1]);
        }
        return isInView;
    }

如果有滿足的元素則執(zhí)行dispatchTransformedTouchEvent方法

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
    final boolean handled;

    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }
    ...
}

因為傳入的child不為null,所以調(diào)用子View的dispatchTouchEvent繼續(xù)分發(fā)。
如果子元素的分發(fā)返回了true,則上面的代碼繼續(xù)執(zhí)行addTouchTarget方法

private TouchTarget addTouchTarget(View child, int pointerIdBits) {
    TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;
}

發(fā)現(xiàn)如果子View消耗掉了點擊事件,則給mFirstTouchTarget賦值。它有了值,后續(xù)的move和up還會判斷是否要攔截。它沒有值,則直接由當前View處理。
如果遍歷所有的子View都沒有消耗事件。則調(diào)用

if (mFirstTouchTarget == null) {
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
}

注意第三個參數(shù)child為null,則依據(jù)上面的源碼知道會調(diào)用super的dispatchTouchEvent方法。super是View,下面看View的dispatchTouchEvent方法

5. View的事件處理

if (onFilterTouchEventForSecurity(event)) {
    //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;
    }
}

首先是onTouchListener的判斷,然后執(zhí)行的onTouchEvent方法,在onTouchEvent的ACTION_UP時,會判斷并調(diào)用click方法。

if (!focusTaken) {
    if (mPerformClick == null) {
        mPerformClick = new PerformClick();
    }
    if (!post(mPerformClick)) {
        performClick();
    }
}

這里注意ViewGroup和View的dispatchTouchEvent方法是不同的,ViewGroup中的分發(fā)有攔截判斷;View中的分發(fā)只有onTouchListener的判斷,接著就調(diào)用了onTouchEvent方法。而ViewGroup是沒有重寫onTouchEvent方法的,在事件攔截后,ViewGroup會調(diào)super的dispatchTouchEvent,也就是View的dispatchTouchEvent,在里面調(diào)onTouchEvent方法,當然我們可以自己重寫onTouchEvent方法。

讀完這一章,覺得作者自己很清楚,但寫出來還是覺得混亂,連個流程圖都沒有。全是文字堆積,讓人看的昏昏欲睡。這里推薦兩篇郭霖的文章,相比之下還是比較清楚的,如果兩個結(jié)合來學習大有益處。
Android事件分發(fā)機制完全解析,帶你從源碼的角度徹底理解(上)
Android事件分發(fā)機制完全解析,帶你從源碼的角度徹底理解(下)
附加一篇簡書上的另一片文章,他總結(jié)的比我好:
Android View 事件分發(fā)機制源碼詳解(ViewGroup篇)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容