Android 事件分發之追本溯源

前言

  • Android設備的界面交互帶來了很是好的體驗,在咱們平常使用中,無時無刻不在觸發着事件的分發;好比點擊了淘寶某個圖片,好比點擊了掘金APP的某個按鈕,都會觸發系統的事件分發;
  • Android 事件分發也是自定義View很重要的一個知識點,搞懂事件分發,當自定義View或者解決滑動衝突的問題,都會顯得成竹在胸了;
  • 和以往結合源碼的方式講解相比,更想經過另外一種有趣的角度來分析,接下來讓咱們正式開始吧;

1. 爲何要有事件分發機制?

1.1,Android手機

Android手機做爲手持設備,界面顯示區域並非很大,爲了有便攜的效果,只能犧牲手機的顯示區域;這就會帶來一個問題,可視內容少;爲了避免影響用戶體驗,咱們必需要在有限的區域作更多的展現,這就對界面的設計有很高的要求了;假如咱們是Google的工程師,咱們要怎麼來設計界面,以此帶來好的體驗效果呢?html

1.2,腦洞一

第一種設計:將界面顯示區域切割,根據所須要顯示的視圖,切割爲無數塊,每一塊對應着一部分視圖;以下:android

這種界面設計簡單粗暴,須要多少個視圖,就將界面切割成多少個視圖模塊,以此來放下全部的視圖內容;固然,這樣設計顯而易見會有問題,當視圖愈來愈多的時候,每個視圖的模塊所能展現的區域就會愈來愈小,這樣體驗效果是確定不行的;git

1.3,腦洞二

第二種設計:既然經過切割顯示區域以此來展現視圖的方案有問題,那麼咱們就來試試重疊的效果吧;以下:github

這種設計很好的解決了視圖模塊過多時,顯示區域不夠展現的問題;可是也會存在問題,每個顯示區域和用戶的交互順序混亂了,好比我要和模塊爲4的視圖作交互,結果觸發了視圖5的交互效果,而腦洞一方案則沒有該問題;既然如此,那麼咱們能不能針對腦洞二的方案來進行優化呢?
答案是:有的!設計模式

1.3,設計交互機制

當多個模塊視圖重疊時,要協調好與用戶的交互就極其重要了,畢竟涉及到用戶體驗;bash

當用戶的觸碰屏幕的顯示區域,咱們並不知道哪一個模塊須要和用戶進行交互,而咱們又不能讓用戶和其中一個模塊的交互失效,那麼咱們只能去遍歷重疊的模塊,由內部的視圖來決定是否須要相應用戶的操做;ide

這樣就能夠解決多個模塊視圖重疊時,哪一個模塊須要相應用戶交互的問題了;oop

而這正是Android的事件分發機制;佈局

固然上面只是個人腦洞,用於方便理解,若是你有更好的想法,能夠和我交流;post

那麼這種機制是怎麼來實現這種效果的呢?請繼續往下看;

在深刻分析事件分發以前,先來了解一下事件的來源;

2. 事件是什麼,是怎麼產生的?

2.1,事件的來源

當屏幕被觸摸,Linux內核會將硬件產生的觸摸事件包裝爲Event存到/dev/input/event[x]目錄下。

接着,系統建立的一個InputReaderThread線程loop起來讓EventHub調用getEvent()不斷的從/dev/input/文件夾下讀取輸入事件。

而後InputReader則從EventHub中得到事件交給InputDispatcher。

而InputDispatcher又會把事件分發到須要的地方,好比ViewRootImpl的WindowInputEventReceiver中。

這裏只是簡單瞭解一下大概的流程,源碼過於複雜,這裏不作具體的分析;

歸納之:當觸摸屏幕的時候,硬件會捕捉到用戶的觸摸動做,告訴系統內核,系統內核將該事件保存下來,而後有一個線程會將這個事件讀取出來,交由專門分發的類進行分發;

2.2,事件

當屏幕被觸摸時,系統底層會將觸摸事件(座標和時間等)封裝成MotionEvent事件返回給上層 View;從用戶首次觸摸屏幕開始,經歷手指在屏幕表面的任何移動,直到手指離開屏幕時結束都會產生一系列事件;

MotionEvent的類型:

  • MotionEvent.ACTION_DOWN:當屏幕檢測到第一個觸點按下以後就會觸發到這個事件
  • MotionEvent.ACTION_MOVE:當觸點在屏幕上移動時觸發;
  • MotionEvent.ACTION_UP:當觸點鬆開時被觸發;
  • MotionEvent.ACTION_CANCEL:由系統在須要的時候觸發,不禁用戶直接觸發;

3. 事件分發機制是怎麼實現的?

3.1,設計模式

在分析事件分發機制以前,咱們先來看一下事件分發涉及的涉及模式;

這個設計模式是事件分發機制的核心,Google工程師是經過這個設計模式來設計事件分發機制的;理解了這個設計模式有助於咱們理解事件分發機制;

而這個設計模式就是責任鏈模式;

3.2,責任鏈模式

顧名思義,責任鏈模式(Chain of Responsibility Pattern)爲請求建立了一個接收者對象的鏈。這種模式給予請求的類型,對請求的發送者和接收者進行解耦。這種類型的設計模式屬於行爲型模式。

在這種模式中,一般每一個接收者都包含對另外一個接收者的引用。若是一個對象不能處理該請求,那麼它會把相同的請求傳給下一個接收者,依此類推。

下面咱們經過一段僞代碼來解讀這個模式:

// 請求
        switch (request) {
            case 0:
                // 對象一接收請求並處理
                break;
            case 1:
                // 對象二接收請求並處理
                break;
            case 2:
                // 對象三接收請求並處理
                break;
            case 3:
                // 對象四接收請求並處理
                break;
            case 4:
                // 對象五接收請求並處理
                break;
            default:
                // 默認對象接收請求並處理
        }
複製代碼

上面這個就是咱們用的最熟悉的責任鏈模式,當有一個請求進入責任鏈的時候,會遍歷當前責任鏈上全部的對象,若是匹配到了則提早結束遍歷,若是匹配不到則會被默認的對象接收;

責任鏈的本質是一個單向的鏈表結構,當有請求進入時,只會單向傳遞,直到被接收;

3.3,具體實現

上面咱們理解了責任鏈設計模式以後,接下來咱們來看看事件分發機制的具體實現;

在上上篇博客裏面分析了View的繪製流程,裏面提到了View的層次關係,Activity是View的宿主,而最頂層的View是DecorView,而DecorView裏面則是View樹的結構,那麼咱們將這些關係一一對應到了責任鏈裏面,來看看效果吧;

當有一個事件進入責任鏈時,會從最頂層的DecorView開始往View樹傳遞,直到被其中一個對象所消費;

那麼由此可知事件分發總共能夠分爲三個部分;

  • Activity的事件分發;
  • ViewGroup的事件分發;
  • View的事件分發;

接下來先來看一下事件分發機制的核心方法,主要有三個;

  • dispatchTouchEvent():傳遞事件,當前對象能夠將事件經過這個方法傳遞給下一個對象;
  • onInterceptTouchEvent():攔截事件;當前對象經過攔截事件,來終止事件的傳遞;
  • onTouchEvent():處理事件,事件的最終去處;

下面咱們經過Demo來看看事件是怎麼傳遞的?

寫了一個簡單的佈局,一個RelativeLayout裏面放一個按鈕;

接下來點擊屏幕,看看流程會怎麼走;

1,Activity的事件分發

step1:當點擊屏幕的時候,會產出一個ACTION_DOWN的事件,傳遞到了Activity的dispatchTouchEvent方法裏,來看一下Activity的dispatchTouchEvent方法,這裏調用了super.dispatchTouchEvent(ev),也就是走了父類的dispatchTouchEvent方法;

step2:進入Activity的dispatchTouchEvent方法裏面,看一下作了啥;

這裏面有三個方法,第一個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 activitys {@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() {
    }
複製代碼

將註釋翻譯過來的意思就是:
每當Key,Touch,Trackball事件分發到當前Activity就會被調用。若是你想當你的Activity在運行的時候,可以得知用戶正在與你的設備交互,你能夠override該方法。

這個回調方法和onUserLeaveHint是爲了幫助Activities智能的管理狀態欄Notification;特別是爲了幫助Activities在恰當的時間取消Notification。

全部Activity的onUserLeaveHint 回調都會伴隨着onUserInteraction。這保證當用戶相關的的操做都會被通知到,例以下拉下通知欄並點擊其中的條目。 這個方法不是重點,不須要過多關注;

須要關注的是第二個方法getWindow().superDispatchTouchEvent(ev),這個方法最終走的是PhoneWindow的superDispatchTouchEvent();

step3:這個mDecor是DecorView,看看DecorView裏的superDispatchTouchEvent(ev)方法作了啥?

這裏面仍是調的super,走的父類的方法;

最終走的是ViewGroup的dispatchTouchEvent()方法;在這個方法裏面經過遍歷當前全部的子View,經過子View的dispatchTouchEvent()方法將事件傳遞下去;ViewGroup的事件分發請看下面的分析;

到這裏Acitivity事件就已經傳遞到ViewGroup了,若是後續的對象都沒有處理該事件,即getWindow().superDispatchTouchEvent(ev)方法返回false時,Activity就會經過onTouchEvent()把當前的事件處理掉;

看一下Activity的onTouchEvent()裏面作了啥?

public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }

        return false;
    }
    
// Window裏面的方法;
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
    final boolean isOutside =
            event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(context, event)
            || event.getAction() == MotionEvent.ACTION_OUTSIDE;
    if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
        return true;
    }
    return false;
}
複製代碼

Activity的onTouchEvent()會判斷當前的事件是否在屏幕的邊緣觸發的,若是是,則返回true,不然返回false;

總結爲流程圖:

2,ViewGroup的事件分發

接下來咱們來分析一下ViewGroup的事件分發;

public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
    // step1;
    if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }
    }
    ...
    for (int i = childrenCount - 1; i >= 0; i--) {
        ...
        // step2;
        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)){
            ...
        }
        ...
    }
}

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
            ...
            if (child == null) {
                    handled = super.dispatchTouchEvent(event);
                } else {
                    final float offsetX = mScrollX - child.mLeft;
                    final float offsetY = mScrollY - child.mTop;
                    event.offsetLocation(offsetX, offsetY);
                    // 將當前的事件分發下去;
                    handled = child.dispatchTouchEvent(event);

                    event.offsetLocation(-offsetX, -offsetY);
                }
                ...
}
複製代碼

step1:在ViewGroup的dispatchTouchEvent()方法裏面,在進行事件分發以前,會先調用onInterceptTouchEvent(ev)方法,用於判斷當前的事件是否攔截,若是被攔截了,則事件不分發給子類了,若是沒有攔截則繼續分發下去;

這裏須要注意的是,當事件爲MotionEvent.ACTION_DOWN,纔會走進onInterceptTouchEvent(ev)方法;

在走這個onInterceptTouchEvent(ev)方法以前,還有一個判斷條件,disallowIntercept,這個條件是用來判斷是否要禁用攔截事件,若是禁用了,則不會調用攔截的方法了;子類能夠經過調用requestDisallowInterceptTouchEvent()方法修改;

若是ViewGroup的子類若是沒有重寫onInterceptTouchEvent(ev)這個方法,那麼就會走ViewGroup的方法,這裏用了4個判斷條件,可是默認都是走的false,不攔截事件;

step2:若是事件沒有被攔截,那麼就會遍歷當前全部的子View,而後調用子View的dispatchTouchEvent()方法,將事件分發下去;

那若是被攔截了,則會走super.dispatchTouchEvent(event)方法,也就是View的dispatchTouchEvent(event)方法;這個邏輯寫在dispatchTransformedTouchEvent()方法裏;

到這裏ViewGroup的分發就講完了,至於ViewGroup攔截事件後,怎麼處理事件,請看下面的View事件分析;

流程圖:

3,View的事件分發

View的事件分發也是調用的dispatchTouchEvent(event)方法,讓咱們來看一下這個方法的邏輯;

public boolean dispatchTouchEvent(MotionEvent event) {
        
       ...
        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                    // step1
                result = true;
            }

            if (!result && onTouchEvent(event)) {
            // step2
                result = true;
            }
        }

       ...

        return result;
    }
複製代碼

經過源碼發現,當事件分發到了View的dispatchTouchEvent(event)後,事件就不會再繼續分發下去了;那麼這裏面的邏輯是怎樣的呢?

step1:先判斷當前View的狀態是可響應的((mViewFlags & ENABLED_MASK) == ENABLED),再判斷觸摸監聽mOnTouchListener的onTouch()的返回值,若是子類實現了OnTouchListener這個監聽,而且返回了true,那麼dispatchTouchEvent(event)就會返回true,表示當前View已經處理該事件;

step2:判斷當step1的狀態爲false時,則調用了onTouchEvent(event)來判斷子類是否返回true,返回true則表示當前View已經處理該事件;

看一下onTouchEvent(event)的源碼:

public boolean onTouchEvent(MotionEvent event) {
        
        // 判斷當前狀態是不是可點擊的
        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

        ...

        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    ...

                    performClickInternal();
                    break;

                case MotionEvent.ACTION_DOWN:
                    ...
                    checkForLongClick(0, x, y);
                    break;

               ...
            }

            return true;
        }

        return false;
    }
複製代碼

這裏須要關注的是MotionEvent.ACTION_DOWN和MotionEvent.ACTION_UP事件;

  • MotionEvent.ACTION_UP:調用了performClickInternal()觸發了點擊監聽的回調onClick(),這個是咱們最經常使用的點擊事件回調;具體是在performClick()方法裏面實現的;

  • MotionEvent.ACTION_DOWN:在這個判斷裏面,調用了checkForLongClick(0, x, y)觸發了長按監聽的回調,也就是onLongClick()方法;

經過判斷當前的視圖是否處於按壓狀態,且判斷此視圖添加的窗口數量是否和原始的一致,若是這兩種狀態都知足,就會觸發長按監聽回調;最終調用是在performLongClickInternal()方法裏面;

流程圖:

4. 總結

到這裏,事件分發的流程就已經講完了;

讓咱們來回憶一下上面提到的三個方法:

  • dispatchTouchEvent(event):將事件傳遞給下一層,當傳遞到View這一層的時候,就不會再繼續往下傳了;
  • onInterceptTouchEvent(ev):將事件攔截下來,只有ViewGroup有這個方法,當攔截後,就會走View的dispatchTouchEvent(event)方法來處理事件;
  • onTouchEvent(event):處理事件,在Activity層時,只有觸摸邊界的時候纔會處理事件,在ViewGroup和View層時,會先判斷是否有touch監聽,沒有的話,纔會觸發這個方法去處理事件;

分析到這裏,關於上面腦洞一的設計,這種分發機制是否是完美的解決了交互的問題;
不管你視圖重疊多少,事件都會一層層的傳遞過去,直到被某一層處理掉;有了這個機制,Android的界面就變的更靈活,更有創造性了;

看一下彙總的流程圖:

關於自定義View相關的文章,以前也總結了幾篇,感興趣的能夠看一下;

參考&感謝

關於我

兄dei,若是個人文章對你有幫助的話,請給我點個❤️,也能夠關注一下個人Github博客;

相關文章
相關標籤/搜索