1. 程式人生 > >筆記:事件分發機制(二):ViewGroup的事件分發

筆記:事件分發機制(二):ViewGroup的事件分發

前言

前面我根據郭大神的部落格做了View的事件分發的筆記
筆記:事件分發機制(一):View的事件分發
對View的事件分發有了一個比較深入的瞭解。
本篇還是就郭大神的部落格
Android事件分發機制完全解析,帶你從原始碼的角度徹底理解(下)
做一下筆記。從原始碼角度深入分析和理解一下ViewGroup的事件分發。

ViewGroup

ViewGroup是View 的子類,一般作為容器,盛放其他View和ViewGroup。是Android佈局控制元件的直接或間接父類,像LinearLayout、FrameLayout、RelativeLayout等都屬於ViewGroup的子類。ViewGroup與View相比,多了可以包含子View和定義佈局引數的功能。ViewGroup的繼承關係如下:
ViewGroup的繼承關係

從ViewGroup的子類中可以找到平常經常用到的佈局控制元件。

ViewGroup的事件分發流程

demo

簡單瞭解了一下ViewGroup,接下來用demo探索ViewGroup的事件分發流程。

首先,自定義一個佈局命名為MyLayout繼承自LinearLayout,如下所示:

public class MyLayout extends LinearLayout {

    public MyLayout(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
}

然後開啟佈局檔案 activity_view_group.xml ,用MyLayout作為根佈局,設定屬性,新增控制元件。如下:

<?xml version="1.0" encoding="utf-8"?>
<com.wzhy.dispatcheventdemo.MyLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation
="vertical">
<Button android:id="@+id/button1" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Button1" /> <Button android:id="@+id/button2" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Button2" /> </com.wzhy.dispatcheventdemo.MyLayout>

MyLayout佈局中新增兩個按鈕,接著在Activity中為MyLayout和兩個按鈕新增監聽事件:

mLayout.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        Log.i("TAG", " MyLayout: onTouch === action: " + event.getAction());
        return false;
    }
});

mButton1.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Log.i("TAG", "Button1: onClick Button1");
    }
});

mButton2.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Log.i("TAG", "Button2: onClick Button2");
    }
});

設定好監聽事件,執行一下:
這裡寫圖片描述

分別點選Button1、Button2和空白區域,列印結果如下:
log_dispatch_vg

根據log列印可以看出,當點選按鈕時,MyLayout註冊的觸控監聽(OnTouchEvent)的onTouch方法並沒有執行,只有點選空白區域時才會執行onTouch方法。此時,可以先理解為Button的onClick方法將事件消費掉了,因此事件不再向下傳遞。

那麼事件的傳遞流程到底是怎樣的呢?難道是事件先經過子View再到ViewGroup嗎?欲知答案,繼續跟進…

原始碼分析

onInterceptTouchEvent事件攔截

在事件分發機制中,ViewGroup有一個View不具備的方法–onInterceptTouchEvent

/**
 * Implement this method to intercept all touch screen motion events.  This
 * allows you to watch events as they are dispatched to your children, and
 * take ownership of the current gesture at any point.
 *
 * <p>Using this function takes some care, as it has a fairly complicated
 * interaction with {@link View#onTouchEvent(MotionEvent)
 * View.onTouchEvent(MotionEvent)}, and using it requires implementing
 * that method as well as this one in the correct way.  Events will be
 * received in the following order:
 *
 * <ol>
 * <li> You will receive the down event here.
 * <li> The down event will be handled either by a child of this view
 * group, or given to your own onTouchEvent() method to handle; this means
 * you should implement onTouchEvent() to return true, so you will
 * continue to see the rest of the gesture (instead of looking for
 * a parent view to handle it).  Also, by returning true from
 * onTouchEvent(), you will not receive any following
 * events in onInterceptTouchEvent() and all touch processing must
 * happen in onTouchEvent() like normal.
 * <li> For as long as you return false from this function, each following
 * event (up to and including the final up) will be delivered first here
 * and then to the target's onTouchEvent().
 * <li> If you return true from here, you will not receive any
 * following events: the target view will receive the same event but
 * with the action {@link MotionEvent#ACTION_CANCEL}, and all further
 * events will be delivered to your onTouchEvent() method and no longer
 * appear here.
 * </ol>
 *
 * @param ev The motion event being dispatched down the hierarchy.
 * @return Return true to steal motion events from the children and have
 * them dispatched to this ViewGroup through onTouchEvent().
 * The current target will receive an ACTION_CANCEL event, and no further
 * messages will be delivered here.
 */
public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
            && ev.getAction() == MotionEvent.ACTION_DOWN
            && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
            && isOnScrollbarThumb(ev.getX(), ev.getY())) {
        return true;
    }
    return false;
}

原始碼註釋很多,程式碼很簡單,返回值是boolean型別,既然如此,可以重寫這個方法,返回一個true試試。

public class MyLayout extends LinearLayout {

    public MyLayout(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        super.onInterceptTouchEvent(ev);
        return true;
    }
}

再次執行,分別點選Button1、Button2和空白區域,log列印如下:
intercept_ture

發現不管在哪裡點選,只會觸發MyLayout的touch事件,按鈕的點選事件被遮蔽了!這是為什麼?如果touch事件先傳遞到子控制元件後傳遞到ViewGroup,那麼MyLayout怎麼可能遮蔽掉Button的點選事件呢?明明之前點選MyLayout中的Button,只觸發了按鈕的點選事件,沒有觸發MyLayout的touch事件。

只能從原始碼中找答案,才能解決心中的疑惑。事先說明,Android中的touch事件的傳遞,絕對是先傳遞到ViewGroup,後傳遞到View的。

ViewGroup的事件分發dispatchTouchEvent

記得在上一篇(筆記:事件分發機制(一):View的事件分發)中分析過,當觸控一個控制元件,都會先呼叫該控制元件的dispatchTouchEvent()方法。說法沒錯,但並不完整。實際情況是當你觸控某個控制元件,首先呼叫該控制元件所在佈局的dispatchTouchEvent()方法,然後在佈局的dispatchTouchEvent()方法中找到相應的被觸控控制元件,然後在呼叫該空間的dispatchTouchEvent()方法。所以,當我們點選了MyLayout的按鈕,首先會呼叫MyLayout的dispatchTouchEvent()方法,這時會發現MyLayout和它的父類LinearLayout中沒有這個方法,繼續往上找在ViewGroup中發現了dispatchTouchEvent()方法。

既然在ViewGroup找到了dispatchTouchEvent()方法,那麼就看一下這裡的dispatchTouchEvent()方法:

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
    }
    // If the event targets the accessibility focused view and this is it, start
    // normal event dispatch. Maybe a descendant is what will handle the click.
    if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
        ev.setTargetAccessibilityFocus(false);
    }
    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;
        // 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.
            //當啟動一個新的觸控手勢,摒棄所有之前的狀態。
            //由於App切換、ANR(應用無響應),或一些其他狀態改變,框架可能會放棄先前手勢的up或cancel事件。
            cancelAndClearTouchTargets(ev);//取消和清除TouchTarget
            resetTouchState();//重置觸控狀態,會將mFirstTouchTarget置空,FLAG_DISALLOW_INTERCEPT重置
        }
        // Check for interception.//檢查攔截
        final boolean intercepted;
        //如果是按下或者mFirstTouchTarget不為空(初始按下時,會尋找觸控目標),當事件由ViewGroup的子元素
        //成功處理時,mFirstTouchTarget會被賦值並指向子元素,也就是
        //ViewGroup不攔截事件並將事件交由子元素處理時mFirstTouchTarget!=null
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            //FLAG_DISALLOW_INTERCEPT,可通過子View通過requestDisallowInterceptTouchEvent方法設定
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {//如果允許攔截
                intercepted = onInterceptTouchEvent(ev);//呼叫ViewGroup自己的事件攔截
                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.
            //沒有觸控目標(mFirstTouchTarget == null)且當前動作(action)不是初始按下動作
            //那麼當前ViewGroup繼續攔截觸控事件。也就是點選位置的ViewGroup內沒有找到目標View,
            //事件由這個ViewGroup攔截處理。比如,點選ViewGroup內空白處。
            intercepted = true;
        }

        //如果已攔截或已經有一個View正在處理手勢,正常事件分發。
        // If intercepted, start normal event dispatch. Also if there is already
        // a view that is handling the gesture, do normal event dispatch.
        if (intercepted || mFirstTouchTarget != null) {
            ev.setTargetAccessibilityFocus(false);
        }
        // Check for cancelation.//檢查是否取消。
        final boolean canceled = resetCancelNextUpFlag(this)
                || actionMasked == MotionEvent.ACTION_CANCEL;

        // Update list of touch targets for pointer down, if needed.
        //如果需要,更新手指按下後一些列touch目標。
        final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
        //新的touch目標
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;

        //動作(手勢)沒有取消,且事件沒有被當前ViewGroup攔截,
        //事件會向下分發,交由它的子類處理。
        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) {
                final int actionIndex = ev.getActionIndex(); // always 0 for down
                final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                        : TouchTarget.ALL_POINTER_IDS;

                // Clean up earlier touch targets for this pointer id in case they
                // have become out of sync.
                //清除當前觸控點之前的觸控目標以防止不同步。
                removePointersFromTouchTargets(idBitsToAssign);

                //遍歷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. 從前往後掃描ViewGroup的子控制元件。
                    final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                    final boolean customOrder = preorderedList == null
                            && isChildrenDrawingOrderEnabled();
                    final View[] children = mChildren;

                    //遍歷ViewGroup的所有子View。
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        final int childIndex = getAndVerifyPreorderedIndex(
                                childrenCount, i, customOrder);
                        final View child = getAndVerifyPreorderedView(
                                preorderedList, children, 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;
                        }

                        //重要:觸控點位置是否在子View的範圍內,或者子View是否可見或在播放動畫;
                        //如果均不符合,則continue, 表示子View不符合條件,開始遍歷下一個子View
                        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);
                        //分發轉換過的觸控事件,獲取最後按下的子View的index。
                        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();
                            //為子View新增一個TouchTarget,並把這個TouchTarget賦值給mFirstTouchTarget
                            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;
                }
            }
        }

        //如果找不到子View來處理事件,最後交由ViewGroup來處理
        // Dispatch to touch targets.
        if (mFirstTouchTarget == null) {
            //沒有觸控的目標,所以把當前ViewGroup當做普通View(,去處理觸控事件)。
            // No touch targets so treat this as an ordinary view.
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            //mFirstTouchTarget不為空,表示已找到一個子View來消耗事件
            // 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;
            }
        }

        // Update list of touch targets for pointer up or cancel, if needed.
        //如果需要,更新觸控點擡起(up)和取消(cancel)的觸控目標集合
        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);
        }
    }

    if (!handled && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
    }
    return handled;
}

從上面的原始碼可知,當ViewGroup不作事件攔截,會遍歷ViewGroup的所有子元素,然後判斷子元素是否能夠接收到事件。是否能夠接收到事件主要由兩點判斷:子元素是否在播放動畫和點選事件是否落在子元素的邊界範圍內。如果某個子元素滿足這兩個條件,那麼事件就會傳遞給這個子控制元件來處理。可以看到,dispatchTransformedTouchEvent實際上呼叫的是子元素的dispatchEvent方法,其內部有一段程式碼:

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

如果找到了能夠接收事件的子View,就會由這個View分發處理事件,否則將當前ViewGroup作為ViewViewGroupsuperView)分發處理該事件。

如果子元素的dispatchTouchEvent返回true,我們就暫時不用考慮事件是怎樣在子View中分發的。那麼mFirstTouchTarget就會被賦值同時跳出for迴圈,如下所示:

newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;

這幾行程式碼完成了mFirstTouchTarget的賦值並終止對子元素的遍歷。如果子元素的dispatchTouchEvent返回false,ViewGroup會繼續遍歷,把事件分發給下一個符合條件的子元素(如果有的話)。

其實mFirstTouchTarget的賦值是在addTouchTarget方法中,從下面addTouchTarget方法的原始碼中可以看出,mFirstTouchTarget其實是一種單鏈表結構。mFirstTouchTarget是否被賦值,將直接影響到ViewGroup對事件的攔截策略,如果mFirstTouchTarget為null,那麼ViewGroup就預設攔截接下來的同一序列中(down-up、down-move…-up等)的所有觸控事件。

/**
 * Adds a touch target for specified child to the beginning of the list.
 * Assumes the target child is not already present.
 */
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
    final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;
}

如果遍歷了所有子元素後,事件沒有被合理地處理。這包含兩種情況:一,ViewGroup沒有子元素;二,子元素處理了事件,但在dispatchTouchEvent中返回了false,這一般是因為子元素在onTouchEvent中返回了false。在這兩種情況下,ViewGroup會自己處理點選事件。如下:

// 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 {
    ......
}

注意dispatchTransformedTouchEvent的第三個引數child為null,從dispatchTransformedTouchEvent方法的分析可知,它會呼叫super.dispatchTouchEvent(event);,而這裡的super是一個View(ViewGroup的super是View),也就是ViewGroup作為View去處理事件,這就轉到了View的事件分發。

下面是dispatchTransformedTouchEvent的原始碼:

//把一個動作事件轉換到一個特定子View的座標空間中,過濾掉不相干的
//觸控點ids,並且在必要時覆蓋它的動作(事件)。如果子控制元件為空,
//動作事件(MotionEvent)將會分發給當前ViewGroup.
/**
 * 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) {
    final boolean handled;
    // Canceling motions is a special case.  We don't need to perform any transformations
    // or filtering.  The important part is the action, not the contents.
    //取消動作時一個特殊情況。我們不需要執行任何轉換或者過濾。
    //它重要的部分是動作(action),不是內容。
    final int oldAction = event.getAction();//獲取動作
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }

    //計算要分發的觸控點數量。過濾掉不相干的觸控點id。
    // Calculate the number of pointers to deliver.
    final int oldPointerIdBits = event.getPointerIdBits();
    final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;

    // If for some reason we ended up in an inconsistent state where it looks like we
    // might produce a motion event with no pointers in it, then drop the event.
    //如果由於一些原因,我們最後處於一個不合邏輯的狀態,就好像我們作出一個沒有觸控點的動作事件,那麼停止事件。
    if (newPointerIdBits == 0) {
        return false;
    }

    //動作事件複用
    // 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.
    //如果兩個觸控點的數值一樣,並且我們不需要執行任何不可逆的變換,
    //那麼我們可以複用動作事件(motion event)來進行分發,只要
    //我們注意恢復我們做出的改變。
    final MotionEvent transformedEvent;
    if (newPointerIdBits == oldPointerIdBits) {
        if (child == null || child.hasIdentityMatrix()) {
            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);
            }
            return handled;
        }
        transformedEvent = MotionEvent.obtain(event);
    } else {
        transformedEvent = event.split(newPointerIdBits);
    }

    //執行任何必要的轉換和事件分發。
    // Perform any necessary transformations and dispatch.
    if (child == null) {
    //如果沒找到接收事件的子類,就由ViewGroup(ViewGroup的super是View)自己作為View分發處理事件。
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {如果有接收事件的子View,座標空間轉換後,事件由子View分發處理。
        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;
}

Activity對事件的分發過程

點選事件用MotionEvent來表示,當一個點選操作發生時,事件首先傳遞給當前Activity,由Activity的dispatchTouchEvent來進行事件分發,具體工作是由Activity內部的Window來完成的。Window會將事件傳遞給DecorView,DecorView一般是當前介面的底層容器(也就是setContentView所設定View的父容器),通過activity.getWindow().getDecorView()可以獲得。先從Activity的dispatchTouchEvent開始分析:

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,意味著事件沒有控制元件處理,那麼Activity的onTouchEvent就會被呼叫。

接下來看Window是如何將事件傳遞給ViewGroup的。通過原始碼我們知道,Window是一個抽象類,而Window的superDispatchTouchEvent是個抽象方法,因此必須找到Window的實現類才行。

Window的實現類到底是誰?其實是PhoneWindow,這一點從Window的原始碼可以知道,在Window原始碼的類註釋中有這麼一段話:

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.

大概意思是:Window類可以控制頂層View的外觀和行為策略。它的唯一實現是android.policy.PhoneWindow,當你想要例項化這個Window類時,你並不 知道它的細節,因為這個類會被重構,只有一個工廠方法可以使用。

由於Window的唯一實現是PhoneWindow,那麼就看一下PhoneWindow是怎樣處理事件的。也就是看一下superDispatchTouchEvent方法:

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

到這裡就很清楚了,PhoneWindow將事件傳遞給了DecorView,這個DecorView是什麼呢?看下面:

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

@Override
public final View getDecorView() {
    if (mDecor == null || mForceDecorInstall) {
        installDecor();
    }
    return mDecor;
}

我們可以通過getWindow().getDecorView().findViewById(R.id.content).getChildAt(0)獲取Activity通過所設定的View。這個mDecor就是getWindow().getDecorView()返回的View,而我們通過setContentView設定的View是它的一個子View。現在事件傳遞到了DecorView這裡,由於DecorView繼承自FrameLayout,且是父View,所以最終事件會傳遞給setContentView設定View。從這裡開始,事件就傳遞到頂級View了,也就是Activity中通過setContentView設定View,另外頂級View也叫根View,頂級View一般都是ViewGroup。

這樣就與上面ViewGroup的事件分發連線上了。

借用何俊林大神(逆流的魚yuiop)的一張圖片,來展示ViewGroup的事件分發流程。
事件分發流程

ViewGroup事件分發試驗

code

與部落格最開始一樣,自定義一個MyLayout,繼承LinearLayout。重寫三個方法:onInterceptTouchEventonTouchEventdispatchTouchEvent,程式碼如下:

public class MyLayout extends LinearLayout {

    public MyLayout(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.i("TAG", "MyLayout: onInterceptTouchEvent === " + getActionString(ev.getAction()));
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i("TAG", "MyLayout: onTouchEvent === " + getActionString(event.getAction()));
        return super.onTouchEvent(event);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.i("TAG", "MyLayout: dispatchTouchEvent === " + getActionString(ev.getAction()));
        return super.dispatchTouchEvent(ev);
    }

    private String getActionString(int action){
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                return "ACTION_DOWN";
            case MotionEvent.ACTION_MOVE:
                return "ACTION_MOVE";
            case MotionEvent.ACTION_UP:
                return "ACTION_UP";
            case MotionEvent.ACTION_CANCEL:
                return "ACTION_CANCEL";
            default: return "ACTION_OTHER";
        }
    }
}

然後,自定義一個MButton,繼承Button,重寫兩個方法onTouchEventdispatchTouchEvent,程式碼如下:

public class MButton extends android.support.v7.widget.AppCompatButton {

    public MButton(Context context) {
        super(context);
    }

    public MButton(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MButton(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.i("TAG", "MButton: dispatchTouchEvent === " + getActionString(event.getAction()));
        return super.dispatchTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i("TAG", "MButton: onTouchEvent === " + getActionString(event.getAction()));
        return super.onTouchEvent(event);
    }

    private String getActionString(int action){
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                return "ACTION_DOWN";
            case MotionEvent.ACTION_MOVE:
                return "ACTION_MOVE";
            case MotionEvent.ACTION_UP:
                return "ACTION_UP";
            case MotionEvent.ACTION_CANCEL:
                return "ACTION_CANCEL";
            default: return "ACTION_OTHER";
        }
    }
}

在佈局檔案activity_view_group.xml中引用MyLayout和MButton,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<com.wzhy.dispatcheventdemo.MyLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/my_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.wzhy.dispatcheventdemo.MButton
        android:id="@+id/m_button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="MButton"
        />

</com.wzhy.dispatcheventdemo.MyLayout>

最後,在ActivityViewGroup中為MyLayoutMButton設定onTouchListener,重寫Activity的onTouchEventdispatchTouchEvent。程式碼如下:

public class ActivityViewGroup extends AppCompatActivity {

    private MyLayout mLayout;
    private MButton mButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_view_group);

        mLayout = (MyLayout) findViewById(R.id.my_layout);
        mButton = (MButton) findViewById(R.id.m_button);

        mLayout.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.i("TAG", "MyLayout: onTouch === " + getActionString(event.getAction()));
                return false;
            }
        });

        mButton.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.i("TAG", "MButton: onTouch === " + getActionString(event.getAction()));
                return false;
            }
        });

    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        String actionString = getActionString(ev.getAction());
        Log.i("TAG", "Activity: dispatchTouchEvent === " + actionString);
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        String actionString = getActionString(event.getAction());
        Log.i("TAG", "Activity: onTouchEvent ===