大領導又給小明安排任務——Android觸控事件

Taylor發表於2019-03-04

這是Android觸控事件系列的第二篇,系列文章目錄如下:

  1. 大領導給小明安排任務——Android觸控事件
  2. 大領導又給小明安排任務——Android觸控事件

把上一篇中領導分配任務的故事,延展一下:

大領導安排任務會經歷一個“遞”的過程:大領導先把任務告訴小領導,小領導再把任務告訴小明。也可能會經歷一個“歸”的過程:小明告訴小領導做不了,小領導告訴大領導任務完不成。然後,就沒有然後了。。。。但如果這次完成了任務,大領導還會繼續將後序任務分配給小明。

故事的延展部分和今天要講的ACTION_DONW後序事件很類似,先來回答上一篇中遺留的另一個問題“攔截事件”:

攔截事件

ViewGroup在遍歷孩子分發觸控事件前還有一段攔截邏輯:

public abstract class ViewGroup extends View implements ViewParent, ViewManager {

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
            ...
            // Check for interception.
            //檢查ViewGroup是否要攔截觸控事件的下發
            final boolean intercepted;
            //第一個條件表示攔截ACTION_DOWN事件
            //第二個條件表示攔截ACTION_DOWN事件已經分發給孩子,現在攔截後序事件
            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 (!canceled && !intercepted) {
                //遍歷孩子並將事件分發給它們
                //如果有孩子聲稱要消費事件,則將其新增到觸控鏈上
                //這段邏輯在上一篇中分析過,這裡就省略了
            }
        }
        
        //將觸控事件分發給觸控鏈
        if (mFirstTouchTarget == null) { //沒有觸控鏈 
            //如果事件被ViewGroup攔截,則觸控鏈為空,ViewGroup自己消費事件
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
        } else {
            ...
        }
    }
    
    //返回true表示攔截事件,預設返回false
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return false;
    }
    
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,View child, int desiredPointerIdBits) {
        ...
        if (child == null) {
            //ViewGroup孩子都不願意接收觸控事件或者觸控事件被攔截 則其將自己當成View處理(呼叫View.dispatchTouchEvent())
            handled = super.dispatchTouchEvent(transformedEvent);
        }
        ...
    }
}
複製程式碼

當允許攔截時,onInterceptTouchEvent()會被呼叫,如果過載這個方法並且返回true,表示ViewGroup要對事件進行攔截,此時不再將事件分發給孩子而是自己消費(通過呼叫View.dispatchTouchEvent()最終走到ViewGroup.onTouchEvent())。

用一張圖總結一下:

圖1

  • 圖中黑色的箭頭表示觸控事件傳遞的路徑,灰色的箭頭表示觸控事件消費的回溯路徑。onInterceptTouchEvent()返回true,導致onTouchEvent()被呼叫,因為onTouchEvent()返回true,導致dispatchTouchEvent()返回true
  • 準確的說,攔截觸控事件的受益者是所有上層的ViewGroup(包括自己),因為觸控事件不再會向下層的View傳遞。

ACTION_MOVE 、 ACTION_UP

上一篇在閱讀原始碼的時候,埋下了一個伏筆,現在將其補全:

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    //觸控鏈頭結點
    private TouchTarget mFirstTouchTarget;
    ...
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (!canceled && !intercepted) {
            ...
            //當ACTION_DOWN的時候才遍歷尋找消費觸控事件的孩子,若找到則將其加入到觸控鏈
            if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                //遍歷孩子
                for (int i = childrenCount - 1; i >= 0; i--) {
                    ...
                    //轉換觸控座標並分發給孩子(child引數不為null)
                    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                          ...
                          //有孩子願意消費觸控事件,將其插入“觸控鏈”
                          newTouchTarget = addTouchTarget(child, idBitsToAssign);
                          //表示已經將觸控事件分發給新的觸控目標
                          alreadyDispatchedToNewTouchTarget = true;
                          break;
                    }
                     ...
                }
            }
        }
    
        if (mFirstTouchTarget == null) {
                //如果沒有孩子願意消費觸控事件,則自己消費(child引數為null)
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
        } 
        //觸控鏈不為null,表示有孩子消費了ACTION_DOWN
        else {
                //將伏筆補全
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                //遍歷觸控鏈將ACTION_DOWN的後序事件分發給孩子
                while (target != null) {
                    final TouchTarget next = target.next;
                    //上一篇分析了,ACTION_DOWN會走這裡
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        //如果已經將觸控事件分發給新的觸控目標,則返回true
                        handled = true;
                    } 
                    //ACTION_DONW的後序事件走這裡
                    else {
                        ...
                        //將觸控事件分發給觸控鏈上的觸控目標
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        ...
                    }
                    predecessor = target;
                    target = next;
                }
        }
        ...
        if (canceled
            || actionMasked == MotionEvent.ACTION_UP
            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                //如果是ACTION_UP事件,則將觸控鏈清空
                resetTouchState();
        }

        return handled;
    }
    
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;
        ...
        // Perform any necessary transformations and dispatch.
        //進行必要的座標轉換然後分發觸控事件
        if (child == null) {
            //ViewGroup孩子都不願意消費觸控事件 則其將自己當成View處理(呼叫View.dispatchTouchEvent())
            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);
        }
        ...
        return handled;
    }
    
    /**
     * Resets all touch state in preparation for a new cycle.
     * 重置Touch標誌
     */
    private void resetTouchState() {
        clearTouchTargets();
        resetCancelNextUpFlag(this);
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        mNestedScrollAxes = SCROLL_AXIS_NONE;
    }
    
    /**
     * Clears all touch targets.
     * 清空觸控鏈
     */
    private void clearTouchTargets() {
        TouchTarget target = mFirstTouchTarget;
        if (target != null) {
            do {
                TouchTarget next = target.next;
                target.recycle();
                target = next;
            } while (target != null);
            mFirstTouchTarget = null;
        }
    }
}
複製程式碼

觸控事件是一個序列,序列總是以ACTION_DOWN開始,緊接著有ACTION_MOVEACTION_UPACTION_DOWN發生時,ViewGroup.dispatchTouchEvent()會將願意消費觸控事件的孩子儲存在觸控鏈中,當後序事件會分發給觸控鏈上的物件。

用兩張圖總結一下:

圖2

  • 圖中黑色箭頭表示ACTION_DOWN事件的傳遞路徑,灰色箭頭表示ACTION_MOVEACTION_UP事件的傳遞路徑。即只要有檢視聲稱消費ACTION_DOWN,則其後序事件也傳遞給它,不管它是否聲稱消費ACTION_MOVEACTION_UP,如果它不消費,則後序事件會像上一篇分析的ACTION_DOWN一樣向上回溯給上層消費。

圖3

  • 圖中黑色箭頭表示ACTION_DOWN事件的傳遞路徑,灰色箭頭表示ACTION_MOVEACTION_UP事件的傳遞路徑。即所有檢視都不消費ACTION_DOWN,則其後序事件只會傳遞給Activity.onTouchEvent()

ACTION_CANCEL

把領導佈置任務的故事繼續延展一下:大領導給小領導佈置了任務1,小領導把他傳遞給小明,小明完成了。緊接著大領導給小領導佈置了任務2,小領導決定自己處理任務2,於是他和小明說後序任務我來接手,你可以忙別的事情。

故事對應的觸控事件傳遞場景是:ActivityACTION_DOWN傳遞給ViewGroupViewGroup將其傳遞給ViewView聲稱消費ACTION_DOWNActivity繼續將ACTION_MOVE傳遞給ViewGroup,但ViewGroup對其做了攔截,此時ViewGroup會傳送ACTION_CANCEL事件給View

看下原始碼:

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    public boolean dispatchTouchEvent(MotionEvent ev) {
        //檢查ViewGroup是否要攔截觸控事件的下發
        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;
                }
        }
        ...
        //如果孩子消費ACTION_DOWN事件,則會在這裡將其新增到觸控鏈中
        if (!canceled && !intercepted) {
            ...
        }
        //將觸控事件分發給觸控鏈
        if (mFirstTouchTarget == null) { //沒有觸控鏈 表示當前ViewGroup中沒有孩子願意接收觸控事件
            //將觸控事件分發給自己
        } 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 {
                    //如果事件被攔截則cancelChild為true
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                    //將ACTION_CANCEL事件傳遞給孩子
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    //如果傳送了ACTION_CANCEL事件,將孩子從觸控鏈上摘除
                    if (cancelChild) {
                        if (predecessor == null) {
                            mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                    }
                }
                predecessor = target;
                target = next;
            }
        }
        ...
    }
    
    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);
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                //將ACTION_CANCEL事件傳遞給孩子
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
    }
    ...
}
複製程式碼

當孩子消費了ACTION_DOWN事件,它的引用被會儲存在父親的觸控鏈中。當父親攔截後序事件時,父親會向觸控鏈上的孩子傳送ACTION_CANCEL事件,並將孩子從觸控鏈上摘除。後序事件就傳遞到父親為止。

總結

經過兩篇文章的分析,對Android觸控事件的分發有了初步的瞭解,得出了以下結論:

  • Activity接收到觸控事件後,會傳遞給PhoneWindow,再傳遞給DecorView,由DecorView呼叫ViewGroup.dispatchTouchEvent()自頂向下分發ACTION_DOWN觸控事件。
  • ACTION_DOWN事件通過ViewGroup.dispatchTouchEvent()DecorView經過若干個ViewGroup層層傳遞下去,最終到達View
  • 每個層次都可以通過在onTouchEvent()OnTouchListener.onTouch()返回true,來告訴自己的父控制元件觸控事件被消費。在父控制元件不攔截事件的情況下,只有當下層控制元件不消費觸控事件時,其父控制元件才有機會自己消費。
  • 觸控事件的傳遞是從根檢視自頂向下“遞”的過程,觸控事件的消費是自下而上“歸”的過程。
  • ACTION_MOVEACTION_UP會沿著剛才ACTION_DOWN的傳遞路徑,傳遞給消費了ACTION_DOWN的控制元件,如果該控制元件沒有宣告消費這些後序事件,則它們也像ACTION_DOWN一樣會向上回溯讓其父控制元件消費。
  • 父控制元件可以通過在onInterceptTouchEvent()返回true來攔截事件向其孩子傳遞。如果在孩子已經消費了ACTION_DOWN事情後才進行攔截,父控制元件會傳送ACTION_CANCEL給孩子。

相關文章