ViewGroup事件分發機制源碼分析

事件分發順序

Android中的事件處理一直是Android自定義控件開發中的重難點,想寫出交互多點的控件處理好觸摸事件是非常關鍵的,觸摸事件就是對手指觸摸到手機屏幕后產生的一系列事件,即手指按下的Down事件,一個或者一連串的Move事件(Move事件很敏感),手指抬起時的Up事件...,而我們要通過具體的交互場景,告訴我們自己代碼,當前的事件應該給誰來處理,或者是不處理。
簡要說明下首先得到事件的是當前所在的Activity,然后是Window,再到DecorView,再到ViewGroup。即
Activity——>Window(實現類PhoneWindow)——>DecorView——>ViewGroup;
這里分享一個鏈接,有興趣的可以去了解下:Activity+Window+View簡單說明

Activity+Window+View關系圖

下面我們從源碼入手分析下是怎么實現的:
起于activity:
手指在觸摸屏上滑動所產生的一系列事件,當Activity接收到這些事件通過調用Activity的dispatchTouchEvent方法來進行對事件的分發操作,下面來看下其源碼

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();//空方法,暫時忽略之  
        }
        //把事件ev交給Window來處理,在window中是抽象方法,
            看下其實現類PhoneWindow對應的具體方法 
        if (getWindow().superDispatchTouchEvent(ev)) {
        //如果返回true表明整個事件到此結束,處理完畢
            return true;
        }
        //如果View未處理事件,則直接調用activity自己的onTouchEvent
        return onTouchEvent(ev);
    }

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

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

DecorView:
在這個時候就能夠很清晰的看到DecorView它調用了父類的dispatchTouchEvent方法。
在上面說到DecorView它繼承了FrameLayout,而這個FrameLayout又繼承自ViewGroup。
我們在Activity界面中你能見到的各個View都是DecorView的子View。到此為止事件已經分發到View上面

public boolean superDispatchTouchEvent(MotionEvent event) {
        //已經分發到ViewGroup上了
        return super.dispatchTouchEvent(event);
    }

ViewGroup事件分發源碼分析

在事件處理中充分發揮了View與ViewGroup的子父類關系,在setContentView()設置布局就是一個具體的ViewGroup子類。但其內部包含一些View或者ViewGroup,且ViewGroup本身繼承自View,所以ViewGroup也是一個View。而通過前面的結論是先到達ViewGroup,這時候ViewGroup可以攔截給本身對事件做出處理,也可以下發到它的子View并交由子View進行處理,如果子View不處理則又會回到布局父節點,正是這種循環調用的設計思想使開發者能巧妙地應對不同的業務場景需求,下面看下最復雜的ViewGroup事件處理源碼
看到這里我們要說下ViewGroup中常提到的幾個事件處理相關的方法:

public boolean dispatchTouchEvent(MotionEvent ev)
public boolean onInterceptTouchEvent(MotionEvent ev)
public boolean onTouchEvent(MotionEvent event)

下面來主要分析下viewGroup中的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.
            cancelAndClearTouchTargets(ev);//mFirstTouchTarget初始化
            resetTouchState();//狀態位重置
        //所以即使我們設置了這個標志位通過requestDisallowInterceptTouchEvent(true),
          viewGroup也依舊能收到down事件
            if (!disallowIntercept) {
        }

        // Check for interception.
        final boolean intercepted;
        //ACTION_DOWN或者mFirstTouchTarget != null都會進行到if當中,
        意味著除了Down以外的事件也是可能進入IF中,mFirstTouchTarget對象很關鍵,此時其已經為非空
        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;
        }

        // 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.
        final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
        //沒有被取消并且沒有被攔截,則進入下面方法,
        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);

                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 = buildTouchDispatchChildList();
                    final boolean customOrder = preorderedList == null
                            && isChildrenDrawingOrderEnabled();
                    final View[] children = mChildren;
                    //倒序遍歷所有的子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;
                        }

                        if (!canViewReceivePointerEvents(child)
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            ev.setTargetAccessibilityFocus(false);
                            continue;
                        }
                        //調用getTouchTarget方法去查找當前子View是否在mFirstTouchTarget.next這條target鏈中,
                        //如果存在則返回這個target,否則返回null。
                        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);
                        //調用dispatchTransformedTouchEvent()方法將Touch事件傳遞給特定的子View
                        //該方法返回false--則說明子view未消耗點擊事件,從而下面的newTouchTarget = addTouchTarget(child, idBitsToAssign)方法無法調用,mFirstTouchTarget則為null
                        //該方法返回true--則說明子view消耗點擊事件,從而進入if區域,從而mFirstTouchTarget不為null。
                        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;
                }
            }
        }

        // Dispatch to touch targets.
        // mFirstTouchTarget == null說明點擊事件被攔截,或者子view沒有消耗事件,
有這里也可以得到onInterceptTouchEvent方法并不是通過自己將事件攔截下來給自己,
只是通過返回值使dispatchTouchEvent()方法不進入查找子view的dispatchTouchEvent(),
使事件隔離開了與子view的關系
        if (mFirstTouchTarget == null) {
            // No touch targets so treat this as an ordinary view.
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        //調用父類dispatchTouchEvent,再調用onTouchEvent處理焦點
        } 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;
        //根據alreadyDispatchedToNewTouchTarget 判斷,如果已經分發了,則返回true
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    //點擊事件未被上面的子view消耗時,事件傳遞過程
 //比如:有些人強制在ViewGroup中MotionEvent.ACTION_DOWN時,
  onInterceptTouchEvent返回false,ACTION_MOVE時,返回true,則進入到此方法清除掉
  mFirstTouchTarget鏈表中所有target,及mFirstTouchTarget==null;
  這樣下一次直接跑入到 if (mFirstTouchTarget == null)內容區域內,
  則點擊事件傳遞到ViewGroup的onTouchEvent處理焦點
                    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.
        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;
}

看下多次用到的dispatchTransformedTouchEvent()方法,

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.
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL);
        //事件處理vIEW鏈表底部
        if (child == null) {
            //父類view的dispatchTouchEvent(),根據返回值由自己的onTouchEvent來決定
            handled = super.dispatchTouchEvent(event);
        } else {
            //由child的onTouchEvent來決定,如果是viewGroup的話 也能進行循環遍歷
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }

    // 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.
    final MotionEvent transformedEvent;
    if (newPointerIdBits == oldPointerIdBits) {
        if (child == null || child.hasIdentityMatrix()) {
            if (child == null) {
        //父類view的dispatchTouchEvent(),根據返回值由自己的onTouchEvent來決定
                handled = super.dispatchTouchEvent(event);
            } else {
                //按照子控件進行坐標轉換,如果是viewGroup的話 也能進行循環遍歷
                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) {
        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;
}

下面用一幅圖來概況下整個流程

事件分發流程

源碼得到的總結

  1. android的事件傳遞是先傳遞到父類,再到子類的。

  2. ViewGroup中可以通過onInterceptTouchEvent方法對事件傳遞進行攔截,但是子View可以通過requestDisallowInterceptTouchEvent(boolean disallowIntercept)控制父類的攔截事件是否調用。

  3. 子View消耗掉點擊事件后,父類onTouchEvent方法不會調用,子View不消耗點擊事件,會傳到父類onTouchEvent方法,父類onTouchEvent方法返回false,則最終傳遞到activity的onTouchEvent方法。

  4. ViewGroup一旦調用onInterceptTouchEvent方法攔截點擊事件后,本次 點擊序列事件則都交于該ViewGroup處理,并且onInterceptTouchEvent將不再執行。判斷是否父類是否攔截點擊事件中的解釋。

  5. 當dispatchTouchEvent在進行事件分發的時候,只有前一個action返回true,才會觸發下一個action.也就是說,子view 未消耗點擊事件,及dispatchTouchEvent返回false,這樣mFirstTouchTarget =null,,則后續action直接由ViewGroup執行,不傳遞給子View。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容