Android面試收集錄6 事件分發機制

轉自:秋招面試寶典。

1、 基礎認知

1.1 事件分發的對象是誰?

答:事件java

  • 當用戶觸摸屏幕時(View或ViewGroup派生的控件),將產生點擊事件(Touch事件)。android

    Touch事件相關細節(發生觸摸的位置、時間、歷史記錄、手勢動做等)被封裝成MotionEvent對象git

  • 主要發生的Touch事件有以下四種:github

    • MotionEvent.ACTION_DOWN:按下View(全部事件的開始)
    • MotionEvent.ACTION_MOVE:滑動View
    • MotionEvent.ACTION_CANCEL:非人爲緣由結束本次事件
    • MotionEvent.ACTION_UP:擡起View(與DOWN對應)
  • 事件列:從手指接觸屏幕至手指離開屏幕,這個過程產生的一系列事件 任何事件列都是以DOWN事件開始,UP事件結束,中間有無數的MOVE事件,以下圖:面試

即當一個MotionEvent 產生後,系統須要把這個事件傳遞給一個具體的 View 去處理,ide

1.2 事件分發的本質

答:將點擊事件(MotionEvent)向某個View進行傳遞並最終獲得處理函數

即當一個點擊事件發生後,系統須要將這個事件傳遞給一個具體的View去處理。這個事件傳遞的過程就是分發過程。源碼分析

1.3 事件在哪些對象之間進行傳遞?

答:Activity、ViewGroup、View佈局

一個點擊事件產生後,傳遞順序是:Activity(Window) -> ViewGroup -> Viewpost

  • Android的UI界面是由Activity、ViewGroup、View及其派生類組合而成的

  • View是全部UI組件的基類

    通常Button、ImageView、TextView等控件都是繼承父類View

  • ViewGroup是容納UI組件的容器,即一組View的集合(包含不少子View和子VewGroup),

    1. 其自己也是從View派生的,即ViewGroup是View的子類
    2. 是Android全部佈局的父類或間接父類:項目用到的佈局(LinearLayout、RelativeLayout等),都繼承自ViewGroup,即屬於ViewGroup子類。
    3. 與普通View的區別:ViewGroup實際上也是一個View,只不過比起View,它多了能夠包含子View和定義佈局參數的功能。

1.4 事件分發過程由哪些方法協做完成?

答:dispatchTouchEvent() 、onInterceptTouchEvent()和onTouchEvent()

下文會對這3個方法進行詳細介紹

1.5 總結

  • Android事件分發機制的本質是要解決:

    點擊事件由哪一個對象發出,通過哪些對象,最終達到哪一個對象並最終獲得處理。

    這裏的對象是指Activity、ViewGroup、View

  • Android中事件分發順序:Activity(Window) -> ViewGroup -> View

  • 事件分發過程由dispatchTouchEvent() 、onInterceptTouchEvent()和onTouchEvent()三個方法協助完成

通過上述3個問題,相信你們已經對Android的事件分發有了感性的認知,接下來,我將詳細介紹Android事件分發機制。

2、 事件分發機制方法&流程介紹

  • 事件分發過程由dispatchTouchEvent() 、onInterceptTouchEvent()和onTouchEvent()三個方法協助完成,以下圖:

方法詳細介紹

  • Android事件分發流程以下:(必須熟記)

    Android事件分發順序:Activity(Window) -> ViewGroup -> View

其中:

  • super:調用父類方法
  • true:消費事件,即事件不繼續往下傳遞
  • false:不消費事件,事件也不繼續往下傳遞 / 交由給父控件onTouchEvent()處理

接下來,我將詳細介紹這3個方法及相關流程。

2.1 dispatchTouchEvent()

屬性 介紹
使用對象 Activity、ViewGroup、View
做用 分發點擊事件
調用時刻 當點擊事件可以傳遞給當前View時,該方法就會被調用
返回結果 是否消費當前事件,詳細狀況以下:

1. 默認狀況:根據當前對象的不一樣而返回方法不一樣

對象 返回方法 備註
Activity super.dispatchTouchEvent() 即調用父類ViewGroup的dispatchTouchEvent()
ViewGroup onIntercepTouchEvent() 即調用自身的onIntercepTouchEvent()
View onTouchEvent() 即調用自身的onTouchEvent()

流程解析

2. 返回true

  • 消費事件
  • 事件不會往下傳遞
  • 後續事件(Move、Up)會繼續分發到該View
  • 流程圖以下:

3. 返回false

  • 不消費事件

  • 事件不會往下傳遞

  • 將事件回傳給父控件的onTouchEvent()處理

    Activity例外:返回false=消費事件

  • 後續事件(Move、Up)會繼續分發到該View(與onTouchEvent()區別)

  • 流程圖以下:

2.2 onTouchEvent()

屬性 介紹
使用對象 Activity、ViewGroup、View
做用 處理點擊事件
調用時刻 在dispatchTouchEvent()內部調用
返回結果 是否消費(處理)當前事件,詳細狀況以下:

與dispatchTouchEvent()相似

1. 返回true

  • 本身處理(消費)該事情

  • 事件中止傳遞

  • 該事件序列的後續事件(Move、Up)讓其處理;

  • 流程圖以下:

2. 返回false(同默認實現:調用父類onTouchEvent())

  • 不處理(消費)該事件

  • 事件往上傳遞給父控件的onTouchEvent()處理

  • 當前View再也不接受此事件列的其餘事件(Move、Up);

  • 流程圖以下:

2.3 onInterceptTouchEvent()

屬性 介紹
使用對象 ViewGroup(注:Activity、View都沒該方法)
做用 攔截事件,即本身處理該事件
調用時刻 在ViewGroup的dispatchTouchEvent()內部調用
返回結果 是否攔截當前事件,詳細狀況以下:

返回結果

  • 流程圖以下:

2.4 三者關係

下面將用一段僞代碼來闡述上述三個方法的關係和點擊事件傳遞規則

// 點擊事件產生後,會直接調用dispatchTouchEvent()方法 public boolean dispatchTouchEvent(MotionEvent ev) { //表明是否消耗事件 boolean consume = false; if (onInterceptTouchEvent(ev)) { //若是onInterceptTouchEvent()返回true則表明當前View攔截了點擊事件 //則該點擊事件則會交給當前View進行處理 //即調用onTouchEvent ()方法去處理點擊事件 consume = onTouchEvent (ev) ; } else { //若是onInterceptTouchEvent()返回false則表明當前View不攔截點擊事件 //則該點擊事件則會繼續傳遞給它的子元素 //子元素的dispatchTouchEvent()就會被調用,重複上述過程 //直到點擊事件被最終處理爲止 consume = child.dispatchTouchEvent (ev) ; } return consume; }

2.5 總結

  • 對於事件分發的3個方法,你應該清楚瞭解
  • 接下來,我將開始介紹Android事件分發的常見流程

3、事件分發場景介紹

下面我將利用例子來講明常見的點擊事件傳遞狀況

3.1 背景描述

咱們將要討論的佈局層次以下:

  • 最外層:Activiy A,包含兩個子View:ViewGroup B、View C
  • 中間層:ViewGroup B,包含一個子View:View C
  • 最內層:View C

假設用戶首先觸摸到屏幕上View C上的某個點(如圖中黃色區域),那麼Action_DOWN事件就在該點產生,而後用戶移動手指並最後離開屏幕。

3.2 通常的事件傳遞狀況

通常的事件傳遞場景有:

  • 默認狀況
  • 處理事件
  • 攔截DOWN事件
  • 攔截後續事件(MOVE、UP)

3.2.1 默認狀況

  • 即不對控件裏的方法(dispatchTouchEvent()、onTouchEvent()、onInterceptTouchEvent())進行重寫或更改返回值
  • 那麼調用的是這3個方法的默認實現:調用父類的方法
  • 事件傳遞狀況:(如圖下所示)
    • 從Activity A---->ViewGroup B--->View C,從上往下調用dispatchTouchEvent()
    • 再由View C--->ViewGroup B --->Activity A,從下往上調用onTouchEvent()

注:雖然ViewGroup B的onInterceptTouchEvent方法對DOWN事件返回了false,後續的事件(MOVE、UP)依然會傳遞給它的onInterceptTouchEvent()

這一點與onTouchEvent的行爲是不同的。

3.2.2 處理事件

假設View C但願處理這個點擊事件,即C被設置成可點擊的(Clickable)或者覆寫了C的onTouchEvent方法返回true。

最多見的:設置Button按鈕來響應點擊事件

事件傳遞狀況:(以下圖)

  • DOWN事件被傳遞給C的onTouchEvent方法,該方法返回true,表示處理這個事件
  • 由於C正在處理這個事件,那麼DOWN事件將再也不往上傳遞給B和A的onTouchEvent();
  • 該事件列的其餘事件(Move、Up)也將傳遞給C的onTouchEvent()

3.2.3 攔截DOWN事件

假設ViewGroup B但願處理這個點擊事件,即B覆寫了onInterceptTouchEvent()返回true、onTouchEvent()返回true。 事件傳遞狀況:(以下圖)

  • DOWN事件被傳遞給B的onInterceptTouchEvent()方法,該方法返回true,表示攔截這個事件,即本身處理這個事件(再也不往下傳遞)

  • 調用onTouchEvent()處理事件(DOWN事件將再也不往上傳遞給A的onTouchEvent())

  • 該事件列的其餘事件(Move、Up)將直接傳遞給B的onTouchEvent()

    該事件列的其餘事件(Move、Up)將不會再傳遞給B的onInterceptTouchEvent方法,該方法一旦返回一次true,就不再會被調用了。

3.2.4 攔截DOWN的後續事件

假設ViewGroup B沒有攔截DOWN事件(仍是View C來處理DOWN事件),但它攔截了接下來的MOVE事件。

  • DOWN事件傳遞到C的onTouchEvent方法,返回了true。

  • 在後續到來的MOVE事件,B的onInterceptTouchEvent方法返回true攔截該MOVE事件,但該事件並無傳遞給B;這個MOVE事件將會被系統變成一個CANCEL事件傳遞給C的onTouchEvent方法

  • 後續又來了一個MOVE事件,該MOVE事件纔會直接傳遞給B的onTouchEvent()

    1. 後續事件將直接傳遞給B的onTouchEvent()處理
    2. 後續事件將不會再傳遞給B的onInterceptTouchEvent方法,該方法一旦返回一次true,就不再會被調用了。
  • C不再會收到該事件列產生的後續事件。

特別注意:

  • 若是ViewGroup A 攔截了一個半路的事件(如MOVE),這個事件將會被系統變成一個CANCEL事件並傳遞給以前處理該事件的子View;
  • 該事件不會再傳遞給ViewGroup A的onTouchEvent()
  • 只有再到來的事件纔會傳遞到ViewGroup A的onTouchEvent()

3.3 總結

  • 對於Android的事件分發機制,你應該已經很是清楚了
  • 若是你只是但願瞭解Android事件分發機制而不想深刻了解,那麼你能夠離開這篇文章了
  • 對於程序猿來講,知其然還須要知其因此然,接下來,我將經過源碼分析來深刻了解Android事件分發機制

4、Android事件分發機制源碼分析

  • Android中事件分發順序:Activity(Window) -> ViewGroup -> View,再次貼出下圖:

其中:

  • super:調用父類方法
  • true:消費事件,即事件不繼續往下傳遞
  • false:不消費事件,事件繼續往下傳遞 / 交由給父控件onTouchEvent()處理

因此,要想充分理解Android分發機制,本質上是要理解:

  • Activity對點擊事件的分發機制
  • ViewGroup對點擊事件的分發機制
  • View對點擊事件的分發機制

接下來,我將經過源碼分析詳細介紹Activity、View和ViewGroup的事件分發機制

4.1 Activity的事件分發機制

4.1.1 源碼分析

  • 當一個點擊事件發生時,事件最早傳到Activity的dispatchTouchEvent()進行事件分發

    具體是由Activity的Window來完成

  • 咱們來看下Activity的dispatchTouchEvent()的源碼

public boolean dispatchTouchEvent(MotionEvent ev) { //關注點1 //通常事件列開始都是DOWN,因此這裏基本是true if (ev.getAction() == MotionEvent.ACTION_DOWN) { //關注點2 onUserInteraction(); } //關注點3 if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }

關注點1 通常事件列開始都是DOWN(按下按鈕),因此這裏返回true,執行onUserInteraction()

關注點2 先來看下onUserInteraction()源碼

/**  * Called whenever a key, touch, or trackball event is dispatched to the  * activity. Implement this method if you wish to know that the user has  * interacted with the device in some way while your activity is running.  * This callback and {@link #onUserLeaveHint} are intended to help  * activities manage status bar notifications intelligently; specifically,  * for helping activities determine the proper time to cancel a notfication.  *  * <p>All calls to your activity's {@link #onUserLeaveHint} callback will  * be accompanied by calls to {@link #onUserInteraction}. This  * ensures that your activity will be told of relevant user activity such  * as pulling down the notification pane and touching an item there.  *  * <p>Note that this callback will be invoked for the touch down action  * that begins a touch gesture, but may not be invoked for the touch-moved  * and touch-up actions that follow.  *  * @see #onUserLeaveHint()  */ public void onUserInteraction() { }

從源碼能夠看出:

  • 該方法爲空方法
  • 從註釋得知:當此activity在棧頂時,觸屏點擊按home,back,menu鍵等都會觸發此方法
  • 因此onUserInteraction()主要用於屏保

關注點3

  • Window類是抽象類,且PhoneWindow是Window類的惟一實現類

  • superDispatchTouchEvent(ev)是抽象方法

  • 經過PhoneWindow類中看一下superDispatchTouchEvent()的做用

    @Override public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); //mDecor是DecorView的實例 //DecorView是視圖的頂層view,繼承自FrameLayout,是全部界面的父類 }
  • 接下來咱們看mDecor.superDispatchTouchEvent(event):

public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); //DecorView繼承自FrameLayout //那麼它的父類就是ViewGroup 而super.dispatchTouchEvent(event)方法,其實就應該是ViewGroup的dispatchTouchEvent() }

因此:

  • 執行getWindow().superDispatchTouchEvent(ev)其實是執行了ViewGroup.dispatchTouchEvent(event)

  • 再回到最初的代碼:

    public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { //關注點2 onUserInteraction(); } //關注點3 if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }

    因爲通常事件列開始都是DOWN,因此這裏返回true,基本上都會進入getWindow().superDispatchTouchEvent(ev)的判斷

  • 因此,執行Activity.dispatchTouchEvent(ev)其實是執行了ViewGroup.dispatchTouchEvent(event)

  • 這樣事件就從 Activity 傳遞到了 ViewGroup

4.1.2 彙總

當一個點擊事件發生時,調用順序以下

  1. 事件最早傳到Activity的dispatchTouchEvent()進行事件分發
  2. 調用Window類實現類PhoneWindow的superDispatchTouchEvent()
  3. 調用DecorView的superDispatchTouchEvent()
  4. 最終調用DecorView父類的dispatchTouchEvent(),即ViewGroup的dispatchTouchEvent()

4.1.3 結論

  • 當一個點擊事件發生時,事件最早傳到Activity的dispatchTouchEvent()進行事件分發,最終是調用了ViewGroup的dispatchTouchEvent()方法
  • 這樣事件就從 Activity 傳遞到了 ViewGroup

4.2 ViewGroup事件的分發機制

在講解ViewGroup事件的分發機制以前咱們先來看個Demo

4.2.1 Demo講解

佈局以下:

結果測試: 只點擊Button

再點擊空白處

從上面的測試結果發現:

  • 當點擊Button時,執行Button的onClick(),但ViewGroupLayout註冊的onTouch()不會執行
  • 只有點擊空白區域時纔會執行ViewGroupLayout的onTouch();
  • 結論:Button的onClick()將事件消費掉了,所以事件不會再繼續向下傳遞。

接下來,咱們開始進行ViewGroup事件分發的源碼分析

4.2.2 源碼分析

ViewGroup的dispatchTouchEvent()源碼分析,該方法比較複雜,篇幅有限,就截取幾個重要的邏輯片斷進行介紹,來解析整個分發流程。

// 發生ACTION_DOWN事件或者已經發生過ACTION_DOWN,而且將mFirstTouchTarget賦值,才進入此區域,主要功能是攔截器 final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) { //disallowIntercept:是否禁用事件攔截的功能(默認是false),即不由用 //能夠在子View經過調用requestDisallowInterceptTouchEvent方法對這個值進行修改,不讓該View攔截事件 final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; //默認狀況下會進入該方法 if (!disallowIntercept) { //調用攔截方法 intercepted = onInterceptTouchEvent(ev); ev.setAction(action); } else { intercepted = false; } } else { // 當沒有觸摸targets,且不是down事件時,開始持續攔截觸摸。 intercepted = true; }

這一段的內容主要是爲判斷是否攔截。若是當前事件的MotionEvent.ACTION_DOWN,則進入判斷,調用ViewGroup onInterceptTouchEvent()方法的值,判斷是否攔截。若是mFirstTouchTarget != null,即已經發生過MotionEvent.ACTION_DOWN,而且該事件已經有ViewGroup的子View進行處理了,那麼也進入判斷,調用ViewGroup onInterceptTouchEvent()方法的值,判斷是否攔截。若是不是以上兩種狀況,即已是MOVE或UP事件了,而且以前的事件沒有對象進行處理,則設置成true,開始攔截接下來的全部事件。這也就解釋了若是子View的onTouchEvent()方法返回false,那麼接下來的一些列事件都不會交給他處理。若是VieGroup的onInterceptTouchEvent()第一次執行爲true,則mFirstTouchTarget = null,則也會使得接下來不會調用onInterceptTouchEvent(),直接將攔截設置爲true。

當ViewGroup不攔截事件的時候,事件會向下分發交由它的子View或ViewGroup進行處理。

/* 從最底層的父視圖開始遍歷,  ** 找尋newTouchTarget,即上面的mFirstTouchTarget  ** 若是已經存在找尋newTouchTarget,說明正在接收觸摸事件,則跳出循環。  */ for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i; final View child = (preorderedList == null) ? children[childIndex] : preorderedList.get(childIndex); // 若是當前視圖沒法獲取用戶焦點,則跳過本次循環 if (childWithAccessibilityFocus != null) { if (childWithAccessibilityFocus != child) { continue; } childWithAccessibilityFocus = null; i = childrenCount - 1; } //若是view不可見,或者觸摸的座標點不在view的範圍內,則跳過本次循環 if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; } newTouchTarget = getTouchTarget(child); // 已經開始接收觸摸事件,並退出整個循環。 if (newTouchTarget != null) { newTouchTarget.pointerIdBits |= idBitsToAssign; break; } //重置取消或擡起標誌位 //若是觸摸位置在child的區域內,則把事件分發給子View或ViewGroup if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // 獲取TouchDown的時間點 mLastTouchDownTime = ev.getDownTime(); // 獲取TouchDown的Index if (preorderedList != null) { for (int j = 0; j < childrenCount; j++) { if (children[childIndex] == mChildren[j]) { mLastTouchDownIndex = j; break; } } } else { mLastTouchDownIndex = childIndex; } //獲取TouchDown的x,y座標 mLastTouchDownX = ev.getX(); mLastTouchDownY = ev.getY(); //添加TouchTarget,則mFirstTouchTarget != null。 newTouchTarget = addTouchTarget(child, idBitsToAssign); //表示以及分發給NewTouchTarget alreadyDispatchedToNewTouchTarget = true; break; }

dispatchTransformedTouchEvent()方法實際就是調用子元素的dispatchTouchEvent()方法。 其中dispatchTransformedTouchEvent()方法的重要邏輯以下:

if (child == null) { handled = super.dispatchTouchEvent(event); } else { handled = child.dispatchTouchEvent(event); }

因爲其中傳遞的child不爲空,因此就會調用子元素的dispatchTouchEvent()。 若是子元素的dispatchTouchEvent()方法返回true,那麼mFirstTouchTarget就會被賦值,同時跳出for循環。

//添加TouchTarget,則mFirstTouchTarget != null。 newTouchTarget = addTouchTarget(child, idBitsToAssign); //表示以及分發給NewTouchTarget alreadyDispatchedToNewTouchTarget = true;

其中在addTouchTarget(child, idBitsToAssign);內部完成mFirstTouchTarget被賦值。 若是mFirstTouchTarget爲空,將會讓ViewGroup默認攔截全部操做。 若是遍歷全部子View或ViewGroup,都沒有消費事件。ViewGroup會本身處理事件。

結論

  • Android事件分發是先傳遞到ViewGroup,再由ViewGroup傳遞到View

  • 在ViewGroup中經過onInterceptTouchEvent()對事件傳遞進行攔截

    1. onInterceptTouchEvent方法返回true表明攔截事件,即不容許事件繼續向子View傳遞;
    2. 返回false表明不攔截事件,即容許事件繼續向子View傳遞;(默認返回false)
    3. 子View中若是將傳遞的事件消費掉,ViewGroup中將沒法接收到任何事件。

4.3 View事件的分發機制

View中dispatchTouchEvent()的源碼分析

public boolean dispatchTouchEvent(MotionEvent event) { if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event)) { return true; } return onTouchEvent(event); }

從上面能夠看出:

  • 只有如下三個條件都爲真,dispatchTouchEvent()才返回true;不然執行onTouchEvent(event)方法

    第一個條件:mOnTouchListener != null; 第二個條件:(mViewFlags & ENABLED_MASK) == ENABLED; 第三個條件:mOnTouchListener.onTouch(this, event);
  • 下面,咱們來看看下這三個判斷條件:

第一個條件:mOnTouchListener!= null

//mOnTouchListener是在View類下setOnTouchListener方法裏賦值的 public void setOnTouchListener(OnTouchListener l) { //即只要咱們給控件註冊了Touch事件,mOnTouchListener就必定被賦值(不爲空) mOnTouchListener = l; }

第二個條件:(mViewFlags & ENABLED_MASK) == ENABLED

  • 該條件是判斷當前點擊的控件是否enable
  • 因爲不少View默認是enable的,所以該條件恆定爲true

第三個條件:mOnTouchListener.onTouch(this, event)

  • 回調控件註冊Touch事件時的onTouch方法

    //手動調用設置 button.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { return false; } });
  • 若是在onTouch方法返回true,就會讓上述三個條件所有成立,從而整個方法直接返回true。

  • 若是在onTouch方法裏返回false,就會去執行onTouchEvent(event)方法。

接下來,咱們繼續看:**onTouchEvent(event)**的源碼分析

public boolean onTouchEvent(MotionEvent event) { final int viewFlags = mViewFlags; if ((viewFlags & ENABLED_MASK) == DISABLED) { // 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)); } if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } //若是該控件是能夠點擊的就會進入到下兩行的switch判斷中去; if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { //若是當前的事件是擡起手指,則會進入到MotionEvent.ACTION_UP這個case當中。 switch (event.getAction()) { case MotionEvent.ACTION_UP: boolean prepressed = (mPrivateFlags & PREPRESSED) != 0; // 在通過種種判斷以後,會執行到關注點1的performClick()方法。 //請往下看關注點1 if ((mPrivateFlags & PRESSED) != 0 || prepressed) { // take focus if we don't have it already and we should in // touch mode. boolean focusTaken = false; if (isFocusable() && isFocusableInTouchMode() && !isFocused()) { focusTaken = requestFocus(); } if (!mHasPerformedLongPress) { // 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)) { //關注點1 //請往下看performClick()的源碼分析 performClick(); } } } if (mUnsetPressedState == null) { mUnsetPressedState = new UnsetPressedState(); } if (prepressed) { mPrivateFlags |= PRESSED; refreshDrawableState(); postDelayed(mUnsetPressedState, ViewConfiguration.getPressedStateDuration()); } else if (!post(mUnsetPressedState)) { // If the post failed, unpress right now mUnsetPressedState.run(); } removeTapCallback(); } break; case MotionEvent.ACTION_DOWN: if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } mPrivateFlags |= PREPRESSED; mHasPerformedLongPress = false; postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); break; case MotionEvent.ACTION_CANCEL: mPrivateFlags &= ~PRESSED; refreshDrawableState(); removeTapCallback(); break; case MotionEvent.ACTION_MOVE: final int x = (int) event.getX(); final int y = (int) event.getY(); // Be lenient about moving outside of buttons int slop = mTouchSlop; if ((x < 0 - slop) || (x >= getWidth() + slop) || (y < 0 - slop) || (y >= getHeight() + slop)) { // Outside button removeTapCallback(); if ((mPrivateFlags & PRESSED) != 0) { // Remove any future long press/tap checks removeLongPressCallback(); // Need to switch from pressed to not pressed mPrivateFlags &= ~PRESSED; refreshDrawableState(); } } break; } //若是該控件是能夠點擊的,就必定會返回true return true; } //若是該控件是不能夠點擊的,就必定會返回false return false; }

關注點1: performClick()的源碼分析

public boolean performClick() { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); if (mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); mOnClickListener.onClick(this); return true; } return false; }
  • 只要mOnClickListener不爲null,就會去調用onClick方法;
  • 那麼,mOnClickListener又是在哪裏賦值的呢?請繼續看:
public void setOnClickListener(OnClickListener l) { if (!isClickable()) { setClickable(true); } mOnClickListener = l; }
  • 當咱們經過調用setOnClickListener方法來給控件註冊一個點擊事件時,就會給mOnClickListener賦值(不爲空),即會回調onClick()。

結論

  1. onTouch()的執行高於onClick()

  2. 每當控件被點擊時:

    • 若是在回調onTouch()裏返回false,就會讓dispatchTouchEvent方法返回false,那麼就會執行onTouchEvent();若是回調了setOnClickListener()來給控件註冊點擊事件的話,最後會在performClick()方法裏回調onClick()。

      onTouch()返回false(該事件沒被onTouch()消費掉) = 執行onTouchEvent() = 執行OnClick()

    • 若是在回調onTouch()裏返回true,就會讓dispatchTouchEvent方法返回true,那麼將不會執行onTouchEvent(),即onClick()也不會執行;

      onTouch()返回true(該事件被onTouch()消費掉) = dispatchTouchEvent()返回true(不會再繼續向下傳遞) = 不會執行onTouchEvent() = 不會執行OnClick()

下面我將用Demo驗證上述的結論

Demo論證

1. Demo1:在回調onTouch()裏返回true

//設置OnTouchListener() button.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { System.out.println("執行了onTouch(), 動做是:" + event.getAction()); return true; } }); //設置OnClickListener button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { System.out.println("執行了onClick()"); } });

點擊Button,測試結果以下:

2. Demo2:在回調onTouch()裏返回false

//設置OnTouchListener() button.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { System.out.println("執行了onTouch(), 動做是:" + event.getAction()); return false; } }); //設置OnClickListener button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { System.out.println("執行了onClick()"); } });

點擊Button,測試結果以下:

總結:onTouch()返回true就認爲該事件被onTouch()消費掉,於是不會再繼續向下傳遞,即不會執行OnClick()。

5、 思考點

5.1 onTouch()和onTouchEvent()的區別

  • 這兩個方法都是在View的dispatchTouchEvent中調用,但onTouch優先於onTouchEvent執行。

  • 若是在onTouch方法中返回true將事件消費掉,onTouchEvent()將不會再執行。

  • 特別注意:請看下面代碼

    //&&爲短路與,即若是前面條件爲false,將再也不往下執行 //因此,onTouch可以獲得執行須要兩個前提條件: //1. mOnTouchListener的值不能爲空 //2. 當前點擊的控件必須是enable的。 mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event)
  • 所以若是你有一個控件是非enable的,那麼給它註冊onTouch事件將永遠得不到執行。對於這一類控件,若是咱們想要監聽它的touch事件,就必須經過在該控件中重寫onTouchEvent方法來實現。

5.2 Touch事件的後續事件(MOVE、UP)層級傳遞

  • 若是給控件註冊了Touch事件,每次點擊都會觸發一系列action事件(ACTION_DOWN,ACTION_MOVE,ACTION_UP等)

  • 當dispatchTouchEvent在進行事件分發的時候,只有前一個事件(如ACTION_DOWN)返回true,纔會收到後一個事件(ACTION_MOVE和ACTION_UP)

    即若是在執行ACTION_DOWN時返回false,後面一系列的ACTION_MOVE和ACTION_UP事件都不會執行

從上面對事件分發機制分析知:

  • dispatchTouchEvent()和 onTouchEvent()消費事件、終結事件傳遞(返回true)

  • 而onInterceptTouchEvent 並不能消費事件,它至關因而一個分叉口起到分流導流的做用,對後續的ACTION_MOVE和ACTION_UP事件接收起到很是大的做用

    請記住:接收了ACTION_DOWN事件的函數不必定能收到後續事件(ACTION_MOVE、ACTION_UP)

這裏給出ACTION_MOVE和ACTION_UP事件的傳遞結論:

  • 若是在某個對象(Activity、ViewGroup、View)的dispatchTouchEvent()消費事件(返回true),那麼收到ACTION_DOWN的函數也能收到ACTION_MOVE和ACTION_UP

    黑線:ACTION_DOWN事件傳遞方向 紅線:ACTION_MOVE和ACTION_UP事件傳遞方向

  • 若是在某個對象(Activity、ViewGroup、View)的onTouchEvent()消費事件(返回true),那麼ACTION_MOVE和ACTION_UP的事件從上往下傳到這個View後就再也不往下傳遞了,而直接傳給本身的onTouchEvent()並結束本次事件傳遞過程。

    黑線:ACTION_DOWN事件傳遞方向 紅線:ACTION_MOVE和ACTION_UP事件傳遞方向

img

相關文章
相關標籤/搜索