Android控件系統(七)——ViewGroup觸摸事件分發


Android版本:7.0(API27)android

[TOC]bash


Activity對點擊事件的分發

  當一個點擊操做發生時,事件最早傳遞給當前的Activity,由Activity的dispatchTouchEvent來進行事件分發,具體的工做由Activity內部的Window來完成。Window會將事件傳遞給decor view,decor view通常就是當前界面的底層容器(即setContentView所設置的View的父容器)。咱們先從Activity的dispatchTouchEvent分析。app

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

  首先事件開始交給Activity所附屬的Window進行分發,若是返回true,整個事件循環就結束了,返回false意味着事件沒人處理,全部View的onTouchEvent都返回fasle,那麼Activity的onTouchEvent就會被調用。ide

  接下來看Window是如何將事件傳遞給ViewGroup的。經過源碼咱們知道,Window是個抽象類,而Window的superDispatchTouchEvent方法也是個抽象方法,所以咱們必須找到Window的實現類才行。源碼分析

Window.superDispatchTouchEvent動畫

public abstract boolean superDispatchTouchEvent(MotionEvent event);
複製代碼

那麼到底Window的實現類是什麼?實際上是PhoneWindow,這一點從Window的源碼中也能夠看出來,在Window的說明中,有這麼一段話:ui

/**
 * Abstract base class for a top-level window look and behavior policy.  An
 * instance of this class should be used as the top-level view added to the
 * window manager. It provides standard UI policies such as a background, title
 * area, default key processing, etc.
 *
 * <p>The only existing implementation of this abstract class is
 * android.view.PhoneWindow, which you should instantiate when needing a
 * Window.
 */
public abstract class Window
複製代碼

PhoneWindow.superDispatchTouchEventthis

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}
複製代碼

PhoneWindow將事件直接傳遞給了DecorView,這個DecorView是什麼呢?請看下面:spa

// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;

@Override
public final View getDecorView() {
    if (mDecor == null) {
        installDecor();
    }
    return mDecor;
}
複製代碼

咱們知道,經過((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0)這種方式就能夠獲取Activity所設置的View,這個mDecor顯然就是getWindow().getDecorView()返回的View,而咱們經過setContentView設置的View是它的一個子View。目前事件傳遞到了DecorView這裏,猶豫DecorView繼承自FrameLayout且是父View,因此最終事件傳遞給View。換句話說,事件確定會傳遞到View,否則應用如何響應點擊事件?因此,重點就到了ViewGroup中事件分發機制了。rest

ViewGroup事件分發源碼解析

基礎知識:

  • 因爲一個完整的事件序列是以down開始,以up結束;
  • 哪個view消費了down事件,那麼後續事件都將交給它處理(若是它的父view不攔截事件);

事件分發會調用ViewGroup.dispatchTouchEvent方法,該方法的核心是按照以下步驟分析:

  • down事件的分發;
    (1)ViewGroup是否攔截;
    (2)ViewGroup中是否有子View消費down事件;
  • 非down事件分發;

重要的字段

該方法中有兩個重要的字段:
intercepted:ViewGroup是否攔截事件,true表示攔截;
mFirstTouchTarget:是否有子View消費了down事件,若是有則mFirstTouchTarget != null;

ACTION_DOWN事件初始化

// Handle an initial 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();
}
複製代碼

  首先這裏先判斷事件是否爲DOWN事件,若是是,則初始化,resetTouchState方法中把mFirstTouchTarget置爲null。因爲一個完整的事件序列是以DOWN開始,以UP結束,因此若是是DOWN事件,那麼說明是一個新的事件序列,因此須要初始化以前的狀態。
  這裏的mFirstTouchTarget很是重要,後面會說到當ViewGroup的子元素成功處理事件的時候,mFirstTouchTarget會指向子元素,這裏要留意一下。

檢查ViewGroup是否要攔截事件

// Check for interception.
final boolean intercepted;
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;
}
複製代碼

經過源碼分析,在以下兩種狀況下會調用ViewGroup的nInterceptTouchEvent檢查ViewGroup是否攔截事件:

  • 事件序列的開始事件ACTION_DOWN;
  • 子View消耗了ACTION_DOWN事件,非DOWN的事件都會通過ViewGroup的nInterceptTouchEvent,看看ViewGroup是否須要攔截;

若是ViewGroup攔截了事件則intercepted = true。

注:此處還有一個FLAG_DISALLOW_INTERCEPT屬性,爲了流程清晰,咱們暫且不分析,後續再說明。

ViewGroup不攔截事件

若是ViewGroup不攔截事件且事件是ACTION_DOWN事件,就會執行這個雙if語句中:

if (!canceled && !intercepted) {

    // If the event is targeting accessiiblity focus we give it to the
    // view that has accessibility focus and if it does not handle it
    // we clear the flag and dispatch the event to all children as usual.
    // We are looking up the accessibility focused host to avoid keeping
    // state since these events are very rare.
    View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
            ? findChildWithAccessibilityFocus() : null;

    if (actionMasked == MotionEvent.ACTION_DOWN
            || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
            
                
            }
                        
}
複製代碼

這個雙if語句的核心做用是:將ACTION_DOWN分發到本身的子View中,找到消費DOWN事件的子View,並賦值給mFirstTouchTarget;若是沒有子View處理down事件那麼mFirstTouchTarget == null。

final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
    final float x = ev.getX(actionIndex);
    final float y = ev.getY(actionIndex);
    // Find a child that can receive the event.
    // Scan children from front to back.
    final ArrayList<View> preorderedList = buildOrderedChildList();
    final boolean customOrder = preorderedList == null
            && isChildrenDrawingOrderEnabled();
    final View[] children = mChildren;
    for (int i = childrenCount - 1; i >= 0; i--) { // ------------------------------------1
        final int childIndex = customOrder
                ? getChildDrawingOrder(childrenCount, i) : i;
        final View child = (preorderedList == null)
                ? children[childIndex] : preorderedList.get(childIndex);

        // If there is a view that has accessibility focus we want it
        // to get the event first and if not handled we will perform a
        // normal dispatch. We may do a double iteration but this is
        // safer given the timeframe.
        if (childWithAccessibilityFocus != null) {
            if (childWithAccessibilityFocus != child) {
                continue;
            }
            childWithAccessibilityFocus = null;
            i = childrenCount - 1;
        }

        if (!canViewReceivePointerEvents(child)
                || !isTransformedTouchPointInView(x, y, child, null)) {// -----------------------2
            ev.setTargetAccessibilityFocus(false);
            continue;
        }

        newTouchTarget = getTouchTarget(child);
        if (newTouchTarget != null) {
            // Child is already receiving touch within its bounds.
            // Give it the new pointer in addition to the ones it is handling.
            newTouchTarget.pointerIdBits |= idBitsToAssign;
            break;
        }

        resetCancelNextUpFlag(child);
        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {// -----------------------3
            // Child wants to receive touch within its bounds.
            mLastTouchDownTime = ev.getDownTime();
            if (preorderedList != null) {
                // childIndex points into presorted list, find original index
                for (int j = 0; j < childrenCount; j++) {
                    if (children[childIndex] == mChildren[j]) {
                        mLastTouchDownIndex = j;
                        break;
                    }
                }
            } else {
                mLastTouchDownIndex = childIndex;
            }
            mLastTouchDownX = ev.getX();
            mLastTouchDownY = ev.getY();
            newTouchTarget = addTouchTarget(child, idBitsToAssign);
            alreadyDispatchedToNewTouchTarget = true;
            break;
        }

        // The accessibility focus didn't handle the event, so clear // the flag and do a normal dispatch to all children. ev.setTargetAccessibilityFocus(false); } if (preorderedList != null) preorderedList.clear(); } 複製代碼

  這裏獲取了childrenCount的值,表示該ViewGroup內部有多少個子View,若是有子View就開始遍歷全部子View判斷是否要把事件分發給子View。
  代碼也比較長,咱們只關注重點部分:

  • 代碼標記1
    是一個for循環,這裏表示對全部的子View進行循環遍歷,因爲以上判斷了ViewGroup不對事件進行攔截,那麼在這裏就要對ViewGroup內部的子View進行遍歷,一個個地找到能接受事件的子View,這裏注意到它是倒序遍歷的,即從最上層的子View開始往內層遍歷,這也符合咱們日常的習慣,由於通常來講咱們對屏幕的觸摸,確定是但願最上層的View來響應的,而不是被覆蓋這的底層的View來響應,不然這有悖於生活體驗。

  • 代碼標記2
    根據方法名字咱們得知這個判斷語句是判斷觸摸點位置是否在子View的範圍內或者子View是否在播放動畫,若是均不符合則continue,表示這個子View不符合條件,開始遍歷下一個子View

  • 代碼標記3
    這裏調用了dispatchTransformedTouchEvent()方法,這個方法有什麼用呢?

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
    final boolean handled;

    // Perform any necessary transformations and dispatch.
    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        final float offsetX = mScrollX - child.mLeft;
        final float offsetY = mScrollY - child.mTop;
        transformedEvent.offsetLocation(offsetX, offsetY);
        if (! child.hasIdentityMatrix()) {
            transformedEvent.transform(child.getInverseMatrix());
        }

        handled = child.dispatchTouchEvent(transformedEvent);
    }

    // Done.
    transformedEvent.recycle();
    return handled;
}
複製代碼

當child != null時,將down事件傳遞到子view的dispatchTouchEvent;不然由ViewGroup的父類dispatchTouchEvent處理。 前面的分析條件是有子View的狀況,全部child != null,down事件傳遞到子view的dispatchTouchEvent處理。

若是子View的onTouchEvent()返回true,那麼就是消耗了Down事件,接着會調用addTouchTarget方法:

private TouchTarget addTouchTarget(View child, int pointerIdBits) {
    TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;
}
複製代碼

該方法中對mFirstTouchTarget = target進行了賦值,這也證明了前面所說的 「若是子View消耗了事件,那麼mFirstTouchTarget不爲null」。

小結:
整一個if(!canceled && !intercepted){ … }代碼塊所作的工做就是對ACTION_DOWN事件的特殊處理。由於ACTION_DOWN事件是一個事件序列的開始,因此咱們要先找到可以處理這個事件序列的一個子View,若是一個子View可以消耗事件,那麼mFirstTouchTarget會指向子View,若是全部的子View都不能消耗事件,那麼mFirstTouchTarget將爲null。

分發非ACTION_DOWN以外的其它事件

上面代碼的整個過程目的只有一個,找到消費down事件的子View,若是找到mFirstTouchTarget != null。下面就是根據mFirstTouchTarget對move、up事件進行分發。

// Dispatch to touch targets.
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;
            }
            if (cancelChild) {
                if (predecessor == null) {
                    mFirstTouchTarget = next;
                } else {
                    predecessor.next = next;
                }
                target.recycle();
                target = next;
                continue;
            }
        }
        predecessor = target;
        target = next;
    }
}
複製代碼

(1)mFirstTouchTarget == null 說明沒有子View消耗down事件,那麼後續全部事件都交給ViewGroup的super.dispatchTouchEvent去處理;
(2)mFirstTouchTarget != null

if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) 
複製代碼

這裏的判斷就是區分了ACTION_DOWN事件和別的事件,由於在上面的分析咱們知道,若是子View消耗了ACTION_DOWN事件,那麼alreadyDispatchedToNewTouchTarget和newTouchTarget已經有值了,因此就直接置handled爲true並返回;那麼若是alreadyDispatchedToNewTouchTarget和newTouchTarget值爲null,那麼就不是ACTION_DOWN事件,便是ACTION_MOVE、ACTION_UP等別的事件,這些事件都會傳遞給消耗down事件的子View即mFirstTouchTarget。

FLAG_DISALLOW_INTERCEPT設置

FLAG_DISALLOW_INTERCEPT標誌位是經過requestDisallowInterceptTouchEvent方法來設置

public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
        // We're already in this state, assume our ancestors are too return; } if (disallowIntercept) { mGroupFlags |= FLAG_DISALLOW_INTERCEPT; } else { mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; } // Pass it up to our parent if (mParent != null) { mParent.requestDisallowInterceptTouchEvent(disallowIntercept); } } 複製代碼

一旦設置了FLAG_DISALLOW_INTERCEPT爲true,那麼ViewGroup就不能經過onInterceptTouchEvent返回true來攔截任何事件了。這一點在分析"檢查ViewGroup是否要攔截事件"中能夠看到。

相關文章
相關標籤/搜索