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

這是Android觸摸事件系列的第二篇,系列文章目錄以下:bash

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

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

大領導安排任務會經歷一個「遞」的過程:大領導先把任務告訴小領導,小領導再把任務告訴小明。也可能會經歷一個「歸」的過程:小明告訴小領導作不了,小領導告訴大領導任務完不成。而後,就沒有而後了。。。。但若是此次完成了任務,大領導還會繼續將後序任務分配給小明。post

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

攔截事件

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

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())。spa

用一張圖總結一下: 3d

圖1

  • 圖中黑色的箭頭表示觸摸事件傳遞的路徑,灰色的箭頭表示觸摸事件消費的回溯路徑。onInterceptTouchEvent()返回true,致使onTouchEvent()被調用,由於onTouchEvent()返回true,致使dispatchTouchEvent()返回true
  • 準確的說,攔截觸摸事件的受益者是全部上層的ViewGroup(包括本身),由於觸摸事件再也不會向下層的View傳遞。

ACTION_MOVE 、 ACTION_UP

上一篇在閱讀源碼的時候,埋下了一個伏筆,如今將其補全:rest

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()會將願意消費觸摸事件的孩子存儲在觸摸鏈中,當後序事件會分發給觸摸鏈上的對象。code

用兩張圖總結一下: orm

圖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給孩子。
相關文章
相關標籤/搜索