天天看點

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

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

前言

第一篇主要講了

dispatchTouchEvent

onTouch

onTouchEvent

onClick

之間的關系。

第二篇主要講了事件的分發路徑: Activity -> ViewGroup -> View。

這兩篇都還有一些東西講得不是很清楚,是以這篇會順帶把之前一些難以了解的地方給講明白。

源碼版本

Android 22

其他版本的源碼可能會有一些不同,但大概的思路都是一樣的。

說明:為了節省篇幅和複雜性,源碼我隻提取了其中有用到的。具體的源碼請大家自己檢視。

主線一

首先,我們先來看這一條主線:

dispatchTouchEvent

onTouch

onTouchEvent

onClick

之間的關系。

找到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) != ) {
            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) != ;
                if ((mPrivateFlags & PFLAG_PRESSED) !=  || 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

中被調用。

onTouch

onTouchEvent

結合起來得到的最後結果會作為

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

的傳回值,進而判斷是否消費了該事件.為了友善大家檢視,我再貼一下圖

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

從結果來看,

super.onTouchEvent(event)

的值為false.不信?自己試試看呗.傳回false後,該事件沒有被任何View消費(注意:該事件是不會分發給CustomButton的),最後回傳給了MainActivity自己處理,由于CustomLayout沒有消費該事件,是以

ACTION_DOWN

在MainActivity中又被處理了一次.

後來,我們讓CustomLayout中的

onTouchEvent

傳回true,即CustomLayout消費了該事件,是以才有了後面的事件.

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

The End

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