這次,我把Android事件分發機制翻了個遍

這次說下Android中的事件分發機制
從開始點擊屏幕開始,就會產生從Activity開始到decorview一直到最裏層的view一連串事件傳遞。每一層view或者viewgroup都會首先調用它的dispatchTouchEvent方法,然後判斷是否就在當前一層消費掉事件

view的事件分發

首先上一段僞代碼,是在書上看到的,也是我覺得總結的最好的

public boolean dispatchTouchEvent(MotionEvent event) {
    boolean isConsume = false;
    if (isViewGroup) {
        if (onInterceptTouchEvent(event)) {
            isConsume = onTouchEvent(event);
        } else {
            isConsume = child.dispatchTouchEvent(event);
        }

    } else {
        //isView
        isConsume = onTouchEvent(event);
    }
    return isConsume;
}

如果當前是viewgroup層級,就會判斷 onInterceptTouchEvent 是否爲true,如果爲true,則代表事件要消費在這一層級,不再往下傳遞。接着便執行當前 viewgroup 的onTouchEvent方法。如果onInterceptTouchEvent爲false,則代表事件繼續傳遞到下一層級的 dispatchTouchEvent方法,接着一樣的代碼邏輯,一直到最裏面一層的view。

ok,還沒完哦,到最裏面一層就會直接執行onTouchEvent方法,這時候,view有沒有權利拒絕消費事件呢? 按道理view作爲最底層的,應該是沒有發言權纔對。但是呢,秉着公平公正原則,view也是可以拒絕的,可以在onTouchEvent方法返回false,表示他不想消費這個事件。那麼這個事件又會怎麼處理呢?見下面一段僞代碼:

public void handleTouchEvent(MotionEvent event) {
    if (!onTouchEvent(event)) {
        getParent.onTouchEvent(event);
    }
}

如果view的onTouchEvent方法返回false,那麼它的父容器的onTouchEvent又會被調用,如果父容器的onTouchEvent又返回false,則又交給上一級。一直到最上層,也就是Activity的onTouchEvent被調用。

至此,消費流程完畢
但是,關於onTouch,onTouchEvent和onClick又是怎麼樣的調用關係呢?
那就再來一段僞代碼:

public void consumeEvent(MotionEvent event) {
    if (setOnTouchListener) {
        onTouch();
        if (!onTouch()) {
            onTouchEvent(event);
        }
    } else {
        onTouchEvent(event);
    }

    if (setOnClickListener) {
        onClick();
    }
}

當某一層view的onInterceptTouchEvent被調用,則代表當前層級要消費事件。如果它的onTouchListener被設置了的話,則onTouch會被調用,如果onTouch的返回值返回true,則onTouchEvent不會被調用。如果返回false或者沒有設置onTouchListener,則會繼續調用onTouchEvent。而onClick方法則是設置了onClickListener則會被正常調用。

這裏用一張流程圖總結下:
在這裏插入圖片描述

源碼分析

一個觸摸事件,首先是傳到Activity層級,然後傳到根view,通過一層層的viewgroup最終到底最裏面一層的view,我們來一層層解析

Activity(dispatchTouchEvent)

直接上代碼

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

這裏可以看到,onUserInteraction方法是空的,主要是調用了getWindow().superDispatchTouchEvent(ev)方法,返回true,就代表事件消費了。返回false,就代表下層沒人處理,那就直接到了activity的onTouchEvent方法,這點根之前的消費傳遞也是吻合的。

繼續看看superDispatchTouchEvent方法,然後就走到了PhoneWindow的superDispatchTouchEvent方法,以及DecorView的superDispatchTouchEvent,看看代碼:

    //PhoneWindow.java
    private DecorView mDecor;
    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
    
    //DecorView.java
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }

這裏可以看到,依次經過了PhoneWindow到達了DecorView,DecorView是activity的根view,也是setcontentView所設置的view的父view,它是繼承自FrameLayout。所以這裏super.dispatchTouchEvent(event)方法,其實就是走到了viewgroup的dispatchTouchEvent 方法。

ViewGroup(dispatchTouchEvent)

   @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (onFilterTouchEventForSecurity(ev)) {
            // Check for interception,表示是否攔截的字段
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
              //FLAG_DISALLOW_INTERCEPT標誌是通過requestDisallowInterceptTouchEvent設置
                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;
            }
          
          
          //mFirstTouchTarget賦值
             while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    } else {
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            continue;
                        }
                    }
                }          
        }

這裏截取了部分關鍵的代碼,首先是兩個條件

  • actionMasked == MotionEvent.ACTION_DOWN

  • mFirstTouchTarget != null

如果滿足了其中一個條件纔會繼續走下去,執行onInterceptTouchEvent方法等,否則就直接intercepted = true,表示攔截。
第一個條件很明顯,就是表示當前事件位按下事件(ACTION_DOWN)
第二個條件是個字段,根據下面的代碼可以得知,當後面有view消費掉事件的時候,這個mFirstTouchTarget字段就會賦值,否則就爲空。

所以什麼意思呢,當ACTION_DOWN事件時候,一定會執行到後面代碼。當其他事件來的時候,要看當前viewgroup是否消費了事件,如果當前viewgroup已經消費了事件,沒傳到子view,那麼mFirstTouchTarget字段就爲空,所以就不會執行到後面的代碼,就直接消費掉所有事件了。
這就符合了之前的所說的一種機制:

某個view一旦開始攔截,那麼後續事件就全部就給它處理了,也不會執行onInterceptTouchEvent方法了

但是,兩個條件滿足了一個,就能執行到onInterceptTouchEvent了嗎?不一定,這裏看到還有一個判斷條件:disallowIntercept。這個字段是由requestDisallowInterceptTouchEvent方法設置的,後面我們會講到,主要用於滑動衝突,意思就是子view告訴你不想讓你攔截,那麼你就不攔截了,直接返回false。

ok,繼續看源碼,之前的內容我們瞭解到,如果viewgroup不攔截事件,應該會傳遞給子view,那在哪裏傳的呢?繼續看看dispatchTouchEvent的代碼:

if (!canceled && !intercepted) {
                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        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 (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }

                            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);
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }

                }
            }

這裏可以看到,進行了一個子view的便利,其中,如果滿足兩個條件中的一個,就跳出。否則就執行dispatchTransformedTouchEvent方法。先看看這兩個條件:

  • !child.canReceivePointerEvents()
  • !isTransformedTouchPointInView(x, y, child, null)

看名字是看不出啥了,直接看代碼吧:

    protected boolean canReceivePointerEvents() {
        return (mViewFlags & VISIBILITY_MASK) == VISIBLE || 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;
    }

哦,原來是這個意思。canReceivePointerEvents方法就代表view是不是可以接受點擊事件,比如是不是在播放動畫。而isTransformedTouchPointInView方法代表點擊事件的座標是不是在這個view的區域上面。
ok,如果條件都滿足,就執行到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) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }
}

這個方法大家應該都猜到了,其實就是執行了child.dispatchTouchEvent(event)。也就是下一層view的dispatchTouchEvent方法唄,開始事件的層級傳遞。

View(dispatchTouchEvent)

到view 層級的時候,自然就執行的view的dispatchTouchEvent,上代碼

    public boolean dispatchTouchEvent(MotionEvent event) {
        boolean result = false;

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }

        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }

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

        if (!result && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }

        // Clean up after nested scrolls if this is the end of a gesture;
        // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
        // of the gesture.
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }

        return result;
    }

這裏可以看到,首先會判斷li.mOnTouchListener != null,如果不爲空,就會執行onTouch方法。
根據onTouch方法返回的結果,如果爲false,result就爲false,那麼onTouchEvent纔會執行。這個邏輯也是符合我們之前說的傳遞方式。

最後我們再看看view的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:
                    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 (!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();
                                }
                            }
                        }
                    }
                    mIgnoreNextUpEvent = false;
                    break;
            }

            return true;
        }

從代碼可以得知,如果設置了CLICKABLE或者LONG_CLICKABLE,那麼這個view就會消費事件,並且執行performClickInternal方法,然後執行到performClick方法。這個performClick方法大家應該都很熟悉,就是觸發點擊的方法,其實內部就是執行了onClick方法。

    private boolean performClickInternal() {
        notifyAutofillManagerOnClick();
        return performClick();
    }
    
    public boolean performClick() {
        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;
        }
        return result;
    }

至此,源代碼也看的差不多了,內部其實有很多細節,這裏也就不一一說明了,大家有空可以去研究下。

事件分發的應用(requestDisallowInterceptTouchEvent)

那既然學會了事件分發機制,我們實際工作中會怎麼應用呢?其實最常見的就是解決滑動衝突的問題。一般有兩種解決辦法:

  • 一種是外部攔截:從父view端處理,根據情況決定事件是否分發到子view
  • 一種是內部攔截:從子view端處理,根據情況決定是否阻止父view進行攔截,其中的關鍵就是requestDisallowInterceptTouchEvent方法。

第一種方法,其實就是在onInterceptTouchEvnet方法裏面進行判斷返回true還是返回false。
第二種方法,就是用到了requestDisallowInterceptTouchEvent方法,這個方法的意思就是讓父view不要去攔截事件了,在dispatchTouchEvent方法裏面就有這個標誌位:FLAG_DISALLOW_INTERCEPT,如果disallowIntercept字段爲true,就不會去執行onInterceptTouchEvent方法,而是返回false,不攔截事件。

上代碼:

    //外部攔截法:父view.java		
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted = false;
        //父view攔截條件
        boolean parentCanIntercept;

        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                intercepted = false;
                break;
            case MotionEvent.ACTION_MOVE:
                if (parentCanIntercept) {
                    intercepted = true;
                } else {
                    intercepted = false;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercepted = false;
                break;
        }
        return intercepted;

    }

外部攔截很簡單,就是判斷條件,然後決定是否進行攔截。

    //父view.java			
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
            return false;
        } else {
            return true;
        }
    }

    //子view.java
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        //父view攔截條件
        boolean parentCanIntercept;

        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                if (parentCanIntercept) {
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return super.dispatchTouchEvent(event);
    }

感覺內部攔截有點複雜呀,還要重寫父view的方法,這裏分析下,爲什麼要去這麼寫:

  • 父view ACTION_DOWN的時候,不能攔截,因爲如果攔截,那麼後續事件也就跟子view無關了
  • 父view 其他事件的時候,要返回true,表示攔截。因爲onInterceptTouchEvent方法的調用是被FLAG_DISALLOW_INTERCEPT標誌位所控制,所以子view需要父view攔截的時候,纔會走到這個onInterceptTouchEvent方法中來,那麼這時候要保證方法中一定是要攔截的。

至此,事件的分發機制也就說的差不多了。有說的不對的地方望指正,謝謝。🙏


你的一個👍,就是我分享的動力❤️。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章