View事件分發機制分析

View 事件分發是很重要的知識點,只有理解其中的原理 在寫代碼過程當中更精準的處理代碼邏輯,控制好 api 的調用時機。本文經過閱讀SDK 28的源碼,在這裏作一次輸出,深刻理解下。android

目錄

1、實例引伸編程

2、事件分發原理api

    1. Activity
    1. ViewGroup
    1. View

3、總結bash

1、實例引伸

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        (Button)findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("MainActivity", "click btn");
            }
        });
    }
}
複製代碼
# activity_main.xml

<RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <Button
            android:id="@+id/btn"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>
</RelativeLayout>
複製代碼

以上是最簡單的點擊按鈕點擊事件,對咱們應用層開發來說就是點擊了一個Button,而後回調到了 listener 中的onClick 方法,但其背後的原理要從觸摸到屏幕開始講起。app

2、事件分發原理

1. Activity

觸摸事件首先會達到 Activity 中的 dispatchTouchEvent 方法內,若是你問我觸摸屏幕後是怎麼到達 Activity 的,這個問題 I don't know!也並非本文談論的範圍。ide

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}
複製代碼

這裏有必要解釋一下 MotionEvent 這個對象,這是觸摸事件發生後,系統將觸摸事件動做(ACTION_DOWN、ACTION_MOVE、ACTION_UP、ACTION_CANCEL)、觸摸座標點、多指觸控等信息保存到此對象中,以便傳輸時操做。而咱們觸摸屏幕時是一序列的事件,會有按壓而後不停的移動,最後會擡起,這些動做和座標點都是會變化的,也就是說會產生down + 不少 move + up/cancel 事件,多指觸控比較複雜不在本文討論範圍內。post

onUserInteraction 方法是 Activity 內的一個空實現,若是想在觸摸屏幕的最初期作一些操做,能夠重寫此方法。對於 View 事件分發必需要有一個「消費」的概念,觸摸事件究竟是在哪一步、哪個組件裏被消費了。在這裏,若 getWindow().superDispatchTouchEvent(ev) 返回 true 表明事件被某個組件消費了,此時直接返回 true 結束,若是事件沒被消費,那麼就繼續走到 onTouchEvent 方法,Activity 的 onTouchEvent 基本上都會返回 false, 表示沒有消費。學習

直接跟到 getWindow().superDispatchTouchEvent(ev) 方法,在 Android 系統中 Window 抽象類惟一的實現類就是 PhoneWindow, 而 PhoneWindow 內部調用了 DecorView.superDispatchTouchEvent(event), 此方法內又調用了 super.dispatchTouchEvent(event), 也就是調到 ViewGroup 的 dispatchTouchEvent 方法。ui

ps: DecorView 就是全部一個頁面(也就是setContentView後)的最頂層View。this

至此觸摸事件從 Activity 傳遞到了 ViewGroup 中,這裏把 Window 和 DecorView 的調用過程都寫在 Activity 範疇內,由於這個流程是很簡單的,不必分開。

下圖是Activity事件分發調用流程圖解:

Activity事件分發

2. ViewGroup

ViewGroup 中有三個關鍵方法:

  • dispatchTouchEvent 用於觸摸事件一開始傳遞到 ViewGroup 時調用,
  • onInterceptTouchEvent 用於攔截觸摸事件,決定是否本身來消費事件。
  • onTouchEvent 用於消費觸摸事件。

看源碼有些細節是真的看不懂,可是那些細節又不是特別重要,那麼就略過好了。。只看重要的調用流程。 因爲 dispatchTouchEvent 方法內容不少,所以分幾塊去看。首先是 ViewGroup 是否須要攔截的部分。

public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
    
    final int action = ev.getAction();
    final int actionMasked = action & MotionEvent.ACTION_MASK;

    // Handle an initial down.
    // 當一個ACTION_DOWN事件來的時候表明用戶剛點擊了屏幕,
    // 此時將以前的觸摸狀態、手勢都還原,從新開始一個事件序列。
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        // Throw away all previous state when starting a new touch gesture.
        // The framework may have dropped the up or cancel event for the previous gesture
        // due to an app switch, ANR, or some other state change.
        cancelAndClearTouchTargets(ev);
        resetTouchState();
    }

    // Check for interception.
    // 此標誌位表明本身是否要攔截這個事件
    final boolean intercepted;
    if (actionMasked == MotionEvent.ACTION_DOWN
            || mFirstTouchTarget != null) {
        // 經過 mGroupFlags 標誌位獲得是否容許我這個ViewGroup攔截事件
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        if (!disallowIntercept) {
            // 基本上onInterceptTouchEvent都會返回false,表明不攔截,
            // 除非自定義ViewGroup,重寫此方法是解決滑動衝突的重要手段
            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;
    }
    
    ...
}
複製代碼

上述一段源碼作了一些註釋,解釋了其流程的邏輯。這裏有幾個重要變量須要解釋的。

mFirstTouchTarget:此對象是一個單鏈表結構,存儲這一系列的事件(ACTION_DOWN、ACTION_MOVE...、ACTION_UP)發生時所涉及到的子View,所以觸摸事件 ACTION_DOWN 發生後若是這個對象仍是爲null,那麼就表示 ViewGroup 沒有將事件傳遞到子View。

mGroupFlags:mGroupFlags 能夠理解爲不少個標誌位的組合。mGroupFlags & FLAG_DISALLOW_INTERCEPT != 0 表示這個標誌位組合內有「不容許攔截事件」這個標誌位(相似於Map中找一個Key是否存在)。對於位運算本人一直很疑惑,雖然說這些不必定都須要看懂,可是這些判斷邏輯的標誌位看不懂就很難受。。反正在看位運算的時候千萬不要按一向的邏輯在腦海裏把數值轉換成十進制的,就用二進制去理解,這裏推薦一篇位操做文章。

...
    
    final View[] children = mChildren;
    for (int i = childrenCount - 1; i >= 0; i--) {
        ...
        
        if (!canViewReceivePointerEvents(child)
            || !isTransformedTouchPointInView(x, y, child, null)) {
            ev.setTargetAccessibilityFocus(false);
            continue;
        }
        
        ...

        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
            // Child wants to receive touch within its bounds.
            // if 代碼塊內主要保存了一些變量,設置標誌位
            // 記錄找到的子View,以便以後的事件序列能夠直接使用目標View
            ...
            break;
        }
    }
    
    ...
}
複製代碼

這裏省略了不少雜七雜八的代碼,關鍵仍是在於遍歷 ViewGroup 的全部子 View, 經過 isTransformedTouchPointInView 方法找到點擊時座標落在哪一個子 View 上,跟進 dispatchTransformedTouchEvent 看看:

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    ...
    
    final boolean handled;
    // If the number of pointers is the same and we don't need to perform any fancy // irreversible transformations, then we can reuse the motion event for this // dispatch as long as we are careful to revert any changes we make. // Otherwise we need to make a copy. final MotionEvent transformedEvent; if (newPointerIdBits == oldPointerIdBits) { if (child == null || child.hasIdentityMatrix()) { if (child == null) { // View來處理事件 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); } return handled; } transformedEvent = MotionEvent.obtain(event); } else { transformedEvent = event.split(newPointerIdBits); } ... return handled; } 複製代碼

這裏的英文註釋解釋的很清晰,這個方法的主要做用就是將觸摸事件轉換成子View相對父容器的座標,並過濾一些不相關的觸摸點(因爲不討論多點觸控因此沒必要糾結),若是沒有子視圖,那麼就會傳到 View 的 dispatchTouchEvent 方法(要知道 ViewGroup 就是繼承自 View)。最後返回的 handled 表明是否被處理了,也就是事件是否被消費了。

以上幾塊代碼在 ViewGroup.dispatchTouchEvent 方法中是針對 ACTION_DOWN 這個動做所作的處理,所以還須要作其餘動做的處理,其實徹底是相似的,只是操做更簡單了:

...

if (mFirstTouchTarget == null) {
    // No touch targets so treat this as an ordinary view.
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
} else {
    // Dispatch to touch targets, excluding the new touch target if we already
    // dispatched to it.  Cancel touch targets if necessary.
    TouchTarget predecessor = null;
    TouchTarget target = mFirstTouchTarget;
    while (target != null) {
        final TouchTarget next = target.next;
        if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
            handled = true;
        } else {
            final boolean cancelChild = resetCancelNextUpFlag(target.child)
                    || intercepted;
            if (dispatchTransformedTouchEvent(ev, cancelChild,
                    target.child, target.pointerIdBits)) {
                handled = true;
            }
            
            ...
        }

    }
}

...
複製代碼

代碼大體意思就是若是沒有找到對應的子 View 即 mFirstTouchTarget = null, 那麼交給 View.dispatchTouchEvent 處理;若是以前的 ACTION_DOWN 動做已經找到了子 View,那麼就繼續給它處理。

ViewGroup.onInterceptTouchEvent 方法,這個方法默認基本不作什麼事,通常會返回 false;但它是解決滑動衝突的關鍵方法,遇到滑動衝突時,須要重寫此方法。

ViewGroup 的 onTouchEvent 徹底是繼承了 View 的 onTouchEvent 方法,所以處理方式和 View 徹底相同,此方法在 View 小節分析。

ViewGroup事件分發調用流程圖解:

ViewGroup事件分發

3. View

View 中有兩個關鍵方法:

  • dispatchTouchEvent 用於觸摸事件傳遞到 View 時觸發。
  • onTouchEvent 用於消費觸摸事件。
/**
 * Pass the touch screen motion event down to the target view, or this
 * view if it is the target.
 *
 * @param event The motion event to be dispatched.
 * @return True if the event was handled by the view, false otherwise.
 */
public boolean dispatchTouchEvent(MotionEvent event) {
    ...
    
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }

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

    ...
    return result;
}
複製代碼

首先判斷 OnTouchListener 是否爲空,再判斷這個 View 是否能夠用(即setEnable屬性,默認都是true),而後調用 OnTouchListener.onTouch 方法執行咱們自定義的觸摸操做,若是此方法返回 true, 則表明事件被消費,接下來不須要執行 onTouchEvent; 若是咱們使其返回 false, 那麼能夠繼續傳遞給 onTouchEvent 去消費。跟進 View.onTouchEvent 看看:

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:
                    ...
                    
                    // 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();
                    }
                    
                    ...
        }

        return true;
    }

    return false;
}
複製代碼

其中最關鍵的部分就是咱們最經常使用的 click 事件,一些長按等事件的邏輯這裏就再也不分析。 經過屬性判斷 View 是否可點擊,而且在手指擡起時即 ACTION_UP 執行 performClick 方法,其內部就是判斷用戶是否設置了 OnClickListener 監聽器,若是有則調用 onClick 方法。

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;
    }
    return result;
}
複製代碼

View 的事件分發大體流程就是這樣了。其中處理的優先級是:

  • 若是用戶設置了 OnTouchListener, 那麼就會調用 onTouch 方法,而且若是 onTouch 方法返回true, 那麼就不會執行 onTouchEvent 了,也就不會執行 onClick 了;
  • 若是來到 onTouchEvent 方法,那麼就有機會去執行 OnClickListener.onClick 方法,除非你執行了長按之類的操做;
  • 最後回調到 onClick;
  • onTouch -> onTouchEvent -> onClick

View 事件分發調用流程圖解:

View事件分發

3、總結

事件分發

觸摸事件會通過如下幾個組件:Activity、Window、DecorView、ViewGroup、View。

  • 當用戶點擊屏幕時,觸摸事件 MotionEvent 最早傳遞到 Activity.dispatchTouchEvent 方法,而後傳遞到 PhoneWindow.superDispatchTouchEvent 方法,緊接着傳到 DecorView.dispatchTouchEvent 方法,而後直接調用了父類 ViewGroup.dispatchTouchEvent 方法。
  • 在 ViewGroup 的 dispatchTouchEvent 中主要作了如下幾件事:當 ACTION_DOWN 事件來的時候,判斷如今的 ViewGroup 是否攔截這個事件,而 onInterceptTouchEvent 方法通常返回 false; 一樣地,針對 ACTION_DOWN 事件,會遍歷一遍 ViewGroup 的全部子 View, 點擊若是落在某個子 View 上,那麼就將觸摸事件傳遞給子 View 的 dispatchTouchEvent 方法,若是沒有找到子 View 那就直接交給父類 View.dispatchTouchEvent 處理事件;當 ACTION_MOVE 或 ACTION_UP 等事件來的時候,依然會傳給子 View 或 父類 View 實現的 dispatchTouchEvent, 只是這個過程不用再攔截了,只要 down 的時候攔截了,那麼都會交由此 View 攔截,除非調用了 requestDisallowInterceptTouchEvent;
  • 最後事件會來到 View, dispatchTouchEvent 主要去找是否有 OnTouchListener 監聽,若是有則調用 onTouch 方法,並根據此方法的返回值決定是否執行 onTouchEvent 方法,onTouchEvent 方法內部會判斷是否有 OnClickListener 監聽,若是有則調用 onClick。
  • 若是事件達到了子 View,而子 View 並無去消費它,那麼這個事件會拋到上一層,若是每層的父視圖都不消費事件,那麼最後會交給 Activity 執行 onTouchEvent 方法。

事件分發的理解是經過《Android開發藝術探索》(好書) + View事件分發(好文)。

理解事件分發機制的原理後,忽然發現,源碼的設計都是很巧妙的,有些業務場景咱們也能夠採用這種從上到下委託的方式去設計代碼不是嗎?所以看源碼能提升自身的代碼質量,這點是毋庸置疑的。後來搜索了下,這就是責任鏈模式啊。。

其實源碼中的註釋很是詳細、清晰,比咱們平時接觸的業務代碼不知道清晰多少倍,但有一點讓大多數人望而卻步,那就是英語。英語對編程來講過重要了,所以本人如今已經從新開始學習英語了。。看不懂的註釋就配合着翻譯強行去看。

相關文章
相關標籤/搜索