Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 【自定義View系列】04--談談事件分發

【自定義View系列】04--談談事件分發

編輯:關於Android編程

引言:這部分會分三個模塊來講,先講View對Touch的處理,再講ViewGroup的事件分發,最後講如何解決滑動沖突。

我習慣通過在源碼中添加注釋來理解源碼,以下是我提取出來幾個重要方法,將不重要的部分刪掉,並且添加了中文注釋。


一、先從View講起

如果一個View(比如Button)接收到Touch,那麼該Touch事件首先會傳入到它的dispatchTouchEvent( )方法,所以我們從這裡開始學習View對Touch事件的處理。

    // 返回值表示Touch事件是否被該View消費
    public boolean dispatchTouchEvent(MotionEvent event) {
        //result的值決定最後該方法的返回值,也就是決定Touch事件是否被消費
        boolean result = false;

        /***/

        if (onFilterTouchEventForSecurity(event)) {
            ListenerInfo li = mListenerInfo;
            //該if判斷中一共包含了4個條件,必須同時滿足時才表示Touch事件被消費
            //在這四個條件中,我們通常最關心的就是最後一個:TouchListener的onTouch()方法。假如這四個條件中的任意一個不滿足,那麼result仍為false;則進入下一步調用自身的onTouchEvent()
            if (li != null && li.mOnTouchListener != null &&
                (mViewFlags&ENABLED_MASK)==ENABLED && li.mOnTouchListener.onTouch(this,event)) {
                        result = true;
            }

            //調用View自身的onTouchEvent()處理Touch事件,由onTouchEvent的返回值決定result的值(當然,如果在上一步中,如果已經將result設為true,就不會去判斷onTouchEvent()了)
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

        /***/

        return result;
    }

onTouchEvent()是決定事件是否被消耗的最後一道門,如果返回false,則它的父View的onTouchEvent會被調用,否則不會;
先來看看重要部分的源碼:

public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();

        //如果一個View是disable的,CLICKABLE,LONG_CLICKABLE,CONTEXT_CLICKABLE消耗掉,且不會觸發onClick事件回調
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            return (((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
        }

        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

        //如果View不是disable的,會繼續執行,對CLICK,LONG_CLICK,CONTEXT_CLICKABLE進行處理
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                    /***/
                    break;

                case MotionEvent.ACTION_MOVE:
                    /***/
                    break;

                case MotionEvent.ACTION_CANCEL:
                    /***/
                    break;

                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        /***/
                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            /***/
                            if (!focusTaken) {
                                /***/
                                if (!post(mPerformClick)) {
                                    //performClick()中會回調onClick,所以我們平時常見的onClick回調都是在ACTION_UP的時候觸發的
                                    performClick();
                                }
                            }
                        }
                    }
                    /***/
            }

            //這裡表示,只要View是enable的,Touch事件都會被消耗掉
            return true;
        }

        return false;
    }

引用一張谷歌的小弟畫的流程圖:

這裡寫圖片描述

需要注意的點:<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPm9uVG91Y2goKdPrb25Ub3VjaEV2ZW50KCnS1LywY2xpY2vI/dXftcTH+LHwus3Bqs+1IKO6PC9wPg0Kb25Ub3VjaCgp0+tvblRvdWNoRXZlbnQoKba8yse0psDttKXD/srCvP61xEFQSSBvblRvdWNoKCnK9NPaVG91Y2hMaXN0ZW5lcr3Tv9rW0LXEt723qKOsysdWaWV3sanCtrj408O7p7XEvdO/2rHj09q0psDttKXD/srCvP6jrLb4b25Ub3VjaEV2ZW50KCnKx0FuZHJvaWTPtc2z19TJ7bbU09pUb3VjaLSmwO21xMq1z9Ygz8i199PDb25Ub3VjaCgpuvO199PDb25Ub3VjaEV2ZW50KCmho7b4x9LWu9PQtbFvblRvdWNoKCnOtM/7t9FUb3VjaMrCvP6yxdPQv8nE3LX308O1vW9uVG91Y2hFdmVudCgpoaO8tG9uVG91Y2goKbXE08XPyLy2schvblRvdWNoRXZlbnQoKbXE08XPyLy2uPy436GjINTab25Ub3VjaEV2ZW50KCnW0LSmwO1BQ1RJT05fVVDKsbvhwPvTw0NsaWNrTGlzdGVuZXLWtNDQQ2xpY2vKwrz+oaPL+dLUVG91Y2i1xLSmwO3Kx9PFz8jT2kNsaWNrtcQgvPK1pbXYy7XI/dXf1rTQ0Muz0PLOqqO6b25Ub3VjaCgpJm5kYXNoOyZndDtvblRvdWNoRXZlbnQoKSZuZGFzaDsmZ3Q7b25DbGljaygpDQo8cD5WaWV3w7vT0MrCvP61xMC5vdgob25JbnRlcmNlcHRUb3VjaEV2ZW50KCApKaOsVmlld0dyb3VwssXT0KOsx+vO8Lvsz/08L3A+DQo8aHIgLz4NCjxoMiBpZD0="二viewgroup的事件分發">二、ViewGroup的事件分發 Touch事件會從PhoneWindow開始一直傳遞到最頂層的ViewGroup,然後調用到最頂層的dispatchTouchEvent()

事件分發體系最重要的幾個方法:

dispatchTouchEvent(event)

主要完成事件分發的邏輯,只要事件到達該View,一定會調用這個方法,返回值表示是否消耗當前事件。

onInterceptTouchEvent(Event)

判斷是否攔截某個事件,返回值表示是否攔截

onTouchEvent(Event)

用來處理Touch事件,返回值表示是否消耗該事件。

他們的關系可以這樣表示:

public boolean dispatchTouchEvent(MotionEvent e) {
    if(onInterceptTouchEvent(ev)) {
        //如果攔截,就自己處理,調用自己的onTouchEvent,如果onTouchEvent返回true就消費掉,如果返回false就傳給上層處理
        return onTouchEvent(ev);
    } else {
        //如果不攔截,就走子view的分發流程
        return child.dispatchTouchEvent(ev);
    }
}
來看看源碼:
public boolean dispatchTouchEvent(MotionEvent ev) {

        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            if (actionMasked == MotionEvent.ACTION_DOWN) {
                //如果是DOWN事件,進行初始化和還原操作
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

            // 判斷是否需要攔截事件,根據intercepted的值確定
            // mFirstTouchTarget用於多點觸控
            // mFirstTouchTarget不為空,表示有子View消費了Touch事件
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    // 如果是DOWN或者有子View消費,則根據onInterceptTouchEvent判斷是否攔截
                    // ViewGroup的onInterceptTouchEvent默認返回false,也就是不攔截
                    // 所以我們往往可以在自定義控件中重寫這個方法,來決定什麼情況下攔截事件
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action);
                } else {
                    // 如果disallowIntercept == false,就是不允許攔截,可以在子View設置不允許父View攔截
                    intercepted = false;
                }
            } else {
                // 執行到這裡,說明mFirstTouchTarget為null或者不是DOWN事件,需要ViewGroup自己處理此次Touch事件
                // 也就是攔截本次Touch事件
                intercepted = true;
            }

            // 如果Touch事件沒有被取消也沒有被攔截,那麼ViewGroup將類型為ACTION_DOWN的Touch事件分發給子View。
            if (!canceled && !intercepted) {

                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {

                    // 根據Touch事件的坐標,找到觸摸到了哪個子View
                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        final ArrayList preorderedList = buildOrderedChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = customOrder
                                    ? getChildDrawingOrder(childrenCount, i) : i;
                            final View child = (preorderedList == null)
                                    ? children[childIndex] : preorderedList.get(childIndex);

                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            /***/

                            // 找到之後,調用dispatchTransformedTouchEvent,傳入子view
                            // 子View沒有消費Touch事件則該方法的返回值為false,此時mFirstTouchTarget仍為null
                            // 如果子View消費掉了Touch事件那麼該方法的返回值為true,然後執行newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                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();
                                // 如果子View消費掉了Touch事件那麼該方法的返回值為true,然後執行
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
                        }
                    }
                }
            }

            // mFirstTouchTarget為空,表示沒有子View消耗Touch事件,需要ViewGroup自己處理
            if (mFirstTouchTarget == null) {
                // 同樣也會調用dispatchTransformedTouchEvent,但是傳入Null,標明由父View自己處理這次Touch事件
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                /***/
            }

            /***/
        }
    }
最後會調用dispatchTransformedTouchEvent
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
                                                  View child, int desiredPointerIdBits) {
        final boolean handled;

        /***/

        // 如果child == null,表明沒有子View消費這次Touch事件
        if (child == null) {
            // 所以會調用super.dispatchTouchEvent,此時,ViewGroup就化身為了普通的View,它會在自己的onTouch(),onTouchEvent()中處理Touch
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            final float offsetX = mScrollX - child.mLeft;
            final float offsetY = mScrollY - child.mTop;
            transformedEvent.offsetLocation(offsetX, offsetY);
            if (! child.hasIdentityMatrix()) {
                transformedEvent.transform(child.getInverseMatrix());
            }

            // 如果child不為空,表示有子View處理這次Touch事件,直接調用child的dispatchTouchEvent
            // 當然該view可能是一個View也可能是一個ViewGroup
            handled = child.dispatchTouchEvent(transformedEvent);
        }

        // Done.
        transformedEvent.recycle();
        return handled;
    }

最後會遞歸的執行子View的這套流程,或者被ViewGroup自身攔截掉,親自用onTouchEvent處理這次事件

onInterceptTouchEvent()表示是否攔截此次事件,ViewGroup的默認實現是不攔截,return false;所以我們往往可以在自定義控件中重寫這個方法,來決定什麼情況下攔截事件

總結下來就是:

Touch事件的傳遞順序為 :

Activity–>外層ViewGroup–>內層ViewGroup–>View

如果Touch事件在中間某一層被攔截了,DOWN事件將不會再傳遞給更底層的View

Touch事件的消費順序為 :

View–>內層ViewGroup–>外層ViewGroup–>Activity

如果Touch事件在中間某一層被消費了,將不會再通知更上層的View,只有當所有子View都不消費Touch事件,頂層ViewGroup才會自己處理這次Touch事件。


三、滑動沖突處理

引發原因:兩個可以滑動的View互相嵌套,且滑動方向相同,則會產生滑動沖突。

有兩個比較常見的解決方案:

1、在父View中准確地進行事件分發和攔截

比如重寫onInterceptTouchEvent()和onTouchEvent(),對事件進行正確的分配,保證在合適的時候Touch時間可以傳遞給子View

2、使用Google在support.v4包提供的兩個支持嵌套滾動的接口:onNestedScrollChild、onNestedScrollParent。(有一個例子,在我的Github上一個快速開發框架裡的下拉刷新SwipeLayout中有用到,貼上地址:https://github.com/miomin/Shareward)


四、需要注意的地方

一個事件序列指的是從手指按下的一刻起直到手指放開,通常情況下,一個事件序列只能被一個View攔截或消耗,攔截和消耗通常都是在DOWN事件進行,如果不對DOWN時間進行消費,則不會有機會消耗後續的MOVE事件,如果消耗了DOWN事件,後續的MOVE和UP事件同樣由這個View消費。(support.v4包中的NestedScrolling接口可以打破這個原則,允許多個View同時處理同一個事件序列)

ViewGroup的onInterceptTouchEvent默認返回false,也就是默認不會攔截事件,交給下層的View來處理。

View沒有onInterceptTouchEvent方法,一旦有事件到達,就會調用onTouchEvent。

可點擊的View的onTouchEvent默認返回true,也就是消耗事件。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved