Android中的事件分發機制——ViewGroup的事件分發

來源:互聯網
上載者:User

標籤:

綜述

  Android中的事件分發機制也就是View與ViewGroup的對事件的分發與處理。在ViewGroup的內部包含了許多View,而ViewGroup繼承自View,所以ViewGroup本身也是一個View。對於事件可以通過ViewGroup下發到它的子View並交由子View進行處理,而ViewGroup本身也能夠對事件做出處理。下面就來詳細分析一下ViewGroup對時間的分發處理。

MotionEvent

  當手指接觸到螢幕以後,所產生的一系列的事件中,都是由以下三種事件類型組成。
  1. ACTION_DOWN: 手指按下螢幕
  2. ACTION_MOVE: 手指在螢幕上移動
  3. ACTION_UP: 手指從螢幕上抬起
  例如一個簡單的螢幕觸摸動作觸發了一系列Touch事件:ACTION_DOWN->ACTION_MOVE->…->ACTION_MOVE->ACTION_UP
  對於Android中的這個事件分發機制,其中的這個事件指的就是MotionEvent。而View的對事件的分發也是對MotionEvent的分發操作。可以通過getRawX和getRawY來擷取事件相對於螢幕左上方的橫縱座標。通過getX()和getY()來擷取事件相對於當前View左上方的橫縱座標。

三個重要方法

public boolean dispatchTouchEvent(MotionEvent ev)

  這是一個對事件分發的方法。如果一個事件傳遞給了當前的View,那麼當前View一定會調用該方法。對於dispatchTouchEvent的傳回型別是boolean類型的,返回結果表示是否消耗了這個事件,如果返回的是true,就表明了這個View已經被消耗,不會再繼續向下傳遞。  
  
public boolean onInterceptTouchEvent(MotionEvent ev)

  該方法存在於ViewGroup類中,對於View類並無此方法。表示是否攔截某個事件,ViewGroup如果成功攔截某個事件,那麼這個事件就不在向下進行傳遞。對於同一個事件序列當中,當前View若是成功攔截該事件,那麼對於後面的一系列事件不會再次調用該方法。返回的結果表示是否攔截當前事件,預設返回false。由於一個View它已經處於最底層,它不會存在子控制項,所以無該方法。
  
public boolean onTouchEvent(MotionEvent event)

  這個方法被dispatchTouchEvent調用,用來處理事件,對於返回的結果用來表示是否消耗掉當前事件。如果不消耗當前事件的話,那麼對於在同一個事件序列當中,當前View就不會再次接收到事件。
  

View事件分發流程圖

  對於事件的分發,在這裡先通過一個流程圖來看一下整個分發過程。

ViewGroup事件分發源碼分析

  根據上面的流程圖現在就詳細的來分析一下ViewGroup事件分發的整個過程。
  手指在觸控螢幕上滑動所產生的一系列事件,當Activity接收到這些事件通過調用Activity的dispatchTouchEvent方法來進行對事件的分發操作。下面就來看一下Activity的dispatchTouchEvent方法。

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

  通過getWindow().superDispatchTouchEvent(ev)這個方法可以看出來,這個時候Activity又會將事件交由Window處理。Window它是一個抽象類別,它的具體實現只有一個PhoneWindow,也就是說這個時候,Activity將事件交由PhoneWindow中的superDispatchTouchEvent方法。現在跟蹤進去看一下這個superDispatchTouchEvent代碼。

public boolean superDispatchTouchEvent(MotionEvent event) {    return mDecor.superDispatchTouchEvent(event);}

  這裡面的mDecor它是一個DecorView,DecorView它是一個Activity的頂級View。它是PhoneWindow的一個內部類,繼承自FrameLayout。於是在這個時候事件又交由DecorView的superDispatchTouchEvent方法來處理。下面就來看一下這個superDispatchTouchEvent方法。

public boolean superDispatchTouchEvent(MotionEvent event) {    return super.dispatchTouchEvent(event);}

  在這個時候就能夠很清晰的看到DecorView它調用了父類的dispatchTouchEvent方法。在上面說到DecorView它繼承了FrameLayout,而這個FrameLayout又繼承自ViewGroup。所以在這個時候事件就開始交給了ViewGroup進行處理了。下面就開始詳細看下這個ViewGroup的dispatchTouchEvent方法。由於dispatchTouchEvent代碼比較長,在這裡就摘取部分代碼進行說明。

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

  從上面代碼可以看出,在dispatchTouchEvent中,會對接收的事件進行判斷,當接收到的是ACTION_DOWN事件時,便會清空事件分發的目標和狀態。然後執行resetTouchState方法重設了觸摸狀態。下面就來看一下這兩個方法。
  1. cancelAndClearTouchTargets(ev)

private TouchTarget mFirstTouchTarget;......private void cancelAndClearTouchTargets(MotionEvent event) {    if (mFirstTouchTarget != null) {        boolean syntheticEvent = false;        if (event == null) {            final long now = SystemClock.uptimeMillis();            event = MotionEvent.obtain(now, now,                    MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);            event.setSource(InputDevice.SOURCE_TOUCHSCREEN);            syntheticEvent = true;        }        for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {            resetCancelNextUpFlag(target.child);            dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);        }        clearTouchTargets();        if (syntheticEvent) {            event.recycle();        }    }}

  在這裡先介紹一下mFirstTouchTarget,它是TouchTarget對象,TouchTarget是ViewGroup的一個內部類,TouchTarget採用鏈表資料結構進行儲存View。而在這個方法中主要的作用就是清空mFirstTouchTarget鏈表並將mFirstTouchTarget設為空白。
  2. resetTouchState()

private void resetTouchState() {    clearTouchTargets();    resetCancelNextUpFlag(this);    mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;    mNestedScrollAxes = SCROLL_AXIS_NONE;}

  在這裡介紹一下FLAG_DISALLOW_INTERCEPT標記,這是禁止ViewGroup攔截事件的標記,可以通過requestDisallowInterceptTouchEvent方法來設定這個標記,當設定了這個標記以後,ViewGroup便無法攔截除了ACTION_DOWN以外的其它事件。因為在上面代碼中可以看出,當事件為ACTION_DOWN時,會重設FLAG_DISALLOW_INTERCEPT標記。
  那麼下面就再次回到dispatchTouchEvent方法中繼續看它的原始碼。

// 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對事件是否需要攔截進行的判斷。下面先對mFirstTouchTarget是否為null這兩種情況進行說明。當事件沒有被攔截時,ViewGroup的子項目成功處理事件後,mFirstTouchTarget會被賦值並且指向其子項目。也就是說這個時候mFirstTouchTarget!=null。可是一旦事件被攔截,mFirstTouchTarget不會被賦值,mFirstTouchTarget也就為null。
  在上面代碼中可以看到根據actionMasked==MotionEvent.ACTION_DOWN||mFirstTouchTarget!=null這兩個情況進行判斷事件是否需要攔截。對於actionMasked==MotionEvent.ACTION_DOWN這個條件很好理解,對於mFirstTouchTarget!=null的兩種情況上面已經說明。那麼對於一個事件序列,當事件為MotionEvent.ACTION_DOWN時,會重設FLAG_DISALLOW_INTERCEPT,也就是說!disallowIntercept一定為true,必然會執行onInterceptTouchEvent方法,對於onInterceptTouchEvent方法預設返回為false,所以需要ViewGroup攔截事件時,必須重寫onInterceptTouchEvent方法,並返回true。這裡有一點需要注意,對於一個事件序列,一旦序列中的某一個事件被成功攔截,執行了onInterceptTouchEvent方法,也就是說onInterceptTouchEvent傳回值為true,那麼該事件之後一系列事件對於條件actionMasked==MotionEvent.ACTION_DOWN||mFirstTouchTarget!=null必然為false,那麼這個時候該事件序列剩下的一系列事件將會被攔截,並且不會執行onInterceptTouchEvent方法。於是在這裡得出一個結論:對於一個事件序列,當其中某一個事件成功攔截時,那麼對於剩下的一些列事件也會被攔截,並且不會再次執行onInterceptTouchEvent方法
  下面再來看一下對於ViewGroup並沒有攔截事件是如何進行處理的。

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

  對於這段代碼雖然說比較長,但是在這裡面的邏輯去不是很複雜。首先擷取當前ViewGroup中的子View和ViewGroup的數量。然後對該ViewGroup中的元素進行逐步遍曆。在擷取到ViewGroup中的子項目後,判斷該元素是否能夠接收觸摸事件。子項目若是能夠接收觸摸事件,並且該觸摸座標在子項目的可視範圍內的話,便繼續向下執行。否則就continue。對于衡量子項目能否接收到觸摸事件的標準有兩個:子項目是否在播放動畫和點擊事件的座標是否在子項目的地區內。
  一旦子View接收到了觸摸事件,然後便開始調用dispatchTransformedTouchEvent方法對事件進行分發處理。對於dispatchTransformedTouchEvent方法代碼比較多,現在只關注下面這五行代碼。從下面5行代碼中可以看出,這時候會調用子View的dispatchTouchEvent,也就是在這個時候ViewGroup已經完成了事件分發的整個過程。

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

  當子項目的dispatchTouchEvent返回為true的時候,也就是子View對事件處理成功。這時候便會通過addTouchTarget方法對mFirstTouchTarget進行賦值。
  如果dispatchTouchEvent返回了false,或者說當前的ViewGroup沒有子項目的話,那麼這個時候便會調用如下代碼。

if (mFirstTouchTarget == null) {    // No touch targets so treat this as an ordinary view.    handled = dispatchTransformedTouchEvent(ev, canceled, null,            TouchTarget.ALL_POINTER_IDS);}

  在這裡調用dispatchTransformedTouchEvent方法,並將child參數設為null。也就是執行了super.dispatchTouchEvent(event)方法。由於ViewGroup繼承自View,所以這個時候又將事件交由自己處理。
  到這裡對於ViewGroup的事件分發已經講完了,在這一路下來,不難發現對於dispatchTouchEvent有一個boolean類型傳回值。對於這個傳回值,當返回true的時候表示當前事件處理成功,若是返回false,一般來說是因為在事件處理onTouchEvent返回了false,這時候變會交由它的父控制項進行處理,最終會交由Activity的onTouchEvent方法進行處理。

總結

  在這裡從宏觀上再看一下這個ViewGroup對事件的分發,當ViewGroup接收一個事件序列以後,首先會判斷是否攔截該事件,若是攔截該事件,則將這個事件交由自己處理。若是不去攔截這一事件,便將該事件下發到子View當中。若果說ViewGroup沒有子View,或者說子View對事件處理失敗,則將該事件有交由該ViewGroup處理,若是該ViewGroup對事件依然處理失敗,最終則會將事件交由Activity進行處理。

Android中的事件分發機制——ViewGroup的事件分發

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.