Android 事件分發源碼解析

事件體系簡介

既然是View的事件分發,那么首先我們應該知道什么是Android體系中的事件,它在code世界中又是如何定義的,下面我們簡單介紹一些MotionEvent的知識。

MotionEvent

Android體系中將所有的輸入事件都放到了MotionEvent中,實際上MotionEvent已經發展到十分復雜,由于本文的重點不在這里,所以就單點觸控事件進行介紹。

單點觸控

單點觸控中有以下幾個事件:

  • ACTION_DOWN 手指初次接觸到屏幕上時觸發,簡單地說就是手指按下觸摸到屏幕的時候
  • ACTION_UP 手指離開屏幕時觸發,及手指抬起離開屏幕時
  • ACTION_MOVE 手指在屏幕上滑動時觸發,當手指在屏幕上連續一段距離后被識別為滑動,滑動在一次事件序列中可能會被觸發多次,并不唯一
  • ACTION_CANCEL 當事件傳遞下來后又被上層收回時觸發
  • ACTION_OUTSIDE 手指觸控的區域不在控件上方時觸發

一次單點觸控流程可以大概描述為手指按下(ACTION_DOWN)——>手指在屏幕上滑動(ACTION_MOVE)——>最后手指離開屏幕(ACTION_UP),這是用戶一般使用的簡化過程。了解過這些基礎事件后,我們再看幾個MotionEvent常用API:

  • getAction() 獲取上述事件類型
  • getX() 獲取觸摸點相對于View的X軸坐標
  • getY() 獲取觸摸點相對于View的Y軸坐標
  • getRewX() 獲取觸摸點相對屏幕的X軸坐標
  • getRewY() 獲取觸摸點相對屏幕的Y軸坐標

上面介紹的知識中首先要強調的是ACTION_MOVE在一次事件流程中可能觸發的次數不唯一,其次就要介紹一下比較冷門的兩個事件ACTION_CANCEL,ACTION_OUTSIDE。

  • ACTION_CANCEL

ACTION_CANCEL顧名思義取消事件,既然叫做取消那么就肯定存在先獲取然后被收回這樣一個過程,舉個例子:你收到了一個會議消息,然后你又被告知這個會議被取消了;對應于View事件分發就是指事件序列分發到了某個環節,但他的上層又收回了這個事件序列,這時這個環節就會收到上層給他的ACTION_CANCEL事件.

  • ACTION_OUTSIDE

對于這個事件可能大家會覺得不可思議,明明觸摸事件的發生點都不在該控件的所在區域,看起來不相關的兩者是如何聯系到一起的呢。實際上這種情況的確存在而且也具有它的意義。比如我們常用的Dialog,是不是會有這樣的情況,我們點擊它本身的關閉或取消按鈕,而是點了一下它周圍的其他區域,Dialog依然能夠關閉;這種情況很多彈窗上出現的很多,就是這個ACTION_OUTSIDE事件的作用。

對于Dialog是如何收到這個ACTION_OUTSIDE事件的,這主要是通過WindowManager的布局參數的flags設置為FLAG_WATCH_OUTSIDE_TOUCH,這樣Daialog就具有了觀察并處理超出本身范圍的事件的能力

正文

Android事件分發從當前Activity開始,在到該Activity持有的Window,又由Window將事件傳遞給DecorView,最后又由DecorView再將事件分發給子View;如果子View并不消費該事件,那么該事件序列又會被交給該View的父容器,再到DecorView,再到Window,最后又回到了Activity。這意味著如果一直沒有View消費該事件序列,那么事件會回到Activity并被處理掉。

當不考慮事件序列中途被攔截并消費,只考慮事件分發到View的情況,比如一個Button或者一個TextView。若該View并沒有消費該事件的話,那么最多就會出現上述流程

Activity的事件分發

Activity的事件分發在其dispatchTouchEvent方法里進行了處理,我們看源碼:

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

這里我們可以看到Activity直接將事件交給了Window處理,如果Window的superDispatchTouchEvent方法返回false,那么Activity就會調用他的onTouchEvent方法,這個里如何具體實現我們后面再看。那么事件已經到了Window了。

Window的事件分發

我們知道Window是抽象類,唯一實現類是PhoneWindow,又因為該superDispatchTouchEvent是抽象方法,那么我們只有去PhoneWindow里去看該方法的實現,看源碼:

public boolean superDispatchTouchEvent(MotionEvent ev){
    return mDecor.dispatchTouchEvent(ev);
}

相比于Activity將事件傳到Window來說,Window的做法則要更加簡單粗暴,這里直接傳遞到了DecorView,而這個DecorView就是我們一般說的頂級View,我們在layout包里寫的xml文件就是它的子View。一般來說這個DecorView都是一個ViewGroup,所以事件序列來到了ViewGroup,我們繼續看。

ViewGroup的事件分發

ViewGroup大家應該已經比熟悉了,一般來說ViewGroup的很多邏輯
處理都跟其自身和其子View息息相關,就像View的measure、draw和layout方法類似,這里也很相似,首先我們來看看幾個比較重要的方法:

public boolean dispatchTouchEvent(MotionEvent ev)

用于事件的分發。當事件傳遞到了該View后,該方法一定會被調用,上面關于Activity等的事件分發時,大家應該都看到了這個方法。該方法的返回值由其自身的onTouchEvent()方法和子View的dispatchTouchEvent方法共同決定。

public boolean onInterceptTouchEvent(MotionEvent ev)

此方法會在上述方法內部被調用(View為ViewGroup時),具體的View不會調用該方法。此方法用于判斷是否攔截某個事件,返回結果表示是否攔截當前事件。當同一個事件序列中的某個事件被攔截后,此方法不會被再次調用。

public boolean onTouchEvent(MotionEvent ev)

用于處理點擊事件,返回結果用于判斷是否消費事件,如果不消費,那么此view不會再收到該事件序列的其他事件。

下面我們結合ViewGroup的源碼來分析它的事件分發機制,首先看dispatchTouchEvent方法,源碼內容很長,我們一段一段的分析:

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

            // Handle an initial down. 注釋1
            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();
            }

            // Check for interception.注釋2
            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.注釋3
                intercepted = true;
            }

這里定義了一個名為handled的布爾值,根據命名我們可以猜測為是否處理的,顯然事件還沒有被處理,這也跟我們說的viewGroup的dispatchEvent方法默認不攔截事件相應證,然后再看注釋1處,如果當前事件是ACTION_DOWN(下文簡稱down事件)的話,會調用兩個方法,又看命名,第一個方法應該是清除并取消觸摸的目標,第二個方法應該是重置觸摸狀態,那么到底與我們的猜測是否一致呢,通過查看這兩個方法的源碼和注釋,與我們的猜測完全一致:

/**
     * Cancels and clears all touch targets.
     * 取消并清除所有觸摸目標
     */
    private void cancelAndClearTouchTargets(MotionEvent event) {
    //mFirstTouchTarget 一個成員變量,初始為null,如果事件被子view處理完后
    //這個值將被賦值并指向子view,
        if (mFirstTouchTarget != null) {
            boolean syntheticEvent = false;
            if (event == null) {
                final long now = SystemClock.uptimeMillis();
                event = MotionEvent.obtain(now, now,
                        MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
                event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
                syntheticEvent = true;
            }
            //遍歷單鏈表
            for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
                resetCancelNextUpFlag(target.child);
                dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
            }
            //清除觸摸目標 進行了單鏈表的刪除操作
            clearTouchTargets();

            if (syntheticEvent) {
                event.recycle();
            }
        }
    }

再看另一個方法:

/**
     * Resets all touch state in preparation for a new cycle.
     * 重置所有狀態以進入新周期
     */
    private void resetTouchState() {
        clearTouchTargets();
        resetCancelNextUpFlag(this);
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        mNestedScrollAxes = SCROLL_AXIS_NONE;
    }

再次回,到dispatchTouchEvent方法上來,看注釋2處又做了什么,這里有定義了一個布爾值,表示是否攔截事件,然后的判斷內容是如果事件down或者mFirstTouchTarget不為空的話則執行onInterceptTouchEvent方法,是不是down事件很好判斷,前面說到mFirstTouchTarget在子view處理了事件后會被賦值,而想要交給viewgroup處理時,顯然它等于null。所以onInterceptTouchEvent方法只會執行一次在這里就可以看出,并且當down事件被攔截后,之后的整個事件序列都會交給他處理(注釋3)。但是在調用onIntercepTouchEvent方法之前還有一個叫disallowIntercept的布爾值,用于判斷是否禁止攔截,主要受FLAG_DISALLOW_INTERCEPT這個標志位的影響,因為這個標志位子view可以通過一定方法來改變,使viewgroup不攔截該事件并將它傳遞到子view上,這聽起來似乎可以用來解決滑動沖突,但是這得要求這個事件不是down事件,上文已經分析過了,因為此時會進行一系列重置工作,包括標記位。如果down事件并沒有攔截呢,那么這個時候就要將事件傳遞給子View了(這可能會有點長):

                    //獲得所有子view的數量
                    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.
                        //創建了一個用于向子view分發touch事件的list
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        //遍歷子view
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            //獲取子view的腳標
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            //通過腳標拿到子view對象
                            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在播放動畫或者點擊事件的坐標不在該view區域內 則遍歷下一個 
                            //否則交由它處理
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }
                            //從單鏈表中拿到這個值為child的TouchTarget對象
                            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);
                            //調用dispatchTransformedTouchEvent方法,用于判斷子view是否攔截事件
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                //記錄時間
                                mLastTouchDownTime = ev.getDownTime();
                                //記錄子view的腳標
                                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添加進鏈表并返回值
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                //已經分發給新的觸摸目標設置為true
                                alreadyDispatchedToNewTouchTarget = true;
                                //跳出循環,因為處理事件的view已經有了
                                break;
                            }

                            // The accessibility focus didn't handle the event, so clear
                            // the flag and do a normal dispatch to all children.
                            ev.setTargetAccessibilityFocus(false);
                        }
                        //清空存放view的list
                        if (preorderedList != null) preorderedList.clear();
                    }

上面這一大片代碼主要完成了遍歷子View,然后判斷子view是否能夠接受到事件,如果能則將事件分發給子view,如果子view攔截該事件,就記錄下它然后設置為newToucthTarget。判斷子view是否攔截是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;
        }
···
}

這里只截取了最主要的一段代碼,可以看到如果子view不為空,就調用子view的dispatchTouchEvent方法,否則調用父類View的dispatchTouchEvent方法,最后返回結果。如果子view攔截了事件返回true,那么viewgroup就會記錄下它,并為newTouchTarget賦值,然后跳出循環不再遍歷,這一點參見上面代碼中的注釋。到這里還會出現另一種情況,那就是子View的dispatchTouchEvent都返回了false,整個遍歷過程都沒有找到view來處理事件怎么辦呢,顯然這時候只能viewgroup自己來處理事件了,看看它是如何處理的:

// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
    // No touch targets so treat this as an ordinary view.
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
    TouchTarget.ALL_POINTER_IDS);
}

這里又調用了dispatchTransformedTouchEvent方法,需要注意的是傳入的child參數為null,根據上面的分析,這里會將事件交給View來處理,這就是Viewgroup的處理邏輯,下面再看view的事件處理過程是怎樣的。

View的事件分發

view相比ViewGroup沒有onInterceptTouchEvent方法,一點有事件傳遞到它,那么它的onTouchEvent方法就一定會被調用。view的onTouchEvent方法默認會消費事件,除非它是不可點擊的,clickable和longClickable同時為false,所有view的longClickable默認都為false,,clickable就各不相同了,比如Button的clickable默認為true,TextView默認為false;但是當我們為TextView設置點擊事件監聽的時候,它依然能夠消費事件,這主要是因為setOnClickListener的時候,會將clickable設置為true。下面來看看view的dispatchTouchEvent方法:

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

        ...
        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不為null,并且是可以點擊的則執行它的onTouch方法,如果onTouch返回true,那么事件處理就結束了,就不會執行下面的onTouchEvent方法,顯然onTouchListener優先級高于onTouchEvent。然后我們再看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();
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

        if ((viewFlags & ENABLED_MASK) == DISABLED) {//注釋1
            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;
        }
        //注釋2
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }
}

看到注釋1處,當view處于不可用狀態下,依然能處理點擊事件,就像源碼注釋寫的那樣,這些view并不會響應,但依然消費了觸摸事件。注釋2處是view的代理來處理點擊事件,處理機制與OnTouchListener相似,就不分析了。現在來看看在onTouchEvent中對具體的事件是如何處理的:

if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    if ((viewFlags & TOOLTIP) == TOOLTIP) {
                        handleTooltipUp();
                    }
                    //如果根本就是不可點擊 那么設置的相關callback都會被移除
                    if (!clickable) {
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        break;
                    }
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                       ...

                        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();
                                }
                            }
                        }
                        ...
                    }
                    ...
                    break;
    }            
    ...
    return true;
}

從上面的代碼可以看出只要CLICKABLE和LONG_CLICKABLE有一個為true,即為可點擊狀態,那么它就會消費這個事件。點擊事件的觸發在action_up這個事件,這個事件里可以看出主要調用了performClickInternal方法,其實這個方法也主要是調用的performClick方法,如果view設置了onClickListener,那么就會調用它的onClick方法:

private boolean performClickInternal() {
        // Must notify autofill manager before performing the click actions to avoid scenarios where
        // the app has a click listener that changes the state of views the autofill service might
        // be interested on.
        notifyAutofillManagerOnClick();

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

一開始我們就說過,在設置clickListener的時候,clickable會被設置為true,所以當我們為View設置OnClickListener或OnClickListener的時候,只要事件傳遞到它這里來,那么就會執行我們設置的邏輯。到這里View的事件分發也就講完了,對事件分發的清楚認識對于滑動沖突的處理和自定義view都有極大的幫助。

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

推薦閱讀更多精彩內容