1. 程式人生 > >Android O: 觸控事件傳遞流程原始碼分析(上)

Android O: 觸控事件傳遞流程原始碼分析(上)

前面的部落格中,我們通過例子分析了一下Android中事件傳遞的流程,
詳細內容可以參考:Android觸控事件傳遞機制簡要分析

貫穿整個Android的觸控事件分發的流程,基本可以抽象成以下的虛擬碼:

public boolean dispatchTouchEvent(MotionEvent ev){
    boolean handle = false;
    if(onInterceptTouchEvent(ev)){
        handle = onTouchEvent(ev);
    }else{
        handle = child.dispatchTouchEvent(ev);
    }
    return
handle; }

如果一個事件傳遞到了ViewGroup處,首先會判斷當前ViewGroup是否要攔截事件,
即呼叫onInterceptTouchEvent()方法。

如果返回true,則表示ViewGroup攔截事件,
那麼ViewGroup就會呼叫自身的onTouchEvent來處理事件;

如果返回false,表示ViewGroup不攔截事件,
此時事件會分發到它的子View處,即呼叫子View的dispatchTouchEvent方法。

如此反覆直到事件被消耗掉。

本篇部落格,我們以Android O的程式碼為例,看看對應流程的原始碼。
考慮到事件分發的流程較為繁瑣,本篇部落格主要針對Activity和ViewGroup部分。

一、Activity部分
系統有一個執行緒在迴圈收集螢幕硬體資訊。
當用戶觸控式螢幕幕時,該執行緒會把從硬體裝置收集到的資訊,
封裝成一個MotionEvent物件,然後把該物件存放到一個訊息佇列中。

與此對應,系統的另一個執行緒迴圈的讀取訊息佇列中的MotionEvent,然後交給WMS去派發。
WMS把該事件派發給當前處於Active狀態的Activity,即處於活動棧最頂端的Activity。

我們就從Activity的dispatchTouchEvent入手,看看整個事件分發的流程:

    public boolean dispatchTouchEvent(MotionEvent ev) {
        if
(ev.getAction() == MotionEvent.ACTION_DOWN) { //空實現,其用途註釋已經說的比較清楚了 //若需要了解使用者點選介面,可以自行實現該介面 //Implement this method if you wish to know that the user has //interacted with the device in some way while your activity is running. onUserInteraction(); } //首先進行事件分發,事件被消費掉就會返回true if (getWindow().superDispatchTouchEvent(ev)) { return true; } //如果事件沒有沒消費掉,那麼就會呼叫Activity的onTouchEvent return onTouchEvent(ev); }

從上面的程式碼容易看出,Activity收到觸控事件後首先會進行分發;
如果事件在分發的過程中沒被消費掉,最終會被Activity的onTouchEvent處理。

在繼續分析之前,我們先看看Activity的getWindow函式:

    public Window getWindow() {
        //返回Activity持有的mWindow物件
        return mWindow;
    }

mWindow物件是Activity顯示在介面上時建立的,如下所示:

    final void attach(.....) {
        ........
        //實際上是一個PhoneWindow物件
        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        ........
    }

由上面的程式碼易知,Activity分發的事件實際上交給了PhoneWindow。
對應分發事件的函式如下:

    public boolean superDispatchTouchEvent(MotionEvent event) {
        //mDecor的型別為DecorView,在PhoneWindow初始化時得到
        return mDecor.superDispatchTouchEvent(event);
    }

DecorView繼承FrameLayout,後者繼承ViewGroup,
於是上述程式碼最終會將觸控事件遞交給ViewGroup處理。

二、ViewGroup部分
接下來我們跟進ViewGroup的dispatchTouchEvent函式。

2.1 處理Accessibility Focus事件

public boolean dispatchTouchEvent(MotionEvent ev) {
    .......
    // 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);
    }
    .......

ViewGroup收到觸控事件後,首先需要處理特殊情況,
即攜帶FLAG_TARGET_ACCESSIBILITY_FOCUS的MotionEvent。
該標誌位的含義可以參考註釋:

/**
* 1、 Private flag indicating that this event was synthesized by the system and
*    should be delivered to the accessibility focused view first.
* 
* 2、 When being dispatched such an event is not handled by predecessors of the accessibility
* focused view and after the event reaches that view the flag is cleared and
* normal event dispatch is performed.
* .......
* /

從上述註釋可以看出,當MotionEvent具有該Flag時,
若點選區域存在accessibility focused view(或ViewGroup),那麼事件會優先被遞交給這種View處理。
即其父View或ViewGroup,無法消費該MotionEvent(從程式碼來看,ViewGroup仍可以攔截)。

當accessibility focused view收到該MotionEvent後,就會呼叫上面的程式碼,
清除掉FLAG_TARGET_ACCESSIBILITY_FOCUS標籤。
此後,MotionEvent就會按照普通的邏輯被分發和消費。

從上面的程式碼也可以看出,當MotionEvent攜帶Flag時,
會利用isAccessibilityFocusedViewOrHost函式,判斷當前View是否有能力處理accessibility focused motion event。
如果滿足條件,將會呼叫setTargetAccessibilityFocus函式,取消掉FLAG_TARGET_ACCESSIBILITY_FOCUS。

我們看看isAccessibilityFocusedViewOrHost函式:

    //實際上定義在ViewGroup的父類View中
    boolean isAccessibilityFocusedViewOrHost() {
        //判斷View是否具有PFLAG2_ACCESSIBILITY_FOCUSED
        //或者利用view root判斷當前View是否具有ACCESSIBILITY_FOCUSED能力
        return isAccessibilityFocused() || (getViewRootImpl() != null && getViewRootImpl()
                .getAccessibilityFocusedHost() == this);
    }

實際上當一個View呼叫requestAccessibilityFocus後,就會具有accessibility focus:

public boolean requestAccessibilityFocus() {
    //ViewManager enable 且 View可視時,就可以申請accessibility focus
    AccessibilityManager manager = AccessibilityManager.getInstance(mContext);
    if (!manager.isEnabled() || !manager.isTouchExplorationEnabled()) {
        return false;
    }

    if ((mViewFlags & VISIBILITY_MASK) != VISIBLE) {
        return false;
    }

    if ((mPrivateFlags2 & PFLAG2_ACCESSIBILITY_FOCUSED) == 0) {
        //置標誌位
        mPrivateFlags2 |= PFLAG2_ACCESSIBILITY_FOCUSED;

        //在ViewRoot中記錄
        ViewRootImpl viewRootImpl = getViewRootImpl();
        if (viewRootImpl != null) {
            viewRootImpl.setAccessibilityFocus(this, null);
        }
        //重新整理
        invalidate();

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
        return true;
    }
    return false;
}

2.2 過濾事件
處理完特殊事件後, 需要進行安全性檢測,如下程式碼所示:

//用於記錄事件最終是否被處理
boolean handled = false;

//滿足條件的事件才能被繼續處理
if (onFilterTouchEventForSecurity(ev)) {
    .....

onFilterTouchEventForSecurity實際上也定義於ViewGroup的父類View中:

    public boolean onFilterTouchEventForSecurity(MotionEvent event) {
        //判斷View是否具有FILTER_TOUCHES_WHEN_OBSCURED
        if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
                //判斷事件是否具有FLAG_WINDOW_IS_OBSCURED
                && (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
            // Window is obscured, drop this touch.
            //該標誌位的含義表示:當View所在的Window被遮擋且事件攜帶對應標誌時,應該過濾收到的觸控事件
            return false;
        }
        return true;
    }

為了理解增加過濾判斷的原因,我們需要看看FLAG_WINDOW_IS_OBSCURED的註釋:

   /**
     * 1、window被覆蓋時,收到其上window的觸控事件時,就會新增該標誌位
     * This flag indicates that the window that received this motion event is partly
     * or wholly obscured by another visible window above it.  This flag is set to true
     * even if the event did not directly pass through the obscured area.
     * 
     * 2、新增標誌位的原因: 防止惡意應用覆蓋在正常應用之上,擷取使用者資訊或誤導使用者
     * 例如:需要輸入密碼時,惡意應用新增透明介面,就可能擷取使用者輸入的資訊
     * A security sensitive application can check this flag to identify situations in which
     * a malicious application may have covered up part of its content for the purpose
     * of misleading the user or hijacking touches.  An appropriate response might be
     * to drop the suspect touches or to take additional precautions to confirm the user's
     * actual intent.
     */

2.3 攔截事件
如果觸控事件沒有被過濾掉,那麼就可以開始正常處理了,對應原始碼如下:

............
//首先得到觸控事件的型別
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;

//Handle an initial down.
//所有的觸控事件都以ACTION_DOWN開始
//因此,當收到ACTION_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);

    //一個MotionEvent從ACTION_DOWN開始,到ACTION_UP結束
    //其間會有多個型別的事件傳遞到View
    //這些事件傳遞的物件,會用一個連結串列TouchTargets記錄起來

    //當一個ACTION_DOWN發生時,該函式中會呼叫clearTouchTargets,
    //清除舊有的記錄
    resetTouchState();
}

// Check for interception.
final boolean intercepted;

//如果收到新的ACTION_DOWN事件
//或者其它型別的事件,但之前已經找到傳遞View
//則進入常規攔截流程
if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) {
    //mFirstTouchTarget指向前一個事件傳遞的View
    //不為null, 說明之前已經有事件未被攔截

    //首先需要判斷ViewGroup是否允許攔截事件
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

    if (!disallowIntercept) {
        //允許攔截時呼叫onInterceptTouchEvent函式
        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.
    // 進入這個分支,以為收到非ACTION_DOWN事件,且mFirstTouchTarget = null
    // 那麼說明這個MotionEvent的ACTION_DOWN事件就被攔截了
    // 那麼之後的UP、MOVE等操作,都會被攔截
    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);
}
..........

除了特殊的MotionEvent以外,預設情況下,
ViewGroup的onInterceptTouchEvent將返回false,
即不攔截MotionEvent。

2.4 查詢處理ACTION_DOWN等事件的View
若觸控事件沒有被擷取,就需要查詢處理事件的View了。

// Check for cancelation.
// 首先判斷該事件是否需要被取消
final boolean canceled = resetCancelNextUpFlag(this)
    || actionMasked == MotionEvent.ACTION_CANCEL;

//判斷是否可以將事件分發給多個子View
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0

//用於記錄本次分發事件的目標
TouchTarget newTouchTarget = null;

//記錄是否成功分發
boolean alreadyDispatchedToNewTouchTarget = false;

//如果事件沒有被攔截或取消,就可以開始進行分發
if (!canceled && !intercepted) {
    //與前面第一部分對應, 對於一個特殊的事件來說
    //如果當前ViewGroup可以處理,在第一部分已經清除掉了flag
    //若無法處理,則在這裡查詢是否存在可以處理的child view
    View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
            ? findChildWithAccessibilityFocus() : null;
    //開始處理ACTION_DOWN型別的事件        
    ..................

從上面程式碼可以看出,當MotionEvent沒有被攔截或取消時,才會執行後續的流程。
否則,將直接進入下一部分(即2.5小節)。

//對於以下型別的MotionEvent,才需要重新查詢target View
//其中比較有代表性的就是ACTION_DOWN
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

    //如果需要將事件分發給多個子View, 則得到actionIndex對應的pointerId
    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.
    // 清除idBitsToAssign對應target的舊有狀態
    removePointersFromTouchTargets(idBitsToAssign);

    final int childrenCount = mChildrenCount;
    //開始進行查詢
    if (newTouchTarget == null && childrenCount != 0) {
        //得到觸控事件對應的座標,根據座標才能得到可以處理該事件的child view
        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的child view
        // 從變數名來看,得到的是先序遍歷的結果
        final ArrayList<View> preorderedList = buildTouchDispatchChildList();

        //此處對應child view按特定順序繪製的情況
        //需要參考getChildDrawingOrder和setChildrenDrawingOrderEnabled
        final boolean customOrder = preorderedList == null
                && isChildrenDrawingOrderEnabled();

        //逆序開始查詢Child View
        //這麼做的原因是:
        //後新增的View作為子View,被繪製在父View的上層
        //於是,按照先序遍歷時,後新增的子View肯定處於List的後端
        //我們點選介面時,按照邏輯,肯定應該讓最上面的View優先進行處理
        //結合上面的原因,就應該讓處於List末端的子View,優先處理MotionEvent
        for (int i = childrenCount - 1; i >= 0; i--) {
            //從preorderedList取出View
            final int childIndex = getAndVerifyPreorderedIndex(
                    childrenCount, i, customOrder);
            final View child = getAndVerifyPreorderedView(
                    preorderedList, children, childIndex);

            //需要處理Accessibility Focus事件時,優先找出對應的View
            if (childWithAccessibilityFocus != null) {
                if (childWithAccessibilityFocus != child) {
                    continue;
                }

                //一旦找到, 就將觸控事件遞交給該View
                //此處置為null, 表明不再尋找具有Accessibility Focus能力的View
                //因此,只有第一個具有Accessiliblity focus能力的View有機會處理這種型別的事件
                childWithAccessibilityFocus = null;

                //此處,將i重置為childrenCount - 1
                //意味著若具有Accessibility Focus能力的View, 無法處理該MotionEvent
                //那麼將按照正常流程, 重新找出能夠處理該MotionEvent的View
                i = childrenCount - 1;
            }

            //判斷View能否處理MotionEvent
            //canViewReceivePointerEvents主要判斷View是否可見(可見或在播放動畫)
            //isTransformedTouchPointInView主要判斷MotionEvent的座標是否落在View內
            if (!canViewReceivePointerEvents(child)
                    || !isTransformedTouchPointInView(x, y, child, null)) {
                // 如果不能處理該View, 則跳過

                //程式碼進入到這裡,意味著:
                //沒有Accessibility Focus View
                //或有Accessibility Focus View, 但無法處理該MotionEvent
                //無論哪種情況,都去掉標誌,將MotionEvent當作普通事件處理
                ev.setTargetAccessibilityFocus(false);
                continue;
            }

            //判斷View是否已經接收過該事件(其它的型別)
            //如果接收過該事件,那麼newTouchTarget != 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;

                //找到了接收事件的View則break, 退出查詢過程
                break;
            }

            //清除child view的PFLAG_CANCEL_NEXT_UP_EVENT
            resetCancelNextUpFlag(child);

            //dispatchTransformedTouchEvent的主要功能,是按需調整MotionEvent,
            //然後遞交給child view的dispatchTouchEvent處理
            //當child為null時,由ViewGroup自己處理
            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                //一旦事件被消耗掉,則更新相關的狀態
                mLastTouchDownTime = ev.getDownTime();

                if (preorderedList != null) {
                    for (int j = 0; j < childrenCount; j++) {
                        if (children[childIndex] == mChildren[j]) {
                            mLastTouchDownIndex = j;
                            break;
                        }
                    }
                } else {
                    mLastTouchDownIndex = childIndex;
                }
                mLastTouchDownX = ev.getX();
                mLastTouchDownY = ev.getY();

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

    //處理沒有找到newTouchTarget的情況
    //將idBitsToAssign付給前一次的TouchTargets
    //這麼做的目的不是很清楚
    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;
    }
}
............

2.5 其它
對於ACTION_DOWN等型別的事件來說,嘗試查詢負責處理的View後,
需要開始進行後續的收尾工作。
對於其它型別的事件而言,這裡是處理的起點。

//Dispatch to touch targets.
if (mFirstTouchTarget == null) {
    // No touch targets so treat this as an ordinary view.
    // 分發MotionEvent失敗,沒有找到mFirstTouchTarget, 該事件將被遞交給ViewGroup處理
    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.
    // 進入到這個分支時,可能是ACTION_DOWN事件,也可能是其它型別的事件

    TouchTarget predecessor = null;
    TouchTarget target = mFirstTouchTarget;

    //輪詢連結串列,找到處理事件的target
    while (target != null) {
        final TouchTarget next = target.next;
        //這個if針對的是已經處理的ACTION_DOWN事件, 將handled置為true
        if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
            handled = true;
        } else {
            //針對未處理的ACTION_DOWN或其它型別事件

            //判斷是否取消
            final boolean cancelChild = resetCancelNextUpFlag(target.child)
                    || intercepted;

            //分發給子檢視
            if (dispatchTransformedTouchEvent(ev, cancelChild,
                    target.child, target.pointerIdBits)) {
                handled = true;
            }

            //如果某個檢視cancel, 就將其從連結串列中移除
            if (cancelChild) {
                if (predecessor == null) {
                    mFirstTouchTarget = next;
                } else {
                    predecessor.next = next;
                }
                target.recycle();
                target = next;
                continue;
           }
        }

        //更新“指標”
        predecessor = target;
        target = next;
    }
}
// 後續收尾工作,主要根據事件型別、處理的結果,更新一些狀態
...............

三、總結
至此,我們已經結合原始碼大致瞭解ViewGroup的事件分發邏輯。
這部分內容細節比較多,但整體邏輯比較清晰,大致如下圖所示:

ViewGroup有攔截事件的能力,同時也有分發事件給Child View的能力。
這導致ViewGroup的事件處理流程比較複雜。

相對而言,View處理觸控事件的流程較為簡單,我們在下一篇部落格中再來分析。