Android源碼分析View的事件分發機制

源碼閱讀,謹記:不要掉坑裏,不要有強迫症,帶着疑惑和問題去找它,閱讀你關心的內容就好,別妄想一次就想搞定它!

事件使用與結論

View.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        ALog.dTag("ViewEvent", "自定義View的onClick事件,View:%s", v);
    }
});
View.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        ALog.dTag("ViewEvent", "自定義View的onTouch事件,event:%s,View:%s", event, v);
        return false;//true-消化此事件,false-不消化此事件
    }
});

事件日誌

  1. onTouch 返回false時
D/ViewEvent: ╔════════════════════════════════════════════════════════════════════════════════════════
D/ViewEvent: ║ onTouch (ViewActivity.java:39) [ALog-Content] 自定義View的onTouch事件,event:0		//觸摸事件中的按下(down)事件
D/ViewEvent: ╚════════════════════════════════════════════════════════════════════════════════════════
D/ViewEvent: ╔════════════════════════════════════════════════════════════════════════════════════════
D/ViewEvent: ║ onTouch (ViewActivity.java:39) [ALog-Content] 自定義View的onTouch事件,event:2		//觸摸事件中的滑動(move)事件
D/ViewEvent: ╚════════════════════════════════════════════════════════════════════════════════════════
D/ViewEvent: ╔════════════════════════════════════════════════════════════════════════════════════════
D/ViewEvent: ║ onTouch (ViewActivity.java:39) [ALog-Content] 自定義View的onTouch事件,event:1		//觸摸事件中的放開(up)事件
D/ViewEvent: ╚════════════════════════════════════════════════════════════════════════════════════════
D/ViewEvent: ╔════════════════════════════════════════════════════════════════════════════════════════
D/ViewEvent: ║ onClick (ViewActivity.java:33) [ALog-Content] 自定義View的onClick事件
D/ViewEvent: ╚════════════════════════════════════════════════════════════════════════════════════════

2.onTouch 返回true時

D/ViewEvent: ╔════════════════════════════════════════════════════════════════════════════════════════
D/ViewEvent: ║ onTouch (ViewActivity.java:39) [ALog-Content] 自定義View的onTouch事件,event:0
D/ViewEvent: ╚════════════════════════════════════════════════════════════════════════════════════════
D/ViewEvent: ╔════════════════════════════════════════════════════════════════════════════════════════
D/ViewEvent: ║ onTouch (ViewActivity.java:39) [ALog-Content] 自定義View的onTouch事件,event:2
D/ViewEvent: ╚════════════════════════════════════════════════════════════════════════════════════════
D/ViewEvent: ╔════════════════════════════════════════════════════════════════════════════════════════
D/ViewEvent: ║ onTouch (ViewActivity.java:39) [ALog-Content] 自定義View的onTouch事件,event:1
D/ViewEvent: ╚════════════════════════════════════════════════════════════════════════════════════════

總結:區別在於onTouch返回值,會影響到onClick是否會接收到事件

事件源碼分析

View中的dispatchTouchEvent()如何被調用

前提:熟知當視圖UI發生點擊事件後,最先響應的是Activity的dispatchTouchEvent()函數,那麼接下來的源碼就以dispatchTouchEvent()爲源頭開始分析

Activity中的dispatchTouchEvent():

    /**
     * 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.//如果事件被消化則返回true
     */
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
	}
  1. 當我們查看getWindow() 可知返回對象是Window,而Window又是一個抽象類,它只有一個唯一的實現類是PhoneWindow.java(從類說明驗證)
/**
 * Abstract base class for a top-level window look and behavior policy.  An
 * instance of this class should be used as the top-level view added to the
 * window manager. It provides standard UI policies such as a background, title
 * area, default key processing, etc.
 *
 * <p>The only existing implementation of this abstract class is
 * android.view.PhoneWindow, which you should instantiate when needing a
 * Window.//該抽象類的唯一實現類是android.view.PhoneWindow,....
 */
public abstract class Window {
	...
}
  1. 根據getWindow().superDispatchTouchEvent(ev)我們應該到PhoneWindow.java類中查找superDispatchTouchEvent方法,這裏,出現了DecorView對象mDecor,並且在這裏是調用了mDecor.superDispatchTouchEvent(event);後直接返回
    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
  1. 關鍵點來了,DecorView是個什麼類?類中的superDispatchTouchEvent(event)裏面執行了什麼東西?
    首先,DecorView是Activity窗口的根視圖,不是很明白的可以查找下相關資料,
    其次,代碼如下
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }

到這裏,dispatchTouchEvent這個就不陌生了吧,就是在這裏調用到View中具體實現的dispatchTouchEvent(MotionEvent ev)
在這裏插入圖片描述

爲何onTouch返回true時onClick不被調用

從上面我們已經知道了dispatchTouchEvent是作爲事件的源頭,那麼接下就以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) {
        ......
        //分析步驟 3
        //result 爲該方法的返回值,同時也是控制是否調用onClick事件的標記位,
        //當爲true表示消耗事件時,則將不會調用到onClick
        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)) {
                //分析步驟 2
                //在這裏,我們姑且把li.mOnTouchListener.onTouch(this, event)前面的&&運算結果理解爲true(其實條件也正常也是true),
                //並且li.mOnTouchListener.onTouch(this, event)的返回值決定是否result = true;
                result = true;
            }
			//分析步驟 1
			//縱觀整個方法體內容,與事件有關的,猜測也只剩下這個if條件了,並且在這個&&運算的條件中,result起着至關重要的前提
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
        ......
        return result;
    }

以上dispatchTouchEvent方法內,忽略掉了部分與本次內容無關的源碼,需要全部源碼的,可自行查閱26版本的SDK

  1. 首先需先分析出onClick是在哪裏被調用
  2. 其次分析調用的前提條件
  3. 再來纔是閱讀分析前提條件的產生
  4. 最後梳理整個目的流程

道路千萬條,辦法無數種,我找到了performClick()這個方法,因爲裏面就有我想要的邏輯,並且從方法註釋中得到了驗證

  • onClick是在哪裏被調用

譯文部分和註釋部分爲分析內容

    /**
     * Call this view's OnClickListener, if it is defined.  Performs all normal
     * actions associated with clicking: reporting accessibility event, playing
     * a sound, etc.
     *
     * @return True there was an assigned OnClickListener that was called, false
     *         otherwise is returned.
     * 譯文:如果該視圖的OnClickListener被定義了,則調用它。......
     * 意思就是如果這個自定義View被View.setOnClickListener,那麼performClick()就會被調用(當然,前提是事件不被攔截)
     */
    public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            //這裏回調到了使用者以匿名內部類的方式實例出來的接口實現類,
            //這個回調是否被使用,就是直接體現了onClick是否被調用,它就是我們今天的目標
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

        notifyEnterOrExitForAutoFillIfNeeded(true);

        return result;
    }
  • onClick是否被調用的條件
    根據上面的源碼進行分析,onClick是否被調用的條件並不在這裏,那麼就往上分析,是誰調用了performClick()
    如果進行搜索的話,我們會發現performClick()的調用者有不少,但是,以dispatchTouchEvent作爲前提,誰與onTouchEvent有關,誰就是我們的目標,所以我們就在onTouchEvent中找到了performClick()的調用
    /**
     * Implement this method to handle touch screen motion events.
     * <p>
     * If this method is used to detect click actions, it is recommended that
     * the actions be performed by implementing and calling
     * {@link #performClick()}. This will ensure consistent system behavior,
     * including:
     * <ul>
     * <li>obeying click sound preferences
     * <li>dispatching OnClickListener calls
     * <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
     * accessibility features are enabled
     * </ul>
     *
     * @param event The motion event.
     * @return True if the event was handled, false otherwise.
     */
    public boolean onTouchEvent(MotionEvent event) {
        ......

        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    ......
                    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)) {
                                    performClick();
                                }
                            }
                        }

                        ......
                    }
                    ......
                    break;

                case MotionEvent.ACTION_DOWN:
                    ....
                    break;

                case MotionEvent.ACTION_CANCEL:
                    ....
                    break;

                case MotionEvent.ACTION_MOVE:
                    ....
                    break;
            }

            return true;
        }

        return false;
    }

到這裏,已經實力驗證了onTouchEvent是否執行的條件就是決定了onClick是否被調用的條件

  • onTouchEvent事件執行的條件
    onTouchEvent是在dispatchTouchEvent中被調用,所以迴歸到dispatchTouchEvent函數內的註釋說明進行閱讀理解
  • 梳理總結
    在整個事件的分發中,從Activity視圖層接收到觸屏事件開始,onTouch的返回值決定了result的值,即決定了是否執行onClick,所以onTouch返回值爲false的時候,onTouchEvent纔有機會被執行,onClick纔有機會被調用
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章