源碼閱讀分析 View的Touch事件分發

其實 Android 事件分發機制在早幾年一直都困擾着我,那時候處理事件分發的自定義 View 腦子都是一片白,老感受處理很差。後來本身看了 android 源碼,也閱讀了不少大牛的文章纔算完全明白,總之掌握 Android 事件分發機制是必不可少的,而 Android 事件分發機制絕對不是三言兩語就能說得清的。android

而今天因爲咱們自定義 View 進階的須要,本身也是籌備了好久。目前雖然網上相關的文章也很多,不少也寫得很是詳細,可是多數文章只是講了講理論,而後配合 Log 打印一下結果而已。而我準備不只帶着你們從源碼的角度進行分析,還須要理論結合實踐寫幾個關於這方面的效果,這樣相信咱們會有更深的理解。閱讀源碼講究由淺入深,按部就班,咱們就不像其餘文章同樣搞混合了,先講 View 的 Touch 事件分發,而後再講 ViewGroup 的事件分發,最後再寫個幾回效果。咱們一向的套路都是理論結合實踐,由淺入深面試

先來看幾個效果,如前幾回咱們寫自定義評分控件的 RatingBar 複寫了 onTouchEvent(),這裏只是舉個例子:bash

public class RatingBar extends View {
    public RatingBar(Context context) {
        super(context);
    }

    public RatingBar(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public RatingBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d("TAG", "onTouchEvent execute -> " + event.getAction());
        return super.onTouchEvent(event);
    }
}
複製代碼

若是想要給這個控件註冊一個點擊事件,只須要調用:ide

mRatingBar.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Log.d("TAG","onClick execute");
    }
});
複製代碼

若是想給這個按鈕再添加一個touch事件,只須要調用:ui

mRatingBar.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        Log.d("TAG", "onTouch execute -> " + event.getAction());
        return false;
    }
});
複製代碼

上面的代碼若是運行起來,哪個會先執行呢? 若是是靠猜,那麼咱們來試一下就知道了,運行程序點擊,打印結果以下:this

運行效果

能夠看到,onTouch 是優先於優先於 onTouchEvent 優先於 onClick 執行的,而且 onTouch 和 onTouchEvent 執行了兩次,一次是 ACTION_DOWN ,一次是 ACTION_UP (你還可能會有屢次 ACTION_MOVE 的執行,若是你在上面觸摸)。所以事件傳遞的順序是先通過 onTouch ,而後通過 onTouchEvent ,再傳遞到 onClick 。spa

若是留心觀察你會發現 setOnTouchListener 是有返回值的,若是返回 ture ,再次運行一下會怎樣?code

運行效果

咱們發現,onTouchEvent 和 onClick 方法再也不執行了!爲何會這樣呢?你能夠先理解成 onTouch 方法返回 true 就認爲這個事件被 onTouch 消費掉了,於是不會再繼續 onTouchEvent 和 onClick 。到目前位置若是你清楚了,那麼面試的時候基本靠背,那麼本身寫效果的時候基本靠蒙。咱們確定不能侷限於這個裝態,接下了咱們就帶着疑問從源碼的角度分析一下,爲何會出現上述狀況?orm

首先咱們須要知道,你點擊或者或者觸摸任何一個 View 都會調用 View 的 dispatchTouchEvent() 方法,咱們就從這裏開始分析源碼:視頻

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;
            }
        }
        
        // 返回 result
        return result;
    }
複製代碼

省略掉部分代碼以後,這個方法就變得很是的簡潔了,只有短短几行代碼!咱們能夠看到,在這個方法內,首先是進行了一個判斷,若是li != null,mOnTouchListener != null,(mViewFlags & ENABLED_MASK) == ENABLED 和 mOnTouchListener.onTouch(this, event) 這三個條件都爲真,result 就是 true,不然就去執行 onTouchEvent(event) 方法並返回。   那麼 ListenerInfo 究竟是什麼?咱們能夠看下源碼,這其實就是有關 View 全部事件的一個集合類,如 OnFocusChangeListener , OnScrollChangeListener , OnClickListener 、、、

static class ListenerInfo {
        /**
         * Listener used to dispatch focus change events.
         * This field should be made private, so it is hidden from the SDK.
         * {@hide}
         */
        protected OnFocusChangeListener mOnFocusChangeListener;

        /**
         * Listeners for layout change events.
         */
        private ArrayList<OnLayoutChangeListener> mOnLayoutChangeListeners;

        protected OnScrollChangeListener mOnScrollChangeListener;

        /**
         * Listeners for attach events.
         */
        private CopyOnWriteArrayList<OnAttachStateChangeListener> mOnAttachStateChangeListeners;

        /**
         * Listener used to dispatch click events.
         * This field should be made private, so it is hidden from the SDK.
         * {@hide}
         */
        public OnClickListener mOnClickListener;

        /**
         * Listener used to dispatch long click events.
         * This field should be made private, so it is hidden from the SDK.
         * {@hide}
         */
        protected OnLongClickListener mOnLongClickListener;

        /**
         * Listener used to dispatch context click events. This field should be made private, so it
         * is hidden from the SDK.
         * {@hide}
         */
        protected OnContextClickListener mOnContextClickListener;

        /**
         * Listener used to build the context menu.
         * This field should be made private, so it is hidden from the SDK.
         * {@hide}
         */
        protected OnCreateContextMenuListener mOnCreateContextMenuListener;

        private OnKeyListener mOnKeyListener;

        private OnTouchListener mOnTouchListener;

        private OnHoverListener mOnHoverListener;

        private OnGenericMotionListener mOnGenericMotionListener;

        private OnDragListener mOnDragListener;

        private OnSystemUiVisibilityChangeListener mOnSystemUiVisibilityChangeListener;

        OnApplyWindowInsetsListener mOnApplyWindowInsetsListener;
    }
複製代碼

先看一下條件 li.mOnTouchListener 這個變量是在哪裏賦值的呢?咱們尋找以後在View裏發現了以下方法:

/**
     * Register a callback to be invoked when a touch event is sent to this view.
     * @param l the touch listener to attach to this view
     */
    public void setOnTouchListener(OnTouchListener l) {
        getListenerInfo().mOnTouchListener = l;
    }

    ListenerInfo getListenerInfo() {
        if (mListenerInfo != null) {
            return mListenerInfo;
        }
        mListenerInfo = new ListenerInfo();
        return mListenerInfo;
    }
複製代碼

第二個條件 mOnTouchListener 正是在 setOnTouchListener 方法裏賦值的,也就是說只要咱們給控件註冊了 touch 事件,mListenerInfo 和 mListenerInfo.mOnTouchListener 就必定被賦值了。

第三個條件(mViewFlags & ENABLED_MASK) == ENABLED是判斷當前點擊的控件是不是enable的,默認都是enable的,所以這個條件恆定爲 true 。

第四個條件就比較關鍵了,mOnTouchListener.onTouch(this, event),其實也就是去回調控件註冊 touch 事件時的 onTouch 方法。也就是說若是咱們在 onTouch 方法裏返回true,就會讓這三個條件所有成立,從而 result 是 true , 那麼 onTouchEvent 就不會被執行 。若是咱們在 onTouch 方法裏返回 false,就會去執行 onTouchEvent() 方法。

如今咱們能夠結合前面的例子來分析一下了,首先在 dispatchTouchEvent 中最早執行的就是 onTouch 方法,所以 onTouch 確定是要優先於 onTouchEvent 方法,也是印證了剛剛的打印結果。而若是在 onTouch 方法裏返回了 true,不會再執行 onTouchEvent 。可是到目前位置咱們尚未看到 onClick 執行,可是咱們能夠猜到,onClick的調用確定是在onTouchEvent(event)方法中的!那咱們立刻來看下onTouchEvent的源碼,以下所示:

public boolean onTouchEvent(MotionEvent event) {
        // 省略部分代碼
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        performClick();
                    }
                    mIgnoreNextUpEvent = false;
                    break;
                    // 省略部分代碼
            }

            return true;
        }

        return false;
    }

    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;
    }
複製代碼

相較於剛纔的 dispatchTouchEvent 方法,onTouchEvent 方法複雜了不少,不過不要緊,咱們只挑重點看就能夠了。switch 中若是當前的事件是擡起手指,則會進入到 MotionEvent.ACTION_UP 這個 case 當中。在通過種種判斷以後,會執行到 performClick() 方法,能夠看到,只要 mListenerInfo.mOnClickListener 不是 null,就會去調用它的 onClick 方法,那 mListenerInfo.mOnClickListener 又是在哪裏賦值的呢?咱們大概能猜到確定在 setOnclickLstener 方法中:

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

    ListenerInfo getListenerInfo() {
        if (mListenerInfo != null) {
            return mListenerInfo;
        }
        mListenerInfo = new ListenerInfo();
        return mListenerInfo;
    }
複製代碼

View 的 Touch 事件分發咱們就講到這裏了,下一節我將帶着你們一塊兒瞭解 ViewGroup 的事件分發和事件攔截,在源碼的基礎上寫幾個效果,我想應該能夠說就堪稱完美了。

全部分享大綱:Android進階之旅 - 自定義View篇

視頻講解地址:pan.baidu.com/s/1hr6ql72

相關文章
相關標籤/搜索