View事件分發

NOTE: 筆記,碎片式內容

控件

App界面的開主要就是使用View,或者稱爲控件。View既繪製內容又響應輸入,輸入事件主要就是觸摸事件。html

ViewTree

控件基類爲View,而ViewGroup是其子類。ViewGroup能夠包含其它View做爲其child。任何一個ViewGroup及其全部直接或間接的child造成一個ViewTree這樣的樹結構java

RootView

顯然每個具體的ViewTree都會有一個root,它是一個ViewGroup,接下來稱它爲RootView。持有一個RootView就能夠引用此ViewTree,最終訪問到全部View。android

以Activity爲例,使用setContentView(View view)來指定要顯示的內容,不過參數view並不是是Activity最終顯示到Window的ViewTree。經過追溯源碼,最終參數view被添加到PhoneWindow.mDecor做爲其childView。mDecor是FramLayout的子類對象:編程

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

private final class DecorView extends FrameLayout {...}

可見mDecor是Activity最終顯示的ViewTree的root。安全

結構特色

ViewTree的特色有:數據結構

  • 只有一個RootView,它是ViewGroup。
  • ViewTree中的非葉子節點都是ViewGroup。
  • ViewTree中的葉子節點能夠是View或ViewGroup。
  • 一個ViewGroup能夠有0或多個直接childView。
  • 一個childView只能有一個直接ViewGroup。

直接或間接的parent和child關係是關於具體2個View而言的,而這個關係在界面中的反映就是View顯示區域的包含關係,即child老是在parent的區域內。顯示區域的包含關係和它們在ViewTree中的結構關係是對應的。app

對於組成ViewTree的全部ViewGroup和View來講,View不須要知道其所在ViewGroup,但ViewGroup知道其全部childView。框架

路徑

這裏爲ViewTree引入路徑這一律念,它表示從RootView出發找到任一child時要通過的全部View的列表。
由於一個ViewGroup只能訪問其直接child,而一個child只有惟一的parent,因此從RootView到達任一child的路徑是惟一的,反之從任一child到達RootView的路線也是惟一的。async

顯然對任何child的路徑老是存在的,雖然能夠依靠額外的數據結構來保存各個View的關係,但樹結構自己已經在作這樣的事情了。ide

View系統的底層原理

View系統是framework層提供給應用開發者的一種方便開發界面的框架,相似其它編程平臺中的控件系統那樣。
Android底層使用WindowManagerService(簡稱WMS)、Surface、InputManagerService(簡稱IMS)這些服務組件和類型來管理界面顯示和輸入事件的。這裏簡單地對View系統的顯示和輸入事件的獲取進行探索。

使用View和Window來顯示界面

Window是像Activity、Dialog、PopupWindow這樣的獨立顯示和交互的界面的抽象。
能夠像下面這樣將一個View顯示到新窗口:

private void newFloatingWindow() {
    final WindowManager wm = (WindowManager)    getSystemService(Context.WINDOW_SERVICE);
    final Button button = new Button(this);
    button.setText("ClickToDismiss");
    LayoutParams lp = new LayoutParams();
    lp.height = LayoutParams.WRAP_CONTENT;
    lp.type = LayoutParams.TYPE_PHONE;
    lp.flags = LayoutParams.FLAG_NOT_FOCUSABLE | LayoutParams.FLAG_NOT_TOUCH_MODAL;

    button.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            wm.removeViewImmediate(button);
        }
    });

    wm.addView(button, lp);
}

上面使用WindowManager建立了一個Window並顯示傳遞的view。
經過追溯源碼:
WindowManager->WindowManagerImpl->WindowManagerGlobal
能夠看到最終addView()的執行是:
ViewRootImpl.setView(View view, WindowManager.LayoutParams attrs, View panelParentView)

實際上,ViewRootImpl和WMS通訊來完成全部實際工做:建立窗口,對View的繪製和事件分發。

NOTE:
newFloatingWindow()的調用能夠在非主線程中,僅要求線程Looper設置ok。這樣onClick()回調也就在對應的子線程中。不過View對象爲了性能其代碼實現是非線程安全的,因此不容許其建立和修改在不一樣的線程中,因此,最方便的就是在主線程中建立View,以後其它線程能夠轉到主線程中去繼續操做View。不然不一樣View在不一樣線程中操做是十分混亂的。要知道,main線程是惟一且必一直存在的。

ViewRootImpl

ViewRootImpl的知識比較多,這裏對它進行一個感性的介紹,便於理解文章中對它的引用。

ViewRootImpl.mView字段就是要顯示的窗口的ViewTree的RootView。
ViewRootImpl做爲ViewTree的管理者,它和WMS通訊完成各類「底層」操做。

做用包括:

  1. 執行ViewTree的繪製。主動發起或是響應requestLayout()或invalidate()而執行performTraversals()/scheduleTraversals()來對ViewTree執行遍歷操做,即測量、佈局和繪製。
  2. 分發InputEvent給ViewTree。

在「將Root添加到Window通知WMS顯示」時,執行performTraversals()中會調用View.dispatchAttachedToWindow(AttachInfo info, int visibility將AttachInfo指定給mView,而ViewGroup會遍歷childViews遞歸此調用。總之,每一個ViewTree的View也會持有其添加到的Window的信息,其中就包含了關聯的ViewRootImpl對象。

NOTE:
通常想知道一些方法的調用時序的話,能夠在可重寫的方法中打印其StackTrace信息查看方法的調用棧。除了那些跨進程IPC調用,或者Handler方式的async調用。

示例ViewTree:MyTree

這裏給出一個構成界面的ViewTree的示例,它將做爲後續討論的例子。爲了描述方便,將此ViewTree稱做「MyTree」。

界面效果:

view-tree-ui

圖1:示例界面

對應的ViewTree結構:

view-tree-structure

圖2:ViewTree結構

ViewTree事件來源

ViewRootImpl接收來自WMS的InputEvent事件,而後調用ViewRootImpl.mView(也就是構成界面的ViewTree的RootView)的View.dispatchPointerEvent(InputEvent event)來向ViewTree傳遞一個MotionEvent event對象。
因此這就是ViewTree事件來源。

InputEvent主要是KeyEvent和MotionEvent,本文僅討論後者。

ViewRootImpl從得到WMS的InputEvent,到分發給mView這裏有一個過程,分兩個部分。

  • InputEvent的接收
    ViewRootImpl使用一個InputEventReceiver對象得到WMS發送的事件,在onInputEvent(InputEvent event)回調中,它執行enqueueInputEvent(event, this, 0, true)將事件添加到一個鏈表,這樣對事件的deliver是保證順序的

  • 分發InputEvent
    過程稍微複雜,由於使用了InputStage組成的一個"input pipeline"來處理InputEvent事件。
    其中一個階段就是將MotionEvent傳遞給mView。

/**
 * Base class for implementing a stage in the chain of responsibility
 * for processing input events.
 * <p>
 * Events are delivered to the stage by the {@link #deliver} method.  The stage
 * then has the choice of finishing the event or forwarding it to the next stage.
 * </p>
 */
abstract class InputStage {...}

ViewRootImpl對事件的分發過程是在主線程中的(它的建立線程和其使用MessageQueue接收事件決定的),並且每次會分發其收到的全部消息。
因此在App的消息循環模型中,響應用戶操做後對UI的改動,所有會一次性獲得執行。以後在下一次主線程下一次Message處理中響應invalidate()/requestLayout()操做進行ViewTree遍歷。

對於一個ViewTree而言,只須要關心輸入事件是從RootView那裏傳入的事實便可。

觸摸操做和觸摸點

用戶第一個手指按下和最終全部手指徹底離開屏幕的過程爲一次觸摸操做,每次操做均可歸類爲不一樣觸摸模式(touch pattern),被定義爲不一樣的手勢。

每一個觸屏的手指——或者稱觸摸點被稱做一個pointer,即一次觸摸過程涉及一或多個pointer。

這裏聲明如下概念:

  • 任意一個pointer的按下定義爲down事件;
  • 任意一個pointer的移動定義爲move事件;
  • 任意一個pointer的擡起定義爲up事件;

第一個down事件,意味着觸摸操做的開始,最後一個up事件意味着觸摸操做的結束。開始和結束時的pointer能夠不是同一個。

事件序列

一次手勢操做過程當中每一個觸摸點都在其down->move->up過程當中產生一系列事件,每一個觸摸點產生的全部事件爲一個獨立的事件序列

  • 事件?
    事件這一律念在代碼中是一個用來攜帶數據的類型,它描述發生了什麼。相似消息這樣的概念,是數據對象而非業務對象。

View.dispatchTouchEvent

在代碼中,ViewRootImpl調用RootView的View.dispatchPointerEvent(MotionEvent event)將事件傳遞給RootView。

public final boolean dispatchPointerEvent(MotionEvent event) {
    if (event.isTouchEvent()) {
        return dispatchTouchEvent(event);
    } else {
        return dispatchGenericMotionEvent(event);
    }
}

對於觸摸事件event.isTouchEvent()爲true,因此執行dispatchTouchEvent(event),方法原型:

/**
  * 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)

方法返回一個boolean值,表示是否此View對象是否處理了傳遞的事件。

傳遞觸摸事件到一個view對象,就是調用其View.dispatchTouchEvent(MotionEvent event)。

對方法View.dispatchTouchEvent()的調用一方面傳遞事件給view,其返回結果又代表了此view是否處理了事件。

事件傳遞

View和ViewGroup兩個類對dispatchTouchEvent()方法提供了不一樣的實現。

ViewGroup的實現是,由於其含有child,它會根據必定的規則選擇調用child.dispatchTouchEvent()將事件傳遞給child,或者不。當ViewGroup.dispatchTouchEvent()中執行了對child.dispatchTouchEvent()的調用時,那麼事件就經由此ViewGroup到達了child。若child依然是ViewGroup,那麼可能繼續傳遞事件給其child。
因此,ViewGroup的dispatchTouchEvent()方法使得多個View對象造成了dispatchTouchEvent()方法的調用棧。這樣事件參數獲得傳遞,並且,返回值也會在方法調用不斷返回時向上返回。

View不包含child,因此不會有調用child.dispatchTouchEvent()的操做,它做爲dispatchTouchEvent()傳遞調用的終點。

基於它們的實現,事件參數從RootView的dispatchTouchEvent()方法的調用開始,會沿着ViewTree的一個路徑不斷傳遞給下一個child——也就是調用child的dispatchTouchEvent()。

以上就是View和ViewGroup的dispatchTouchEvent()方法使得ViewTree產生事件傳遞的原理。

事件序列傳遞給View的規則

做爲事件序列的第一個事件down,dispatchTouchEvent()對它殊性處理,dispatchTouchEvent()傳遞調用時,任何view若返回true,則表示它處理了down事件,那麼後續事件會繼續傳遞給它。若是某個view返回false,那麼調用的傳遞在它這裏終止,後續事件也不會再傳遞給它。

實際上也只在傳遞down事件時,ViewGroup纔會採起必定規則來決定是否傳遞事件給child。
而且它使用TouchTarget類來保存可能的傳遞目標,做爲後續事件傳遞的依據,後續的事件再也不應用down事件那樣的規則。這反映的是事件序列的連續性原則,一個view處理了down事件那麼它必定收到後續事件,不然再也不傳遞事件給它。可見down事件傳遞完成後會肯定下後續事件傳遞的路徑。

NOTE:
一個View收到並處理某個觸摸點的down事件後,那麼即使以後觸摸點移動到View以外,或在View的範圍以外離開屏幕,此View也會收到相應的move、up事件,不過收到的事件中觸摸點的(x,y)座標是在View的區域外。

有關down事件的傳遞細節和TouchTarget等概念,下面源碼分析時再詳細探索。

MotionEvent

上面對事件的描述都是概念上的,代碼中,觸摸事件由MotionEvent表示,它包含了當前事件類型和全部觸摸點的數據,產生事件時觸摸點座標等。

事件拆分

ViewTree中,事件是通過parent到達child的。因爲parent和child的一對多關係和顯示區域包含關係,一個ViewGroup能夠前後收到兩個手指的按下操做,而這兩個觸摸點能夠落在不一樣的child中,而且在不一樣的child來看都是第一個手指的按下。

可見child和parent所「應該」處理的觸摸點是不一樣的,那麼傳遞給它們的事件數據也應該不同。

ViewGroup.setMotionEventSplittingEnabled(boolean split)能夠用來設置一個ViewGroup對象是否啓用事件拆分,方法原型:

/**
 * Enable or disable the splitting of MotionEvents to multiple children during touch event
 * dispatch. This behavior is enabled by default for applications that target an
 * SDK version of {@link Build.VERSION_CODES#HONEYCOMB} or newer.
 *
 * <p>When this option is enabled MotionEvents may be split and dispatched to different child
 * views depending on where each pointer initially went down. This allows for user interactions
 * such as scrolling two panes of content independently, chording of buttons, and performing
 * independent gestures on different pieces of content.
 *
 * @param split <code>true</code> to allow MotionEvents to be split and dispatched to multiple
 *              child views. <code>false</code> to only allow one child view to be the target of
 *              any MotionEvent received by this ViewGroup.
 * @attr ref android.R.styleable#ViewGroup_splitMotionEvents
 */
public void setMotionEventSplittingEnabled(boolean split);

若不開啓拆分,那麼第一個觸摸點落在哪一個child中,以後全部觸摸點的事件都發送給此view。若開啓,每一個觸摸點落在哪一個view中,其事件序列就發送給此child。並且由於RootView收到的事件老是包含了全部觸摸到數據,因此非第一個觸摸點操做時,第一個觸摸點收到「拆分後獲得的move事件」。

由於ViewGroup處理的pointer的數量確定是大於等於全部child處理的pointer的數量的,特別的,傳遞給RootView的事件確定包含全部觸摸點的數據。但child只處理它感興趣的觸摸點的事件——就是down事件發生在自身顯示範圍內的那些pointer。

事件拆分可讓ViewGroup將要分發的事件根據其pointer按下時所屬的child進行拆分,而後把拆分後的事件分別發送給不一樣child。child收到的事件只包含它所處理的pointer的數據,而不含不相干的pointer的事件數據。

最初的MotionEvent中攜帶全部觸摸點數據是爲了便於一些view同時根據多個觸摸點進行手勢判斷。而事件拆分目的是讓不一樣的view能夠同時處理不一樣的事件序列——從原事件序列中分離出來的,以容許不一樣內容區域同時處理本身的手勢。

事件類型

action表示事件的動做類型,即上面描述的down、move、up等,不過MotionEvent類提供了更詳細的劃分。

MotionEvent.getAction()返回一個int值,它包含了兩部分信息:action和產生此事件的觸摸點的pointerIndex。

/**
 * Return the kind of action being performed.
 * Consider using {@link #getActionMasked} and {@link #getActionIndex} to retrieve
 * the separate masked action and pointer index.
 * @return The action, such as {@link #ACTION_DOWN} or
 * the combination of {@link #ACTION_POINTER_DOWN} with a shifted pointer index.
 */
public final int getAction();

實際的動做類型應該經過getActionMasked()來得到。

當一個View處理多個觸摸點的事件序列時,觸摸點產生不一樣事件過程是:

  1. 用戶第一個手指按下,產生ACTION_DOWN事件。
  2. 其它手指按下,觸發ACTION_POINTER_DOWN。
  3. 任何手指的移動,觸發ACTION_MOVE。
  4. 非最後一個手指離開,觸發ACTION_POINTER_UP。
  5. 最好一個手指離開,觸發ACTION_UP。
  6. 收到ACTION_CANCEL,例如View被移除、彈框、界面切換等引發的View忽然不可見。此時收到cancel事件,終止一次手勢。

pointerIndex和pointerId

一個MotionEvent對象中記錄了當前View所處理的全部觸摸點(1或多個)的數據

在MotionEvent中,pointerId是觸摸點的惟一標識,每根手指按下至離開期間其pointerId是不變的,因此能夠用來在一次事件序列中用來連續訪問某個觸摸點的數據。

pointerIndex是當前觸摸點在數據集合中的索引,須要先根據pointerId獲得其pointerIndex,再根據pointerIndex來調用「以它爲參數的各類方法」來獲取MotionEvent中此觸摸點的各類屬性值,如x,y座標等。

NOTE:
出於性能的考慮,多個move事件會被batch到一個MotionEvent對象,可使用getHistorical**()等方法來訪問最近的其它move事件的數據。

源碼分析

通過上面的「理論描述」,能夠得到View系統事件處理的一個總體認識。接下來分析View、ViewGroup中如何實現這些設計的。

源碼:View.dispatchTouchEvent

View.dispatchTouchEvent()中不涉及事件傳遞,它只能本身處理事件。

/**
 * 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) {
    ...

    boolean result = false;    
    final int actionMasked = event.getActionMasked();
    ...

    if (onFilterTouchEventForSecurity(event)) {
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        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;
}

操做以下:

  1. 調用OnTouchListener.onTouch(),傳遞事件給外部監聽者。
  2. 若監聽器未處理,則將事件交給自身的onTouch()去處理。

note:對OnTouchListener調用須要view的enabled=true,即爲激活狀態。而onTouchEvent()的調用受enabled狀態的影響。

源碼:ViewGroup.dispatchTouchEvent

首先須要理解TouchTarget的概念。

TouchTarget

當一個觸摸點的down事件被某個child處理時,ViewGroup使用一個TouchTarget對象來保存child和pointer的對應關係。此pointer的後續事件就直接根據發給此TouchTarget中的child處理,由於down事件決定了整個事件序列的接收者。
由於TouchTarget記錄了接收後續觸摸點事件的child,然後事件將傳遞給它們,因此能夠稱它爲派發目標。

TouchTarget是ViewGroup的靜態內部類:

private static final class TouchTarget {
  // The touched child view.
  public View child;

  // The combined bit mask of pointer ids for all pointers captured by the target.
  public int pointerIdBits;

  // The next target in the target list.
  public TouchTarget next;

  ...
}

字段pointerIdBits存儲了一個child處理的全部觸摸點的id信息,使用了bit mask技巧。好比id = n (pointer ids are always in the range 0..31 )那麼pointerIdBits = 1 << n

由於ViewGroup中能夠是多個child接收不一樣的pointer的事件序列,因此它將TouchTarget設計爲一個鏈表節點的結構,它使用字段mFirstTouchTarget來引用一個TouchTarget鏈表來記錄一次觸屏操做中的全部派發目標。

// First touch target in the linked list of touch targets.
private TouchTarget mFirstTouchTarget;

ACTION_CANCEL

通常的,一個觸摸點的序列遵循down-move-up這樣的序列,但若是在down或者move以後,忽然發生界面切換或者相似view被移除,不可見等狀況,那麼此時觸摸點不會收的「正常」狀況下的up事件,取而代之的是來自parent的一個ACTION_CANCEL類型的事件。
此時child應該以「取消」的形式終止對一次事件序列的處理,如返回以前狀態等。

總體過程

方法的總體操做過程以下:

  • ACTION_DOWN產生時重置狀態,準備迎接新觸屏操做的處理。主要就是清除上次事件派發用到的派發目標。
  • 在down事件時肯定pointer的派發目標。
  • 根據派發目標,派發事件給child。
  • 在up事件時移除對應view處理的觸摸點。

初始化操做

ACTION_DOWN意味着一次新觸摸操做的的事件序列的開始,即第一個手指按下。
這時就須要重置View的觸摸狀態,清除上一次跟蹤的觸摸點的TouchTarget列表。

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

攔截事件

ViewGroup的設計思路是優先傳遞事件給child去處理,但child的設計是不考慮其parent——不現實,
因此爲了不child返回true優先拿走parent指望去先處理的事件序列,能夠重寫onInterceptTouchEvent()來根據自身狀態(也能夠包含child的狀態判斷)選擇攔截事件序列。注意onInterceptTouchEvent()只能用返回值通知dispatchTouchEvent()傳遞過程須要攔截的意思,但對事件的處理是onTouchEvent()中或者OnTouchListener——和View中的處理同樣。

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

onInterceptTouchEvent()的默認實現返回false——即不攔截,而子類根據須要在一些狀態下時攔截DOWN事件。

同時,ViewGroup提供了方法requestDisallowInterceptTouchEvent(boolean disallowIntercept)供childView申請parent不要攔截某些事件。ViewGroup會傳遞此方法到上級parent,使得整個路徑上的parent收到通知,不去攔截髮送給child的一個事件序列
通常child在onInterceptTouchEvent或onTouchEvent中已經肯定要處理一個事件序列時(每每是在ACTION_MOVE中判斷出了本身關注的手勢)就調用此方法確保parent不打斷正在處理的事件序列。

處理down事件:肯定派發目標

在ACTION_DOWN或ACTION_POINTER_DOWN產生時,顯然一個新的觸摸點按下了,此時ViewGroup須要肯定接收此down事件的child,而且將pointerId關聯給child。

TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
    ...
    if (actionMasked == MotionEvent.ACTION_DOWN
            || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)) {
        final int actionIndex = ev.getActionIndex(); // always 0 for down
        final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                : TouchTarget.ALL_POINTER_IDS;

        ...

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

        if (newTouchTarget == null && mFirstTouchTarget != null) {
            // Did not find a child to receive the event.
            // Assign the pointer to the least recently added target.
            newTouchTarget = mFirstTouchTarget;
            while (newTouchTarget.next != null) {
                newTouchTarget = newTouchTarget.next;
            }
            newTouchTarget.pointerIdBits |= idBitsToAssign;
        }
    }
}

上面的方法主要工做:

  1. 根據x,y位置,根據繪製順序「後繪製的在上」的假設對children執行倒序遍歷,找到顯示區域包含事件且能夠接收事件的第一個child,由於處理的是down事件,它將做爲此pointer的TouchTarget。
  2. 遍歷過程當中,若child已經在mFirstTouchTarget所記錄的鏈表中,那麼將pointerId增長給它。此時事件未派發,等待後面根據TouchTarget進行派發。
  3. 調用dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)將down事件派發給child,若child處理了事件,那麼它做爲此pointer的TouchTarget,被添加到mFirstTouchTarget鏈表。
  4. 若是沒找到newTouchTarget,ViewGroup會選擇將pointer綁定到最近處理觸摸點的那個child——仍是不本身處理。

NOTE:

  • 方法dispatchTransformedTouchEvent()在檢查child是否處理事件的過程當中同時已經完成了事件的派發,因此變量alreadyDispatchedToNewTouchTarget用來記錄當前event是否已經派發。

  • split變量表示是否對事件拆分,根據前面的理論知識,不拆分那麼整個觸屏操做過程全部的觸摸點的全部事件只會發給第一個接收ACTION_DOWN的view。拆分的話,每一個觸摸點的事件都是一個單獨的事件序列,發送給不一樣的處理它們的child。

  • 不管事件拆分與否,若觸摸點沒有找到合適的child去處理,而已經有child在處理以前的觸摸點,那麼ViewGroup仍是選擇將事件交給已經處理事件的child,由於有理由相信它在處理多點觸摸事件,然後續觸摸點是整個手勢的一部分。

 dispatchTransformedTouchEvent

/**
 * Transforms a motion event into the coordinate space of a particular child view,
 * filters out irrelevant pointer ids, and overrides its action if necessary.
 * If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
 */
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits);

parent在傳遞事件給child前將座標轉換爲child座標空間下的,即對x,y進行偏移。

若child=null,則意味着ViewGroup本身處理事件,那麼它以父類View.dispatchTouchEvent()的方式處理事件。

參數desiredPointerIdBits中使用位標記的方式記錄了此child處理的那些pointer,全部參數event在真正傳遞給child時會調用MotionEvent.split()來得到僅包含這些pointerId的那些數據。也就是拆分後的子序列的事件。

派發事件

只有down事件會產生一個肯定派發目標的過程。以後,pointer已經和某個child經過TouchTarget進行關聯,後續事件只須要根據mFirstTouchTarget鏈表找到接收當前事件的child,而後分發給它便可。

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

若mFirstTouchTarget=null說明沒有child處理事件,那麼ViewGroup本身處理事件。
傳遞給dispatchTransformedTouchEvent()的參數child==null。
不然,就循環mFirstTouchTarget鏈表,由於event中是包含了全部pointer的數據的,在
dispatchTransformedTouchEvent()中,會根據target.pointerIdBits對事件進行拆分,只發送包含對應pointerId的那些事件數據給target.child。

處理up/cancel事件

每一個pointer的ACTION_UP和ACTION_CANCEL事件意味着其事件序列的終止。
此時在傳遞事件給child以後,應該從mFirstTouchTarget鏈表中移除包含這些pointerId的那些派發目標。

// Update list of touch targets for pointer up or cancel, if needed.
if (canceled
        || actionMasked == MotionEvent.ACTION_UP
        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
    resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
    final int actionIndex = ev.getActionIndex();
    final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
    removePointersFromTouchTargets(idBitsToRemove);
}

本身處理事件

在mFirstTouchTarget鏈表爲空時,ViewGroup本身處理事件。
它經過傳遞給dispatchTransformedTouchEvent()的child參數爲null來表示這一點。

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

以後在上面的調用方法中:

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    // Perform any necessary transformations and dispatch.
    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
      ...
}

由於ViewGroup的父類就是View,因此super.dispatchTouchEvent(transformedEvent)其實就是執行了
View.dispatchTouchEvent(),這時ViewGroup以普通View的方式本身處理事件。

流程總結

設計理論

  • MotionEvent
  • dispatchTouchEvent
  • MotionEvent.split
  • TouchTarget
  • onInterceptTouchEvent
  • disallowIntercept
  • OnTouchListener和onTouchEvent

流程

  • View
    通知OnTouchListener去處理;
    不處理?
    本身的onTouchEvent()處理。
    dispatchTouchEvent()返回true?繼續處理後續事件;
    false?再也不收到後續事件。

  • ViewGroup
    child讓你攔截嗎,onInterceptTouchEvent()本身攔截嗎?
    不攔截?——找TouchTarget;傳遞給child。
    找不到child?攔截?——本身處理。
    dispatchTouchEvent()返回true?繼續處理後續事件;
    false?再也不收到後續事件。

補充

  • 不要重寫dispatchTouchEvent
    能夠看到,從View系統的設計原則上看,View和ViewGroup對dispatchTouchEvent()的不一樣實現造成了View事件的傳遞機制。
    若是須要在ViewGroup中攔截處理事件,那麼應該配合使用onInterceptTouchEvent()和requestDisallowInterceptTouchEvent()。

  • ACTION_MOVE中的getAction()
    此時action中不包含pointerIndex信息,其實只有ACTION_POINTER_UP和
    ACTION_POINTER_DOWN的action才須要保護pointerIndex信息,由於此時pointerCount>1。

  • 攔截和不攔截
    在正常的事件傳遞行爲中補充了parent的優先處理和child的優先處理的動做。
    向上傳遞child的反對攔截的請求。
    在onTouchEvent中作處理,而不是在onInterceptTouchEvent中。
    明確各個方法的職責。

資料

  • MotionEvent和手勢識別
    http://www.cnblogs.com/everhad/p/6075716.html

(本文使用Atom編寫)

相關文章
相關標籤/搜索