Android View 事件分發機制源碼詳解(View篇)

前言

Android View 事件分發機制源碼詳解(ViewGroup篇)一文中,主要對ViewGroup#dispatchTouchEvent的源碼做了相應的解析,其中說到在ViewGroup把事件傳遞給子View的時候,會調用子View的dispatchTouchEvent,這時分兩種情況,如果子View也是一個ViewGroup那么再執行同樣的流程繼續把事件分發下去,即調用ViewGroup#dispatchTouchEvent;如果子View只是單純的一個View,那么調用的是View#dispatchTouchEvent。因此,本文將分析View(非ViewGroup)的事件分發、處理機制。

View#dispatchTouchEvent

事件來到View的時候,會調用該方法,前提是你的自定義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)),這個主要是判斷當前事件到來的時候,窗口有沒有被遮擋,如果被遮擋則會直接返回false,從而中斷事件的處理。如果窗口沒被遮擋,那么會正常處理事件。在IF體內部,首先定義了一個ListenerInfo,那么這個ListenerInfo是什么呢?我們跟進去看看:

static class ListenerInfo {

        public OnClickListener mOnClickListener;

        protected OnLongClickListener mOnLongClickListener;

        private OnKeyListener mOnKeyListener;

        private OnTouchListener mOnTouchListener;
        ...
    }

可以看到,這是View里面的一個內部類,定義了一系列的Listener,其中有我們經常用到的onClickListener,這里是獲取當前View所設置的Listener。接著是①號處的一個判斷,判斷當前View是否設置了onTouchListener,如果設置了onTouchListener的話,則會調用onTouchListener.onTouch方法,然后根據onTouch方法的返回值來設置result,表示事件是否被處理。這里可以看出:onTouchListener的優先級最高,如果在onTouchListener#onTouch中返回true即消耗了事件,那么就無必要繼續執行下面的語句了。如果沒有設置onTouchListener或者該監聽器內部沒有消耗事件,那么就會執行②號代碼,來調用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);
        }

以上判斷了當前View是否可用,如果不可用則進入IF體,根據注釋我們知道,即使是不可以狀態下的View,如果它自身是可點擊或者可長按的話,一樣會消耗事件,只是不作出任何反應罷了。
2、接著往下看:

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

這里判斷是否設置了mTouchDelegate,這個表示View的代理,即如果設置了代理,那么當前View的點擊事件會交給代理的View來處理,調用代理View的onTouchEvent方法,如果代理View消耗了事件,那么相當于當前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;
}

首先是判斷當前View是否可以點擊或者長按,其中一個為true的話,就會進入IF體。進入IF體后,是對事件進行判斷,可以看到最后會返回true,即事件最后會被消耗。也就是說,如果一個View是clickable或者long_clickable的話,該onTouchEvent方法會返回true,把事件消耗掉。
我們看看對ACTION_UP的事件進行響應的部分,首先會判斷當前View是否是pressed狀態,即按下狀態,如果是按下狀態就會觸發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;
    }

可以看出,這里檢測了當前View是否設置了onClickListener,如果設置了那么回調它的onClick方法,所以我們平時對一個Button設置點擊事件之后,都會在其onTouchEvent方法的ACTION_UP邏輯里面得到回調。
這里可以得出結論:onTouchListener、onTouchEvent、onClickListener三者的優先級是:onTouchListener>onTouchEvent>onClickListener。

至此,對于View的事件分發、處理過程分析完畢,接下來總結一下:
1、事件傳遞給View的時候,會調用dispatchTouchEvent()方法,但是View沒有onIntercept方法,所以會接著調用onTouchEvent()方法。
2、如果一個View是可點擊的(clickable或long_clickable),那么它默認會消耗事件。對于一個Button來說,默認是可點擊的,對于一個textView來說,默認是不可點擊的,而對于一個自定義View來說,默認也是不可點擊的,可以在xml布局中設置View的點擊性質。
3、如果對一個View設置了onClickListener監聽,那么確保它的可點擊的,而且接收到了ACTION_DOWN和ACTION_UP事件。

驗證性試驗

以下是驗證性試驗,根據這兩篇文章所述內容來設置不同的場景來驗證以上的源碼分析的正確性。
①首先新建一個ViewGroupA,繼承自LinearLayout,重寫了三個重要方法,但是只是打印了事件,dispatchTouchEvent和onIntercept會調用父類的響應方法,而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內部的一個子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內部只是設置了布局,并無別的代碼,這里不再貼出。
④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

上面藍色區域是ViewGroupA,紅色區域是ViewA,運行程序,我們在紅色區域滑動一下,結果如下所示:
驗證0.jpg

可以看出,事件正常分發,從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);
    }

運行,結果如下所示:


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

運行結果如下:

驗證2.jpg

可以看出,ViewA還是能正常處理ACTION_DOWN事件,但是由于ACTION_MOVE事件被ViewGroup攔截了,所以ViewGroup來處理ACTION_MOVE事件,我們注意到,onIntercept方法來攔截成功后,后續的事件分發流程并不會再次調用,所以一個View攔截了事件后,后續的所有事件都交由這個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);
    }

結果如下:

驗證3.jpg

可以看出,super.onTouchEvent(ev)返回了false,表示不消耗事件,為什么會這樣呢?根據本文分析,一個View只有在可點擊的狀態下,自身的onTouchEvent方法才會返回true,這里調用的是super.onTouchEvent表示調用父類的onTouchEvent方法,又由于ViewGroupA繼承自LinearLayout,本身是不可點擊的,所以這里自然會返回false。然后我們看到,最終這些沒被消耗的時候回到了Activity,被Activity消耗掉了。其實這也很好理解,上一篇文章說過,事件的分發是從Activity開始的,不斷往下尋找能消耗事件的子元素,但如果事件沒被子元素消耗,則會逐層返回到Activity。
所以這里得出結論:如果View不消耗除了ACTION_DOWN事件之外的其他事件(因為ACTION_DWON事件會初始化事件序列),這個View依然也會接收后續的事件,同時這些沒被消耗的事件最終會被Activity消耗。

4、ViewGroupA不做任何修改,對ViewA修改,為ViewA設置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 ");
            }
        });

結果如下:


驗證4.jpg

可以看出,事件分發給子View后,如果設置了onTouchListener,那么直接調用它,如果返回true,那么后續并不會調用onTouchEvent以及onClickListener了。如果返回false,繼而調用onTouchEvent方法,所以onTouchListener的優先級最高,這也符合本文的分析。但是要注意一點,onClickListener在ACTION_UP中起作用,如果子View重寫了onTouchEvent()方法,而最后返回的時候沒有返回super.onTouchEvent(),那么不會調用onClickListener。因為壓根沒有調用到父類的onTouchEvent方法。

至此,對于View的事件分發、處理機制講述完畢,謝謝閱讀。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容