從源碼的角度瞭解事件分發機制

前言

Android開發中,事件分發已經成爲開發者的必備知識,那麼若是面試官讓你講一下對事件分發的理解,你將會怎麼去講呢?面試

咱們都在說的事件分發的本質是什麼呢?bash

咱們都知道事件的傳遞和分發都是在手指觸發屏幕的一瞬間開始,那麼在觸發後,系統會作出什麼反應呢?app

接下來,讓咱們一一揭開這層神祕的面紗。ide

事件的傳遞機制

全部的事件發生的起源就在於Touch事件,咱們的在屏幕上的操做動做,能夠分爲四種:post

動做 描述
MotionEvent.ACTION_DOWN 在頁面上按下某個位置
MotionEvent.ACTION_MOVE 手指在頁面上進行移動(滑動)
MotionEvent.ACTION_UP 手指擡起
MotionEvent.ACTION_CANCEL 結束事件(在ACTION_UP後會執行)

從用戶角度來講,咱們手指接觸的地方是一個頁面,也就是咱們所說的UI,UI從上到下依次是ActivityViewGroupView動畫

有可能一個ViewGroup裏還有一個或者多個ViewGroup,這裏咱們只哪最簡單的頁面構造來分析。ui

那麼顯然得知,事件的傳遞機制就是從上到下的,依次就是 Activity -> ViewGroup -> Viewthis

事件的分發機制

事件的分發涉及到三個方法: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開始,直到有一個控件處理了這個事件。咱們來看下源碼中作了些什麼。

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中的分發機制,這也是本篇中比較難的點。

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的分發機制

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。

總結

到這裏,事件分發的機制就瞭解完了,你是否是對剛開始提出的問題有了答案了呢?

相關文章
相關標籤/搜索