Android View 事件分發(fā)機(jī)制源碼詳解(View篇)

前言

Android View 事件分發(fā)機(jī)制源碼詳解(ViewGroup篇)一文中围俘,主要對ViewGroup#dispatchTouchEvent的源碼做了相應(yīng)的解析泛释,其中說到在ViewGroup把事件傳遞給子View的時候更哄,會調(diào)用子View的dispatchTouchEvent闪萄,這時分兩種情況誊涯,如果子View也是一個ViewGroup那么再執(zhí)行同樣的流程繼續(xù)把事件分發(fā)下去洪鸭,即調(diào)用ViewGroup#dispatchTouchEvent;如果子View只是單純的一個View仑扑,那么調(diào)用的是View#dispatchTouchEvent览爵。因此,本文將分析View(非ViewGroup)的事件分發(fā)夫壁、處理機(jī)制拾枣。

View#dispatchTouchEvent

事件來到View的時候,會調(diào)用該方法盒让,前提是你的自定義View沒有重寫該方法梅肤。我們先看看它的源碼:

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)) {  // 1
               result = true;
            }

        if (!result && onTouchEvent(event)) {  // 2
                result = true;
        }
    }
    ...
    return result;
}

我們只看重點部分,這里有一個判斷if(onFilterTouchEventForSecurity(event))邑茄,這個主要是判斷當(dāng)前事件到來的時候姨蝴,窗口有沒有被遮擋,如果被遮擋則會直接返回false肺缕,從而中斷事件的處理左医。如果窗口沒被遮擋,那么會正常處理事件同木。在IF體內(nèi)部浮梢,首先定義了一個ListenerInfo,那么這個ListenerInfo是什么呢彤路?我們跟進(jìn)去看看:

static class ListenerInfo {

        public OnClickListener mOnClickListener;

        protected OnLongClickListener mOnLongClickListener;

        private OnKeyListener mOnKeyListener;

        private OnTouchListener mOnTouchListener;
        ...
    }

可以看到秕硝,這是View里面的一個內(nèi)部類,定義了一系列的Listener洲尊,其中有我們經(jīng)常用到的onClickListener远豺,這里是獲取當(dāng)前View所設(shè)置的Listener。接著是①號處的一個判斷坞嘀,判斷當(dāng)前View是否設(shè)置了onTouchListener躯护,如果設(shè)置了onTouchListener的話,則會調(diào)用onTouchListener.onTouch方法丽涩,然后根據(jù)onTouch方法的返回值來設(shè)置result棺滞,表示事件是否被處理。這里可以看出:onTouchListener的優(yōu)先級最高矢渊,如果在onTouchListener#onTouch中返回true即消耗了事件检眯,那么就無必要繼續(xù)執(zhí)行下面的語句了。如果沒有設(shè)置onTouchListener或者該監(jiān)聽器內(nèi)部沒有消耗事件昆淡,那么就會執(zhí)行②號代碼,來調(diào)用View#onTouchEvent()刽严。

View#onTouchEvent

由于源碼較長昂灵,這里分段來講述避凝。
1、先看下面這一段:

if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                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)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
        }

以上判斷了當(dāng)前View是否可用眨补,如果不可用則進(jìn)入IF體管削,根據(jù)注釋我們知道,即使是不可以狀態(tài)下的View撑螺,如果它自身是可點擊或者可長按的話含思,一樣會消耗事件,只是不作出任何反應(yīng)罷了甘晤。
2含潘、接著往下看:

if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

這里判斷是否設(shè)置了mTouchDelegate,這個表示View的代理线婚,即如果設(shè)置了代理遏弱,那么當(dāng)前View的點擊事件會交給代理的View來處理,調(diào)用代理View的onTouchEvent方法塞弊,如果代理View消耗了事件漱逸,那么相當(dāng)于當(dāng)前View消耗了事件。
3游沿、接下來便是onTouchEvent對View事件的具體處理了:

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) {
                ...
                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;
        ...
    }
    return true;
}

首先是判斷當(dāng)前View是否可以點擊或者長按饰抒,其中一個為true的話,就會進(jìn)入IF體诀黍。進(jìn)入IF體后袋坑,是對事件進(jìn)行判斷,可以看到最后會返回true蔗草,即事件最后會被消耗咒彤。也就是說,如果一個View是clickable或者long_clickable的話咒精,該onTouchEvent方法會返回true镶柱,把事件消耗掉
我們看看對ACTION_UP的事件進(jìn)行響應(yīng)的部分模叙,首先會判斷當(dāng)前View是否是pressed狀態(tài)歇拆,即按下狀態(tài),如果是按下狀態(tài)就會觸發(fā)performClick()方法范咨,我們看看這個方法做了什么故觅,View#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;
    }

可以看出,這里檢測了當(dāng)前View是否設(shè)置了onClickListener渠啊,如果設(shè)置了那么回調(diào)它的onClick方法输吏,所以我們平時對一個Button設(shè)置點擊事件之后,都會在其onTouchEvent方法的ACTION_UP邏輯里面得到回調(diào)替蛉。
這里可以得出結(jié)論:onTouchListener贯溅、onTouchEvent拄氯、onClickListener三者的優(yōu)先級是:onTouchListener>onTouchEvent>onClickListener。

至此它浅,對于View的事件分發(fā)译柏、處理過程分析完畢,接下來總結(jié)一下:
1姐霍、事件傳遞給View的時候鄙麦,會調(diào)用dispatchTouchEvent()方法,但是View沒有onIntercept方法镊折,所以會接著調(diào)用onTouchEvent()方法胯府。
2、如果一個View是可點擊的(clickable或long_clickable)腌乡,那么它默認(rèn)會消耗事件盟劫。對于一個Button來說,默認(rèn)是可點擊的与纽,對于一個textView來說侣签,默認(rèn)是不可點擊的,而對于一個自定義View來說急迂,默認(rèn)也是不可點擊的影所,可以在xml布局中設(shè)置View的點擊性質(zhì)。
3僚碎、如果對一個View設(shè)置了onClickListener監(jiān)聽猴娩,那么確保它的可點擊的,而且接收到了ACTION_DOWN和ACTION_UP事件勺阐。

驗證性試驗

以下是驗證性試驗卷中,根據(jù)這兩篇文章所述內(nèi)容來設(shè)置不同的場景來驗證以上的源碼分析的正確性。
①首先新建一個ViewGroupA渊抽,繼承自LinearLayout蟆豫,重寫了三個重要方法,但是只是打印了事件懒闷,dispatchTouchEvent和onIntercept會調(diào)用父類的響應(yīng)方法十减,而onTouchEvent方法則返回true。代碼如下:

public class ViewGroupA extends LinearLayout {

    public ViewGroupA(Context context) {
        super(context);
    }

    public ViewGroupA(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public ViewGroupA(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();
        switch (action){
            case MotionEvent.ACTION_DOWN:
                Log.d("cylog", "ViewGroupA onTouchEvent ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d("cylog","ViewGroupA onTouchEvent ACTION_MOVE");
                break;
        }
        return true;
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.d("cylog","ViewGroupA dispatchTouchEvent down");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d("cylog","ViewGroupA dispatchTouchEvent move");
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.d("cylog","ViewGroupA onInterceptTouchEvent down");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d("cylog","ViewGroupA onInterceptTouchEvent move");
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }
}

②接下來是在ViewGroupA內(nèi)部的一個子View愤估,ViewA帮辟,重寫了dispatchToucheEvent和onTouchEvent方法,如下所示:

package com.chenyu.viewstudy;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

/**
 * Created by Administrator on 2016/4/17.
 */
public class ViewA extends View {

    public ViewA(Context context) {
        super(context);
    }

    public ViewA(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public ViewA(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.d("cylog","ViewA onTouchEvent down");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d("cylog","ViewA onTouchEvent move");
                break;
            case MotionEvent.ACTION_UP:
                Log.d("cylog","ViewA onTouchEvent up");
                break;
        }
        return super.onTouchEvent(event);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.d("cylog","ViewA dispatchTouchEvent down");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d("cylog","ViewA dispatchTouchEvent move");
                break;
        }
        return super.dispatchTouchEvent(event);
    }
}

③MainActivity內(nèi)部只是設(shè)置了布局玩焰,并無別的代碼由驹,這里不再貼出。
④xml布局文件如下:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <com.chenyu.viewstudy.ViewGroupA
        android:id="@+id/viewgroupa"
        android:layout_width="400dp"
        android:layout_height="400dp"
        android:gravity="center"
        android:background="#2e8abb">
        <com.chenyu.viewstudy.ViewA
            android:id="@+id/viewa"
            android:layout_width="200dp"
            android:layout_height="200dp"
            android:clickable="true"
            android:background="#ed132e"/>
    </com.chenyu.viewstudy.ViewGroupA>
</RelativeLayout>

我們先看看布局圖如下:

布局.jpg

上面藍(lán)色區(qū)域是ViewGroupA,紅色區(qū)域是ViewA,運行程序昔园,我們在紅色區(qū)域滑動一下荔棉,結(jié)果如下所示:
驗證0.jpg

可以看出闹炉,事件正常分發(fā),從ViewGroup開始到View,并在View中得到處理润樱。
以下開始改變條件:
1、ViewGroup攔截ACTION_DOWN事件
在ViewGroupA中做出如下改動:

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            ...
        }
        //對ACTION_DWON攔截羡棵,返回true壹若。
        if (ev.getAction() == MotionEvent.ACTION_DOWN){
            return true;
        }
        return super.onInterceptTouchEvent(ev);
    }

運行,結(jié)果如下所示:


驗證1.jpg

可以看出皂冰,ViewGroupA攔截了ACTION_DOWN事件店展,那么ViewA接收不到事件了,所以后面的全部事件都由ViewGroupA處理秃流。

2赂蕴、ViewGroup攔截ACTION_MOVE事件
同樣,在ViewGroupA中做出如下改動:

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            ...
        }
        if (ev.getAction() == MotionEvent.ACTION_MOVE){
            return true;
        }
        return super.onInterceptTouchEvent(ev);
    }

運行結(jié)果如下:

驗證2.jpg

可以看出舶胀,ViewA還是能正常處理ACTION_DOWN事件概说,但是由于ACTION_MOVE事件被ViewGroup攔截了,所以ViewGroup來處理ACTION_MOVE事件嚣伐,我們注意到糖赔,onIntercept方法來攔截成功后,后續(xù)的事件分發(fā)流程并不會再次調(diào)用轩端,所以一個View攔截了事件后放典,后續(xù)的所有事件都交由這個View處理,并不會再次判斷是否需要攔截基茵,所以這也符合上一篇文章的分析奋构。

3、基于第2點攔截了MOVE事件拱层,同時ViewGroup的onTouchEvent返回值修改弥臼,原來是直接返回true的,表示消耗了事件舱呻,那么這里直接返回super.onTouchEvent(ev):

@Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();
        switch (action){
            ...
        }
        return super.onTouchEvent(event);
    }

同時在Activity中重寫onTouchEvent()方法:

@Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_MOVE:
                Log.d("cylog","Activity onTouchEvent ACTION_MOVE");
                break;
        }
        return super.onTouchEvent(event);
    }

結(jié)果如下:

驗證3.jpg

可以看出醋火,super.onTouchEvent(ev)返回了false,表示不消耗事件箱吕,為什么會這樣呢芥驳?根據(jù)本文分析,一個View只有在可點擊的狀態(tài)下茬高,自身的onTouchEvent方法才會返回true兆旬,這里調(diào)用的是super.onTouchEvent表示調(diào)用父類的onTouchEvent方法,又由于ViewGroupA繼承自LinearLayout怎栽,本身是不可點擊的丽猬,所以這里自然會返回false宿饱。然后我們看到,最終這些沒被消耗的時候回到了Activity脚祟,被Activity消耗掉了谬以。其實這也很好理解,上一篇文章說過由桌,事件的分發(fā)是從Activity開始的为黎,不斷往下尋找能消耗事件的子元素,但如果事件沒被子元素消耗行您,則會逐層返回到Activity铭乾。
所以這里得出結(jié)論:如果View不消耗除了ACTION_DOWN事件之外的其他事件(因為ACTION_DWON事件會初始化事件序列),這個View依然也會接收后續(xù)的事件娃循,同時這些沒被消耗的事件最終會被Activity消耗炕檩。

4、ViewGroupA不做任何修改捌斧,對ViewA修改笛质,為ViewA設(shè)置onTouchListener和onClickListener

View viewA = findViewById(R.id.viewa);
        viewA.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()){
                    case MotionEvent.ACTION_DOWN:
                        Log.d("cylog","ViewA onTouchListener down");
                        break;
                    case MotionEvent.ACTION_MOVE:
                        Log.d("cylog", "ViewA onTouchListener move");
                }
                return true;
            }
        });
        viewA.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("cylog","ViewA onClickListener ");
            }
        });

結(jié)果如下:


驗證4.jpg

可以看出,事件分發(fā)給子View后骤星,如果設(shè)置了onTouchListener经瓷,那么直接調(diào)用它,如果返回true洞难,那么后續(xù)并不會調(diào)用onTouchEvent以及onClickListener了舆吮。如果返回false,繼而調(diào)用onTouchEvent方法队贱,所以onTouchListener的優(yōu)先級最高色冀,這也符合本文的分析。但是要注意一點柱嫌,onClickListener在ACTION_UP中起作用锋恬,如果子View重寫了onTouchEvent()方法,而最后返回的時候沒有返回super.onTouchEvent()编丘,那么不會調(diào)用onClickListener与学。因為壓根沒有調(diào)用到父類的onTouchEvent方法。

至此嘉抓,對于View的事件分發(fā)索守、處理機(jī)制講述完畢,謝謝閱讀抑片。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末卵佛,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌截汪,老刑警劉巖疾牲,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異衙解,居然都是意外死亡阳柔,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進(jìn)店門蚓峦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來盔沫,“玉大人,你說我怎么就攤上這事枫匾。” “怎么了拟淮?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵干茉,是天一觀的道長。 經(jīng)常有香客問我很泊,道長角虫,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任委造,我火速辦了婚禮戳鹅,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘昏兆。我一直安慰自己枫虏,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布爬虱。 她就那樣靜靜地躺著隶债,像睡著了一般。 火紅的嫁衣襯著肌膚如雪跑筝。 梳的紋絲不亂的頭發(fā)上死讹,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天,我揣著相機(jī)與錄音曲梗,去河邊找鬼赞警。 笑死,一個胖子當(dāng)著我的面吹牛虏两,可吹牛的內(nèi)容都是我干的愧旦。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼碘举,長吁一口氣:“原來是場噩夢啊……” “哼忘瓦!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤耕皮,失蹤者是張志新(化名)和其女友劉穎境蜕,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體凌停,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡粱年,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了罚拟。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片台诗。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖赐俗,靈堂內(nèi)的尸體忽然破棺而出拉队,到底是詐尸還是另有隱情,我是刑警寧澤阻逮,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布粱快,位于F島的核電站,受9級特大地震影響叔扼,放射性物質(zhì)發(fā)生泄漏事哭。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一瓜富、第九天 我趴在偏房一處隱蔽的房頂上張望鳍咱。 院中可真熱鬧,春花似錦与柑、人聲如沸谤辜。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽每辟。三九已至,卻和暖如春干旧,著一層夾襖步出監(jiān)牢的瞬間渠欺,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工椎眯, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留挠将,地道東北人。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓编整,卻偏偏與公主長得像舔稀,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子掌测,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,037評論 2 355

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