Android 從源碼角度分析事件分發機制(三)

說明:終於寫到了事件分發機制的最後一篇,如果還沒看過Android學習筆記之事件分發機制(一)Android學習筆記之事件分發機制(二)的話可以先看看,再結合源碼會有助於理解。

前言

第一篇主要講了dispatchTouchEventonTouchonTouchEventonClick之間的關係。
第二篇主要講了事件的分發路徑: Activity -> ViewGroup -> View。
這兩篇都還有一些東西講得不是很清楚,所以這篇會順帶把之前一些難以理解的地方給講明白。

源碼版本

Android 22
其他版本的源碼可能會有一些不同,但大概的思路都是一樣的。
說明:爲了節省篇幅和複雜性,源碼我只提取了其中有用到的。具體的源碼請大家自己查看。

主線一

首先,我們先來看這一條主線:dispatchTouchEventonTouchonTouchEventonClick之間的關係。
找到View.java中的dispatchTouchEvent

/**
 * Pass the touch screen motion event down to the target view, or this
 * view if it is the target.
 *
 * @param event The motion event to be dispatched.
 * @return True if the event was handled by the view, false otherwise.
 */
public boolean dispatchTouchEvent(MotionEvent event) {
    boolean result = false;
    if (onFilterTouchEventForSecurity(event)) {
        //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;
} 

先來看下注釋:
將點擊屏幕事件分發到指定的View。
當事件被當前的View處理(消費)時返回true,否則返回false。

然後跳到第13行。if中有四個判斷條件。第一個和第二個我們可以直接認爲是true了,因爲onTouch能被執行也就意味着前兩個條件爲true,不用去追蹤源碼了。看一下第三個條件吧,這裏的意思是判斷當前的View是否是Enable的,Button默認是Enable的,所以第三個條件也爲true。也就是說,onTouch決定了result的值。假設onTouch返回了true,result的值變爲true。來到19行,第一個條件爲false,所以直接跳出判斷,後面的onTouchEvent是不會被執行的。
所以第一個結論來了,當前View爲Enable的前提下,只有當onTouch返回false時,onTouchEvent纔會被執行。

假設我們將onTouch返回false,再進入onTouchEvent中探個究竟。有點長,所以只挑重點來講。

public boolean onTouchEvent(MotionEvent event) {
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);
        }
        // A disabled view that is clickable still consumes the touch
        // events, it just doesn't respond to them.
        return (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
    }

    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
        switch (event.getAction()) {
            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 (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) {
                        // 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)) {
                                performClick();
                            }
                        }
                    }

                    if (mUnsetPressedState == null) {
                        mUnsetPressedState = new UnsetPressedState();
                    }

                    if (prepressed) {
                        postDelayed(mUnsetPressedState,
                                ViewConfiguration.getPressedStateDuration());
                    } else if (!post(mUnsetPressedState)) {
                        // If the post failed, unpress right now
                        mUnsetPressedState.run();
                    }

                    removeTapCallback();
                }
                break;
            case MotionEvent.ACTION_DOWN:
                ...
            case MotionEvent.ACTION_CANCEL:
                ...
            case MotionEvent.ACTION_MOVE:
                ...
        }
        return true;
    }
    return false;
}

先看2-10行。如果當前View是被disable但仍然可以點擊的,返回true,即當前View消費掉此次事件,但沒有對它們做出反應。
從12行開始了一個很長的if塊,一直到74行(中間省略了很多代碼)。先不管if裏面是什麼,只看12行和73行。如果當前View是可以點擊的,最後會返回true消費掉該事件。如果不可點擊,直接返回false。
好了,再來看12行到73行之間的代碼,在46行找到了

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

    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
    return result;
}

看到了很熟悉的onClick有沒有!執行完onClick後會返回true,即消費掉該事件。

先來總結一下主線一吧。對於一個View來說,事件首先會到達dispatchTouchEvent,然後在該方法裏面會先執行onTouch,接着如果onTouch返回false的話就去執行onTouchEvent,然後onClick方法在onTouchEvent中被調用。onTouchonTouchEvent結合起來得到的最後結果會作爲dispatchTouchEvent的返回值。

看到這裏,希望你能看得明白。如果可以的話,那麼接下來的也會很好理解了,不過我更希望你順着這個思路自己分析Activity和ViewGroup的源碼。

主線二

主線二是: Activity -> ViewGroup -> View
先看Activity.java

/**
 * Called to process touch screen events.  You can override this to
 * intercept all touch screen events before they are dispatched to the
 * window.  Be sure to call this implementation for touch screen events
 * that should be handled normally.
 *
 * @param ev The touch screen event.
 *
 * @return boolean Return true if this event was consumed.
 */
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

結合之前第二篇的實驗結果,onTouchEvent最開始是沒有被執行的,也就是說,事件分發發生在這裏面。

if (getWindow().superDispatchTouchEvent(ev)) {
    return true;
}

當事件被消費的時候,getWindow().superDispatchTouchEvent(ev)返回true,從而讓Activity的dispatchTouchEvent返回true。

再來看onTouchEvent的源碼

/**
 * Called when a touch screen event was not handled by any of the views
 * under it.  This is most useful to process touch events that happen
 * outside of your window bounds, where there is no view to receive it.
 *
 * @param event The touch screen event being processed.
 *
 * @return Return true if you have consumed the event, false if you haven't.
 * The default implementation always returns false.
 */
public boolean onTouchEvent(MotionEvent event) {
    if (mWindow.shouldCloseOnTouch(this, event)) {
        finish();
        return true;
    }

    return false;
}

看一下注釋就可以了,當事件沒有被任何View處理的時候,事件會返回給Activity處理。這也就可以解釋爲什麼onTouchEvent有時候會被執行有時候沒被執行了。

接着看ViewGroup.java,只看一個方法就好了。

/**
* Implement this method to intercept all touch screen motion events.  This
* allows you to watch events as they are dispatched to your children, and
* take ownership of the current gesture at any point.
*
* <p>Using this function takes some care, as it has a fairly complicated
* interaction with {@link View#onTouchEvent(MotionEvent)
* View.onTouchEvent(MotionEvent)}, and using it requires implementing
* that method as well as this one in the correct way.  Events will be
* received in the following order:
*
* <ol>
* <li> You will receive the down event here.
* <li> The down event will be handled either by a child of this view
* group, or given to your own onTouchEvent() method to handle; this means
* you should implement onTouchEvent() to return true, so you will
* continue to see the rest of the gesture (instead of looking for
* a parent view to handle it).  Also, by returning true from
* onTouchEvent(), you will not receive any following
* events in onInterceptTouchEvent() and all touch processing must
* happen in onTouchEvent() like normal.
* <li> For as long as you return false from this function, each following
* event (up to and including the final up) will be delivered first here
* and then to the target's onTouchEvent().
* <li> If you return true from here, you will not receive any
* following events: the target view will receive the same event but
* with the action {@link MotionEvent#ACTION_CANCEL}, and all further
* events will be delivered to your onTouchEvent() method and no longer
* appear here.
* </ol>
*
* @param ev The motion event being dispatched down the hierarchy.
* @return Return true to steal motion events from the children and have
* them dispatched to this ViewGroup through onTouchEvent().
* The current target will receive an ACTION_CANCEL event, and no further
* messages will be delivered here.
*/
public boolean onInterceptTouchEvent(MotionEvent ev) {
    return false;
}

在第二篇中已經講過了這個方法,現在是想來看下它的註釋。
大致意思是說:實現這個方法來截獲所有的觸摸屏幕事件,可以在事件發給你(ViewGroup,下同)的孩子之前監聽到事件,並接管這些事件,從而使你的孩子無法收到這些觸摸事件。
使用這個方法需要小心點,它和View.onTouchEvent有着複雜的交互,使用這個方法需要同時也重寫onTouchEvent。事件會以下面的順序接收到:

  • 你會在這裏收到ACTION_DOWN事件
  • ACTION_DOWN事件會被你的孩子處理或者你自己的onTouchEvent處理。這意味着你必須在onTouchEvent中返回true,進而你才能繼續看到其餘的事件(而不是尋找你的父節點去處理)。還有,在onTouchEvent中返回true的話,在onInterceptTouchEvent中你不會再接收到剩下的任何事件,所有的事件會像正常情況一樣在onTouchEvent中被處理。
  • 如果在該方法中返回false的話,接下來的事件會先被分發到這裏,然後到達目標View的onTouchEvent
  • 如果在該方法中返回true的話,目標View會接收到ACTION_CANCEL。進一步的事件將不會出現在這裏而是直接到達你的onTouchEvent方法。
    返回true會從你的子節點中偷走事件,然後將事件分發給自己的onTouchEvent處理,目標View會收到ACTION_CANCEL事件,進一步的消息將不會出現在onInterceptTouchEvent中。

講了一大段,其實講得有點囉嗦。大致意思就是如果在這個方法中返回true的話,事件會被自己的onTouchEvent方法處理,不會傳遞到孩子節點中。同時,在onTouchEvent中要返回true,否則系統就會去尋找父節點處理該事件。

主線一和主線二就講到這裏了,希望大家能自己分析一下源碼,再自己寫一寫效果會加倍的.

第二篇的最後還遺留了一個問題,看完這篇,分析起來就很清晰了.
onInterceptTouchEvent返回true的時候, CustomLayout自己的onTouchEvent會被調用,最後返回super.onTouchEvent(event),而這裏的結果最後又會作爲dispatchTouchEvent的返回值,從而判斷是否消費了該事件.爲了方便大家查看,我再貼一下圖

調試信息

從結果來看,super.onTouchEvent(event)的值爲false.不信?自己試試看唄.返回false後,該事件沒有被任何View消費(注意:該事件是不會分發給CustomButton的),最後回傳給了MainActivity自己處理,由於CustomLayout沒有消費該事件,所以ACTION_DOWN在MainActivity中又被處理了一次.
後來,我們讓CustomLayout中的onTouchEvent返回true,即CustomLayout消費了該事件,所以纔有了後面的事件.

調試信息

The End

安卓的事件分發機制寫到這裏總算完了,希望這幾篇博文能讓你對事件分發機制有進一步的瞭解.

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