事件分發(源碼分析)

老規矩,看源碼一定要帶著問題、推測或結論去看,不能看到太深,要能剎得車~不然會陷在源碼中。像我這樣的小菜鳥,總是在看過源碼之后才知道Android源碼設計的強大,所以在此總結一下源碼中是如何處理事件分發的。

注意:本文中所有源碼分析部分均基于 API25 版本,由于安卓系統源碼改變很多,可能與之前版本有所不同,但基本流程都是一致的。
本文將對ViewGroup的dispatchTouchEvent()做了一個比較全面的注釋,并形成了簡化后的偽代碼輔助理解。

單個View分析

首先我們需要先引發一個問題。需要自定義一個View,重載構造方法,并在Activity中實現兩個接口,然后來看一下他都點擊處理情況.

        button= (WidgetButton) findViewById(R.id.btn_content);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.i(TAG,"事件");
            }
        });
        button.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.i(TAG,"Touch");
                return false;
            }
        });

當我們點擊這個自定義控件時

I/widget: Touch
I/widget: Touch
I/widget: Touch
I/widget: 事件

但是如果我們將OnTouchListener中的OnTouch返回true

        button.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.i(TAG,"Touch");
                return true;
            }
        });

點擊控件的結果是

I/widget: Touch
I/widget: Touch
I/widget: Touch
好了問題出現

為什么setOnTouchListener中的onTouch返回true后OnClick就不執行了呢?我們點進去看setOnTouchListener和setOnClickListener做了什么?

    public void setOnTouchListener(OnTouchListener l) {
        getListenerInfo().mOnTouchListener = l;
    }

    public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }

這里發現將監聽賦給了mOnTouchListener和mOnClickListener,我們暫時記住這兩個變量名。
我們要知道當我們點擊自定義View控件時,首先會調用父類(View)的dispatchTouchEvent方法。然后看一下這里邊幾句重要的代碼。

9999    boolean result = false;
...
10017  if (li != null && li.mOnTouchListener != null
10018      && (mViewFlags & ENABLED_MASK) == ENABLED
10019      && li.mOnTouchListener.onTouch(this, event)) {
10020     result = true;
10021   }
  
10023  if (!result && onTouchEvent(event)) {
            result = true;
10025   }

這里我們可以看到之前記住的那個變量名mOnTouchListener,這里判斷如果它的onTouch返回true的時候result賦值為ture。(在源碼中如果有些變量不太明白干什么的,千萬不要糾結,這里判斷的這幾個條件都會滿足)繼續往下看,如過result為false,因為用這里用的是&&所以在第一個條件未滿足的情況下是不會調用第二的條件的,但是重點就在這個第二個條件中(onTouchEvent方法)。

11185    case MotionEvent.ACTION_UP:
...
11216      performClick();

在performClick方法的5637行會調用 li.mOnClickListener.onClick(this); 是不是之前賦值的那個變量名。所以這里可以知道,當mOnTouchListener.onTouch()為true時就不會調用onTouchEvent()方法,但是mOnClickListener.onClick()在onTouchEvent() - up事件 - performClick()方法中,所以也不會調用。

Activity - ViewGroup 分析

為什么要從Activity說起呢。事件收集之后最先傳遞給 Activity, 然后依次向下傳遞。
首先自定義ViewGroup,我這里繼承的是RelativeLayout因為它也是繼承自ViewGroup并且沒有對觸摸事件進行處理,然后重載構造方法,并重寫三個與觸摸事件有關的方法

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i(TAG,"  WidgetViewGroup    onTouchEvent   "+event.getAction());
        return super.onTouchEvent(event);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.i(TAG,"  WidgetViewGroup    onInterceptTouchEvent  "+ev.getAction());
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.i(TAG,"  WidgetViewGroup    dispatchTouchEvent"+  ev.getAction());
        return super.dispatchTouchEvent(ev);
    }

在MainActivity中重寫兩個與觸摸事件有關的方法,為什么是兩個呢,因為在Activity中沒有Intercept事件,因為沒有意義,如果攔截了會導致點擊什么效果都沒有。

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.i(TAG,"   MainActivity    dispatchTouchEvent   "+  ev.getAction());
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i(TAG,"   MainActivity    onTouchEvent   "+  event.getAction());
        return super.onTouchEvent(event);
    }

點擊屏幕時輸出如下 這里0表示Down 2表示Move 1表示Up

I/widget:    MainActivity    dispatchTouchEvent   0
I/widget:   WidgetViewGroup    dispatchTouchEvent0
I/widget:   WidgetViewGroup    onInterceptTouchEvent  0
I/widget:   WidgetViewGroup    onTouchEvent   0
I/widget:    MainActivity    onTouchEvent   0
I/widget:    MainActivity    dispatchTouchEvent   2
I/widget:    MainActivity    onTouchEvent   2
I/widget:    MainActivity    dispatchTouchEvent   1
I/widget:    MainActivity    onTouchEvent   1

這樣就知道了觸摸事件之間執行順序的情況。

好了問題出現

為什么會這樣調用呢?那么先從Activity的dispatchTouchEvent看起。

    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();//這個方法是個空方法,是給用戶進行重寫的
        }
        //這里說明如果這個判斷返回true就不執行Activity的onTouchEvent()方法
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

這里有簡單的注釋,那判斷中的方法是什么呢?進去看一下。

    public abstract boolean superDispatchTouchEvent(MotionEvent event);

可以發現他是Window抽象類的一個抽象方法,在文件開始的注釋中說明了Window抽象類僅有一個實現類PhoneWindow,那么就得去PhoneWidow找這個方法的實現。
PhoneWindow

    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

這里調用的DecorView的方法

    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }

這里調用的父類的方法,但是父類FrameLayout并沒有這個方法,只好再去FrameLayout的類ViewGroup中去找。最終在ViewGroup中找到了相應的方法(dispatchTouchEvent),并做了很多操作,那么我們看一下做了什么操作。2145行開始

        //判斷是否有觸摸設備 比如觸摸筆一類的
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
        }

        // If the event targets the accessibility focused view and this is it, start
        // normal event dispatch. Maybe a descendant is what will handle the click.
        //這是一個賦值功能 AccessibilityService 可以不用手指進行點擊   比如搶紅包
        if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
            ev.setTargetAccessibilityFocus(false);
        }

        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {//事件安全檢查
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;
          /**        
            * 第一步:對于ACTION_DOWN進行處理(Handle an initial down)
            * 因為ACTION_DOWN是一系列事件的開端,當是ACTION_DOWN時進行一些初始化操作.
            * 從源碼的注釋也可以看出來:清除以往的Touch狀態(state)開始新的手勢(gesture)
            * cancelAndClearTouchTargets(ev)中有一個非常重要的操作:
            * 將mFirstTouchTarget設置為null!!!!
            * 隨后在resetTouchState()中重置Touch狀態標識
            * */
            // Handle an initial down.
            //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.
                //這個方法用于清理標志,里邊有一個TouchTarget判讀是否是第一次觸發Down,如果是什么都不處理,如果不是將一些標志全部置為初始化
                cancelAndClearTouchTargets(ev);
                //重置所有接觸狀態,準備一個新的觸摸循環
                resetTouchState();
            }
            /**
             * 第二步:檢查是否要攔截(Check for interception)
             * 在dispatchTouchEvent(MotionEventev)這段代碼中
             * 使用變量intercepted來標記ViewGroup是否攔截Touch事件的傳遞.
             * 該變量在后續代碼中起著很重要的作用.
             *
             * 攔截    intercepted =true
             */
            // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {//正因為這個||所以在move事件是也能夠攔截
                 //這里的 (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0是否為true取決于 requestDisallowInterceptTouchEvent(?)
                 // 這個方法是用于是否允許父類進行攔截 true 為不讓父控件攔截。這里個人理解為判斷子View是否有調用這個方法。
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    //這里的攔截并沒有返回 只是將intercepted 設為 實現方法中返回的值 標示是否做攔截
                    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;
            }

            /**
             * 第三步:檢查cancel(Check for cancelation)
             *
             */
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;

            // Update list of touch targets for pointer down, if needed.
            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;

            /**
             * 第四步:事件分發(Update list of touch targets for pointer down, if needed)
             */
            //不是ACTION_CANCEL并且ViewGroup的攔截標志位intercepted為false(不攔截)

            //intercepted  未被攔截  如果攔截了會跳過這里邊的方法
            if (!canceled && !intercepted) {

                //之前說的那個賦值功能
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;
                //判斷是否是Down事件   還有一個 多手指的判斷
                //第二次Move事件時不會執行這里的代碼 所以不會遍歷子控件,由于move事件頻繁調用 這是對move事件的一個優化
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    final int actionIndex = ev.getActionIndex(); // always 0 for down
                    final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                            : TouchTarget.ALL_POINTER_IDS;

                    // Clean up earlier touch targets for this pointer id in case they
                    // have become out of sync.
                    // 移除所有的多手指操作
                    removePointersFromTouchTargets(idBitsToAssign);

                    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.
                        //重排序  按找Z來排序
                        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);
                            //拿到第i個View  相當于 preorderedList.get(childIndex )
                            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;
                            }
                            //這里判斷這個View能不能夠接收事件
                            //clickable   Invisiable   點擊事件不在  view范圍內(通過pointInView)  正在動畫
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }
                             /**
                             *  執行到了下面
                             *  child  絕對會接受到事件
                             */
                            //如果只分析Down操作 這里返回空  如果是move它就不為null
                            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.
                                //如果能夠找到接收事件的target直接break
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            resetCancelNextUpFlag(child);
                            /**
                             * 真正做事件分發
                             * child  不為空
                             *
                             *
                             * 如果子類   onTouch  返回true  (根據子類onTouchEvent 來返回)
                             */
                            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();
                                 //將child添加到Target  并賦給mFirstTouchTarget 做為將要接收move事件的view
                                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();
                    }

                    if (newTouchTarget == null && mFirstTouchTarget != null) {
                        // Did not find a child to receive the event.
                        // Assign the pointer to the least recently added target.
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
                }
            }
            
            // Dispatch to touch targets.
            //如果該ViewGroup攔截事件為true 那么mFirstTouchTarget 為null (會跳過上面的方法直接到這)
            //如果沒有攔截并且點擊鎖定子view那么mFilrstTouchTarget不為null
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
               /**
                 * 真正做事件分發
                 * child  為空
                 */
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        //如果第三個參數為null將會調用ViewGroup的onTouchEvent(),如果不為null將不會調用ViewGroup的onTouchEvent()調用的是子View的onTouchEvent()
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }

            // Update list of touch targets for pointer up or cancel, if needed.
            if (canceled
                    || actionMasked == MotionEvent.ACTION_UP
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                resetTouchState();
            } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
                final int actionIndex = ev.getActionIndex();
                final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
                removePointersFromTouchTargets(idBitsToRemove);
            }
        }

        if (!handled && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
        }
        return handled;

這就是整個dispatchTouchEvent()的代碼,重要的都做了注釋。還有一部分重要的代碼是

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        ...
        if (child == null) {
            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());
            }

            handled = child.dispatchTouchEvent(transformedEvent);
        }
}

之前一直提到的真正的事件分布,傳view和不傳view 的情況,如過view為Null調用ViewGroup的super.dispatchTouchEvent(),相當于調用ViewGroup的onTouchEvent(),如果不為null這調用子view的dispatchTouchEvent()。
經過源碼的分析,可以簡化一個偽源碼,如下:

    public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean handled = false;

        if (!onInterceptTouchEvent(ev)) {//如果沒有攔截
              //之前會判斷是否存在這個 符合要求的child
              //這個返回結果相當于子View的onTouchEvent的返回結果
              if(child.dispatchTouchEvent(transformedEvent)){
                      //target是鎖定事件的那個view之后的move事件就發生在它身上    mFirstTouchTarget 默認為null
                      mFirstTouchTarget = target;
              }
        }
        //這個判斷相當于沒有攔截或不存在符合條件的子view
        if(mFirstTouchTarget == null){
              handled = onTouchEvent();//調用自身的onTouchEvent();
        }else{
              handled = true;
        }

        return handled;
    }

通過源碼可以直接的了解出 Activity - ViewGroup - View 的一個事件傳遞情況。這樣也就解釋了上面的問題(個人理解,如果是ViewGroup - ViewGroup 將會是一個遞歸的情況)

總結

  1. 事件分發中用到了責任鏈模式,上層View可以攔截事件自己處理,也可以發布給子View,如果子View處理不了還可以返回到上層View進行處理,既保證了事件的有序性,又非常的靈活。
  2. View 的 dispatchTouchEvent 主要用于調度自身的監聽器和 onTouchEvent。
  3. View的事件的調度順序是 onTouchListener > onTouchEvent > onLongClickListener > onClickListener 。
  4. 不論 View 自身是否注冊點擊事件,只要 View 是可點擊的就會消費事件。
  5. 事件是否被消費由返回值決定,true 表示消費,false 表示不消費,與是否使用了事件無關。
  6. ViewGroup 中可能有多個 ChildView 時,將事件分配給包含點擊位置的 ChildView。
  7. 只要接受 ACTION_DOWN 就意味著接受所有的事件,拒絕 ACTION_DOWN 則不會收到后續內容。
  8. (源碼理解)ViewGroup的dispatchTouchEvent 方法處理了所有觸摸操作,onInterceptTouchEvent和onTouchEvent這兩個方法并沒有返回結果,只是返回true、false告訴dispatchTouchEvent應該做啥。

這篇文章是在我學習的基礎上進行了總結,可想而知我還是個很小的菜鳥,如果其中有錯誤還請指出,我會盡快修改文章,并改正自己的理解,謝謝。

最后推薦大神作品

安卓自定義View教程目錄

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容