源碼閱讀分析 - View的Touch事件分發(fā)

其實 Android 事件分發(fā)機(jī)制在早幾年一直都困擾著我,那時候處理事件分發(fā)的自定義 View 腦子都是一片白,老感覺處理不好。后來自己看了 android 源碼,也閱讀了很多大牛的文章才算徹底明白,總之掌握 Android 事件分發(fā)機(jī)制是必不可少的,而 Android 事件分發(fā)機(jī)制絕對不是三言兩語就能說得清的。

而今天由于我們自定義 View 進(jìn)階的需要,自己也是籌備了很久。目前雖然網(wǎng)上相關(guān)的文章也不少,很多也寫得非常詳細(xì),但是多數(shù)文章只是講了講理論,然后配合 Log 打印一下結(jié)果而已。而我準(zhǔn)備不僅帶著大家從源碼的角度進(jìn)行分析,還需要理論結(jié)合實踐寫幾個關(guān)于這方面的效果,這樣相信我們會有更深的理解。閱讀源碼講究由淺入深,循序漸進(jìn),我們就不像其他文章一樣搞混合了,先講 View 的 Touch 事件分發(fā),然后再講 ViewGroup 的事件分發(fā),最后再寫個幾次效果。我們一貫的套路都是理論結(jié)合實踐,由淺入深

先來看幾個效果,如前幾次我們寫自定義評分控件的 RatingBar 復(fù)寫了 onTouchEvent(),這里只是舉個例子:

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

如果想要給這個控件注冊一個點擊事件,只需要調(diào)用:

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

如果想給這個按鈕再添加一個touch事件,只需要調(diào)用:

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

上面的代碼如果運行起來,哪一個會先執(zhí)行呢? 如果是靠猜,那么我們來試一下就知道了,運行程序點擊,打印結(jié)果如下:

運行效果

可以看到,onTouch 是優(yōu)先于優(yōu)先于 onTouchEvent 優(yōu)先于 onClick 執(zhí)行的,并且 onTouch 和 onTouchEvent 執(zhí)行了兩次,一次是 ACTION_DOWN ,一次是 ACTION_UP (你還可能會有多次 ACTION_MOVE 的執(zhí)行,如果你在上面觸摸)。因此事件傳遞的順序是先經(jīng)過 onTouch ,然后經(jīng)過 onTouchEvent ,再傳遞到 onClick 。

如果留心觀察你會發(fā)現(xiàn) setOnTouchListener 是有返回值的,如果返回 ture ,再次運行一下會怎樣?

運行效果

我們發(fā)現(xiàn),onTouchEvent 和 onClick 方法不再執(zhí)行了!為什么會這樣呢?你可以先理解成 onTouch 方法返回 true 就認(rèn)為這個事件被 onTouch 消費掉了,因而不會再繼續(xù) onTouchEvent 和 onClick 。到目前位置如果你清楚了,那么面試的時候基本靠背,那么自己寫效果的時候基本靠蒙。我們肯定不能局限于這個裝態(tài),接下了我們就帶著疑問從源碼的角度分析一下,為什么會出現(xiàn)上述情況?

首先我們需要知道,你點擊或者或者觸摸任何一個 View 都會調(diào)用 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;
    }

省略掉部分代碼之后,這個方法就變得非常的簡潔了,只有短短幾行代碼!我們可以看到,在這個方法內(nèi),首先是進(jìn)行了一個判斷,如果li != null,mOnTouchListener != null,(mViewFlags & ENABLED_MASK) == ENABLED 和 mOnTouchListener.onTouch(this, event) 這三個條件都為真,result 就是 true,否則就去執(zhí)行 onTouchEvent(event) 方法并返回。
  那么 ListenerInfo 到底是什么?我們可以看下源碼,這其實就是有關(guān) 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里發(fā)現(xiàn)了如下方法:

    /**
     * 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是判斷當(dāng)前點擊的控件是否是enable的,默認(rèn)都是enable的,因此這個條件恒定為 true 。

第四個條件就比較關(guān)鍵了,mOnTouchListener.onTouch(this, event),其實也就是去回調(diào)控件注冊 touch 事件時的 onTouch 方法。也就是說如果我們在 onTouch 方法里返回true,就會讓這三個條件全部成立,從而 result 是 true , 那么 onTouchEvent 就不會被執(zhí)行 。如果我們在 onTouch 方法里返回 false,就會去執(zhí)行 onTouchEvent() 方法。

現(xiàn)在我們可以結(jié)合前面的例子來分析一下了,首先在 dispatchTouchEvent 中最先執(zhí)行的就是 onTouch 方法,因此 onTouch 肯定是要優(yōu)先于 onTouchEvent 方法,也是印證了剛剛的打印結(jié)果。而如果在 onTouch 方法里返回了 true,不會再執(zhí)行 onTouchEvent 。但是到目前位置我們還沒有看到 onClick 執(zhí)行,但是我們可以猜到,onClick的調(diào)用肯定是在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 方法復(fù)雜了很多,不過沒關(guān)系,我們只挑重點看就可以了。switch 中如果當(dāng)前的事件是抬起手指,則會進(jìn)入到 MotionEvent.ACTION_UP 這個 case 當(dāng)中。在經(jīng)過種種判斷之后,會執(zhí)行到 performClick() 方法,可以看到,只要 mListenerInfo.mOnClickListener 不是 null,就會去調(diào)用它的 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 事件分發(fā)我們就講到這里了,下一節(jié)我將帶著大家一起了解 ViewGroup 的事件分發(fā)和事件攔截,在源碼的基礎(chǔ)上寫幾個效果,我想應(yīng)該可以說就堪稱完美了。

所有分享大綱:Android進(jìn)階之旅 - 自定義View篇

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容