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

其實(shí) Android 事件分發(fā)機(jī)制在早幾年一直都困擾著我双谆,那時(shí)候處理事件分發(fā)的自定義 View 腦子都是一片白绎秒,老感覺(jué)處理不好一姿。后來(lái)自己看了 android 源碼拖云,也閱讀了很多大牛的文章才算徹底明白,總之掌握 Android 事件分發(fā)機(jī)制是必不可少的踏施,而 Android 事件分發(fā)機(jī)制絕對(duì)不是三言兩語(yǔ)就能說(shuō)得清的石蔗。

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

先來(lái)看幾個(gè)效果虽填,如前幾次我們寫自定義評(píng)分控件的 RatingBar 復(fù)寫了 onTouchEvent()丁恭,這里只是舉個(gè)例子:

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

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

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

如果想給這個(gè)按鈕再添加一個(gè)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;
    }
});

上面的代碼如果運(yùn)行起來(lái)牲览,哪一個(gè)會(huì)先執(zhí)行呢? 如果是靠猜恶守,那么我們來(lái)試一下就知道了第献,運(yùn)行程序點(diǎn)擊,打印結(jié)果如下:

運(yùn)行效果

可以看到兔港,onTouch 是優(yōu)先于優(yōu)先于 onTouchEvent 優(yōu)先于 onClick 執(zhí)行的庸毫,并且 onTouch 和 onTouchEvent 執(zhí)行了兩次,一次是 ACTION_DOWN 衫樊,一次是 ACTION_UP (你還可能會(huì)有多次 ACTION_MOVE 的執(zhí)行岔绸,如果你在上面觸摸)。因此事件傳遞的順序是先經(jīng)過(guò) onTouch 橡伞,然后經(jīng)過(guò) onTouchEvent 盒揉,再傳遞到 onClick 。

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

運(yùn)行效果

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

首先我們需要知道,你點(diǎn)擊或者或者觸摸任何一個(gè) View 都會(huì)調(diào)用 View 的 dispatchTouchEvent() 方法如孝,我們就從這里開(kāi)始分析源碼:

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

省略掉部分代碼之后宪哩,這個(gè)方法就變得非常的簡(jiǎn)潔了,只有短短幾行代碼第晰!我們可以看到锁孟,在這個(gè)方法內(nèi)彬祖,首先是進(jìn)行了一個(gè)判斷,如果li != null品抽,mOnTouchListener != null涧至,(mViewFlags & ENABLED_MASK) == ENABLED 和 mOnTouchListener.onTouch(this, event) 這三個(gè)條件都為真,result 就是 true桑包,否則就去執(zhí)行 onTouchEvent(event) 方法并返回南蓬。
  那么 ListenerInfo 到底是什么?我們可以看下源碼哑了,這其實(shí)就是有關(guān) View 所有事件的一個(gè)集合類赘方,如 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 這個(gè)變量是在哪里賦值的呢?我們尋找之后在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;
    }

第二個(gè)條件 mOnTouchListener 正是在 setOnTouchListener 方法里賦值的拆火,也就是說(shuō)只要我們給控件注冊(cè)了 touch 事件跳夭,mListenerInfo 和 mListenerInfo.mOnTouchListener 就一定被賦值了。

第三個(gè)條件(mViewFlags & ENABLED_MASK) == ENABLED是判斷當(dāng)前點(diǎn)擊的控件是否是enable的们镜,默認(rèn)都是enable的币叹,因此這個(gè)條件恒定為 true 。

第四個(gè)條件就比較關(guān)鍵了模狭,mOnTouchListener.onTouch(this, event)颈抚,其實(shí)也就是去回調(diào)控件注冊(cè) touch 事件時(shí)的 onTouch 方法。也就是說(shuō)如果我們?cè)?onTouch 方法里返回true嚼鹉,就會(huì)讓這三個(gè)條件全部成立贩汉,從而 result 是 true , 那么 onTouchEvent 就不會(huì)被執(zhí)行 锚赤。如果我們?cè)?onTouch 方法里返回 false匹舞,就會(huì)去執(zhí)行 onTouchEvent() 方法。

現(xiàn)在我們可以結(jié)合前面的例子來(lái)分析一下了线脚,首先在 dispatchTouchEvent 中最先執(zhí)行的就是 onTouch 方法赐稽,因此 onTouch 肯定是要優(yōu)先于 onTouchEvent 方法,也是印證了剛剛的打印結(jié)果酒贬。而如果在 onTouch 方法里返回了 true又憨,不會(huì)再執(zhí)行 onTouchEvent 。但是到目前位置我們還沒(méi)有看到 onClick 執(zhí)行锭吨,但是我們可以猜到,onClick的調(diào)用肯定是在onTouchEvent(event)方法中的寒匙!那我們馬上來(lái)看下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ò)沒(méi)關(guān)系考蕾,我們只挑重點(diǎn)看就可以了祸憋。switch 中如果當(dāng)前的事件是抬起手指,則會(huì)進(jìn)入到 MotionEvent.ACTION_UP 這個(gè) case 當(dāng)中肖卧。在經(jīng)過(guò)種種判斷之后蚯窥,會(huì)執(zhí)行到 performClick() 方法,可以看到塞帐,只要 mListenerInfo.mOnClickListener 不是 null拦赠,就會(huì)去調(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ǔ)上寫幾個(gè)效果榔幸,我想應(yīng)該可以說(shuō)就堪稱完美了允乐。

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市削咆,隨后出現(xiàn)的幾起案子牍疏,更是在濱河造成了極大的恐慌,老刑警劉巖拨齐,帶你破解...
    沈念sama閱讀 211,817評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件麸澜,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡奏黑,警方通過(guò)查閱死者的電腦和手機(jī)炊邦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)熟史,“玉大人馁害,你說(shuō)我怎么就攤上這事□迤ィ” “怎么了碘菜?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,354評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)限寞。 經(jīng)常有香客問(wèn)我忍啸,道長(zhǎng),這世上最難降的妖魔是什么履植? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,498評(píng)論 1 284
  • 正文 為了忘掉前任计雌,我火速辦了婚禮,結(jié)果婚禮上玫霎,老公的妹妹穿的比我還像新娘凿滤。我一直安慰自己妈橄,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,600評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布翁脆。 她就那樣靜靜地躺著眷蚓,像睡著了一般。 火紅的嫁衣襯著肌膚如雪反番。 梳的紋絲不亂的頭發(fā)上沙热,一...
    開(kāi)封第一講書(shū)人閱讀 49,829評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音罢缸,去河邊找鬼篙贸。 笑死,一個(gè)胖子當(dāng)著我的面吹牛祖能,可吹牛的內(nèi)容都是我干的歉秫。 我是一名探鬼主播,決...
    沈念sama閱讀 38,979評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼养铸,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼雁芙!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起钞螟,我...
    開(kāi)封第一講書(shū)人閱讀 37,722評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤兔甘,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后鳞滨,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體洞焙,經(jīng)...
    沈念sama閱讀 44,189評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,519評(píng)論 2 327
  • 正文 我和宋清朗相戀三年拯啦,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了澡匪。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,654評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡褒链,死狀恐怖唁情,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情甫匹,我是刑警寧澤甸鸟,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布,位于F島的核電站兵迅,受9級(jí)特大地震影響抢韭,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜恍箭,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,940評(píng)論 3 313
  • 文/蒙蒙 一刻恭、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧季惯,春花似錦吠各、人聲如沸臀突。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,762評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至藕筋,卻和暖如春纵散,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背隐圾。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,993評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工伍掀, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人暇藏。 一個(gè)月前我還...
    沈念sama閱讀 46,382評(píng)論 2 360
  • 正文 我出身青樓蜜笤,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親盐碱。 傳聞我的和親對(duì)象是個(gè)殘疾皇子把兔,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,543評(píng)論 2 349

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