Android進階知識:事件分發與滑動衝突(一)

1、前言

Android學習一段時間,需求做多了必然會遇到滑動衝突問題,比如在一個ScrollView中要嵌套一個地圖View,這時候觸摸移動地圖或者放大縮小地圖就會變得不太準確甚至沒有反應,這就是遇到了滑動衝突,ScrollView中上下滑動與地圖的觸摸手勢發生衝突。想要解決滑動衝突就不得不提到Android的事件分發機制,只有吃透了事件分發,才能對滑動衝突的解決得心應手。

2、事件分發機制相關方法

Android事件分發機制主要相關方法有以下三個:

  • 事件分發:public boolean dispatchTouchEvent(MotionEvent ev)
  • 事件攔截:public boolean onInterceptTouchEvent(MotionEvent ev)
  • 事件響應:public boolean onTouchEvent(MotionEvent ev)

以下是這三個方法在Activity、ViewGroup和View中的存在情況:

相關方法 Activity ViewGroup View
dispatchTouchEvent yes yes yes
onInterceptTouchEvent no yes no
onTouchEvent yes yes yes

這三個方法都返回一個布爾類型,根據返回的不同對事件進行不同的分發攔截和響應。一般有三種返回truefalsesuper引用父類對應方法。

dispatchTouchEvent 返回true:表示改事件在本層不再進行分發且已經在事件分發自身中被消費了。
dispatchTouchEvent 返回 false:表示事件在本層不再繼續進行分發,並交由上層控件的onTouchEvent方法進行消費。

onInterceptTouchEvent 返回true:表示將事件進行攔截,並將攔截到的事件交由本層控件 的onTouchEvent 進行處理。
onInterceptTouchEvent 返回false:表示不對事件進行攔截,事件得以成功分發到子View。並由子ViewdispatchTouchEvent進行處理。

onTouchEvent 返回 true:表示onTouchEvent處理完事件後消費了此次事件。此時事件終結,將不會進行後續的傳遞。
onTouchEvent 返回 false:事件在onTouchEvent中處理後繼續向上層View傳遞,且有上層ViewonTouchEvent進行處理。

除此之外還有一個方法也是經常用到的:

  • public void requestDisallowInterceptTouchEvent(boolean disallowIntercept)

它的作用是子View用來通知父View不要攔截事件。下面先寫一個簡單的Demo來看一下事件分發和傳遞:

簡單的日誌的Demo:

這裏的代碼只是自定義了兩個ViewGroup和一個View,在其對應事件分發傳遞方法中打印日誌,來查看調用順序情況,所有相關分發傳遞方法返回皆是super父類方法。
例如: MyViewGroupA.java:

public class MyViewGroupA extends RelativeLayout {
    public MyViewGroupA(Context context) {
        super(context);
    }
    public MyViewGroupA(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public MyViewGroupA(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.d(getClass().getSimpleName(),"dispatchTouchEvent:ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d(getClass().getSimpleName(),"dispatchTouchEvent:ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.d(getClass().getSimpleName(),"dispatchTouchEvent:ACTION_UP");
                break;
        }        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_UP");
                break;
        }        return super.onInterceptTouchEvent(ev);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_UP");
                break;
        }        return super.onTouchEvent(event);
    }
}

其他的代碼都是類似的,這裏再貼一下Acitivity裏的佈局:

<?xml version="1.0" encoding="utf-8"?>
<com.example.sy.eventdemo.MyViewGroupA xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/viewGroupA"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/colorPrimary"
    tools:context=".MainActivity">

    <com.example.sy.eventdemo.MyViewGroupB
        android:id="@+id/viewGroupB"
        android:layout_width="300dp"
        android:layout_height="300dp"
        android:layout_centerInParent="true"
        android:background="@android:color/white">

        <com.example.sy.eventdemo.MyView
            android:id="@+id/myView"
            android:layout_width="200dp"
            android:layout_height="200dp"
            android:layout_centerInParent="true"
            android:background="@android:color/holo_orange_light" />
    </com.example.sy.eventdemo.MyViewGroupB>
</com.example.sy.eventdemo.MyViewGroupA>

Demo中的Activity佈局層級關係:


webp

除去外層Activity和Window的層級,從MyViewGroup開始是自己定義的打印日誌View。接下來運行Demo查看日誌:

 D/MainActivity: dispatchTouchEvent:ACTION_DOWN
 D/MyViewGroupA: dispatchTouchEvent:ACTION_DOWN
 D/MyViewGroupA: onInterceptTouchEvent:ACTION_DOWN
 D/MyViewGroupB: dispatchTouchEvent:ACTION_DOWN
 D/MyViewGroupB: onInterceptTouchEvent:ACTION_DOWN
 D/MyView: dispatchTouchEvent:ACTION_DOWN
 D/MyView: onTouchEvent:ACTION_DOWN
 D/MyViewGroupB: onTouchEvent:ACTION_DOWN
 D/MyViewGroupA: onTouchEvent:ACTION_DOWN
 D/MainActivity: onTouchEvent:ACTION_DOWN
 D/MainActivity: dispatchTouchEvent:ACTION_MOVE
 D/MainActivity: onTouchEvent:ACTION_MOVE
 D/MainActivity: dispatchTouchEvent:ACTION_UP
 D/MainActivity: onTouchEvent:ACTION_UP

結合日誌可以大概看出(先只看ACTION_DOWN事件):
事件的分發順序:Activity-->MyViewGroupA-->MyViewGroupB-->MyView自頂向下分發
事件的響應順序:MyView-->MyViewGroupB-->MyViewGroupA-->Activity自底向上響應消費

同時這裏通過日誌也發現一個問題:

  • 問題一爲什麼這裏只有ACTION_DOWN事件有完整的從Activity到ViewGroup再到View的分發攔截和響應的運行日誌,爲什麼ACTION_MOVEACTION_UP事件沒有?

接着再測試一下之前提的requestDisallowInterceptTouchEvent方法的使用。現在佈局文件中將MyView添加一個屬性android:clickable="true"。此時在運行點擊打印日誌是這樣的:

 /-------------------ACTION_DOWN事件------------------
 D/MainActivity: dispatchTouchEvent:ACTION_DOWN
 D/MyViewGroupA: dispatchTouchEvent:ACTION_DOWN
 D/MyViewGroupA: onInterceptTouchEvent:ACTION_DOWN
 D/MyViewGroupB: dispatchTouchEvent:ACTION_DOWN
 D/MyViewGroupB: onInterceptTouchEvent:ACTION_DOWN
 D/MyView: dispatchTouchEvent:ACTION_DOWN
 D/MyView: onTouchEvent:ACTION_DOWN
 /-------------------ACTION_MOVE事件------------------
 D/MainActivity: dispatchTouchEvent:ACTION_MOVE
 D/MyViewGroupA: dispatchTouchEvent:ACTION_MOVE
 D/MyViewGroupA: onInterceptTouchEvent:ACTION_MOVE
 D/MyViewGroupB: dispatchTouchEvent:ACTION_MOVE
 D/MyViewGroupB: onInterceptTouchEvent:ACTION_MOVE
 D/MyView: dispatchTouchEvent:ACTION_MOVE
 D/MyView: onTouchEvent:ACTION_MOVE
 /-------------------ACTION_UP事件------------------
 D/MainActivity: dispatchTouchEvent:ACTION_UP
 D/MyViewGroupA: dispatchTouchEvent:ACTION_UP
 D/MyViewGroupA: onInterceptTouchEvent:ACTION_UP
 D/MyViewGroupB: dispatchTouchEvent:ACTION_UP
 D/MyViewGroupB: onInterceptTouchEvent:ACTION_UP
 D/MyView: dispatchTouchEvent:ACTION_UP
 D/MyView: onTouchEvent:ACTION_UP

這下ACTION_MOVEACTION_UP事件也有日誌了。接下來在MyViewGroupB的onInterceptTouchEvent的方法中修改代碼如下:

 @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_DOWN");
                return false;
            case MotionEvent.ACTION_MOVE:
                Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_MOVE");
                return true;
            case MotionEvent.ACTION_UP:
                Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_UP");
                return true;
        }
        return false;
    }

也就是攔截下ACTION_MOVEACTION_UP事件不攔截下ACTION_DOWN事件,然後在運行查看日誌:

 /------------------ACTION_DOWN事件------------------------------
 D/MainActivity: dispatchTouchEvent:ACTION_DOWN
 D/MyViewGroupA: dispatchTouchEvent:ACTION_DOWN
 D/MyViewGroupA: onInterceptTouchEvent:ACTION_DOWN
 D/MyViewGroupB: dispatchTouchEvent:ACTION_DOWN
 D/MyViewGroupB: onInterceptTouchEvent:ACTION_DOWN
 D/MyView: dispatchTouchEvent:ACTION_DOWN
 D/MyView: onTouchEvent:ACTION_DOWN
 /------------------ACTION_MOVE事件-----------------------------
 D/MainActivity: dispatchTouchEvent:ACTION_MOVE
 D/MyViewGroupA: dispatchTouchEvent:ACTION_MOVE
 D/MyViewGroupA: onInterceptTouchEvent:ACTION_MOVE
 D/MyViewGroupB: dispatchTouchEvent:ACTION_MOVE
 D/MyViewGroupB: onInterceptTouchEvent:ACTION_MOVE
 /------------------ACTION_UP事件-------------------------------
 D/MainActivity: dispatchTouchEvent:ACTION_UP
 D/MyViewGroupA: dispatchTouchEvent:ACTION_UP
 D/MyViewGroupA: onInterceptTouchEvent:ACTION_UP
 D/MyViewGroupB: dispatchTouchEvent:ACTION_UP
 D/MyViewGroupB: onTouchEvent:ACTION_UP
 D/MainActivity: onTouchEvent:ACTION_UP

根據日誌可知ACTION_MOVEACTION_UP事件傳遞到MyViewGroupB就沒有再向MyView傳遞了。接着在MyView的onTouchEvent方法中調用requestDisallowInterceptTouchEvent方法通知父容器不要攔截事件。

 @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);
                Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_UP");
                break;
        }
        return super.onTouchEvent(event);
    }

再次運行查看日誌:

 /------------------ACTION_DOWN事件------------------------------
 D/MainActivity: dispatchTouchEvent:ACTION_DOWN
 D/MyViewGroupA: dispatchTouchEvent:ACTION_DOWN
 D/MyViewGroupA: onInterceptTouchEvent:ACTION_DOWN
 D/MyViewGroupB: dispatchTouchEvent:ACTION_DOWN
 D/MyViewGroupB: onInterceptTouchEvent:ACTION_DOWN
 D/MyView: dispatchTouchEvent:ACTION_DOWN
 D/MyView: onTouchEvent:ACTION_DOWN
 /------------------ACTION_MOVE事件-----------------------------
 D/MainActivity: dispatchTouchEvent:ACTION_MOVE
 D/MyViewGroupA: dispatchTouchEvent:ACTION_MOVE
 D/MyViewGroupB: dispatchTouchEvent:ACTION_MOVE
 D/MyView: dispatchTouchEvent:ACTION_MOVE
 D/MyView: onTouchEvent:ACTION_MOVE
 /------------------ACTION_UP事件-------------------------------
 D/MainActivity: dispatchTouchEvent:ACTION_UP
 D/MyViewGroupA: dispatchTouchEvent:ACTION_UP
 D/MyViewGroupB: dispatchTouchEvent:ACTION_UP
 D/MyView: dispatchTouchEvent:ACTION_UP
 D/MyView: onTouchEvent:ACTION_UP

這時可以發現ACTION_MOVEACTION_UP事件又傳遞到了MyView中並且兩個ViewGroup中都沒有執行onInterceptTouchEvent方法。 明顯是requestDisallowInterceptTouchEvent方法起了作用。但是又出現了兩個新問題。

  • 問題二:爲什麼將設置clickable="true"之後ACTION_MOVEACTION_UP事件就會執行了?
  • 問題三:requestDisallowInterceptTouchEvent方法是怎樣通知父View不攔截事件,爲什麼連onInterceptTouchEvent方法也不執行了?

想弄明白這些問題就只能到源碼中尋找答案了。

3、事件分發機制源碼

在正式看源碼之前先講一個概念:事件序列

我們常說的事件,一般是指從手指觸摸到屏幕在到離開屏幕這麼一個過程。在這個過程中其實會產生多個事件,一般是以ACTION_DOWN作爲開始,中間存在多個ACTION_MOVE,最後以ACTION_UP結束。我們稱一次ACTION_DOWN-->ACTION_MOVE-->ACTION_UP過程稱爲一個事件序列。

ViewGroup中有一個內部類TouchTarget,這個類將消費事件的View封裝成一個節點,使得可以將一個事件序列的DOWNMOVEUP事件構成一個單鏈表保存。ViewGroup中也有個TouchTarget類型的成員mFirstTouchTarget用來指向這個單鏈表頭。在每次DOWN事件開始時清空這個鏈表,成功消費事件後通過TouchTarget.obtain方法獲得一個TouchTarget,將消費事件的View傳入,然後插到單鏈表頭。後續MOVEUP事件可以通過判斷mFirstTouchTarget來知道之前是否有能夠消費事件的View。

TouchTarget的源碼:

private static final class TouchTarget {
        private static final int MAX_RECYCLED = 32;
        private static final Object sRecycleLock = new Object[0];
        private static TouchTarget sRecycleBin;
        private static int sRecycledCount;

        public static final int ALL_POINTER_IDS = -1; // all ones

        // The touched child view.
        //接受事件的View
        public View child;

        // The combined bit mask of pointer ids for all pointers captured by the target.
        public int pointerIdBits;

        // The next target in the target list.
        //下一個TouchTarget的地址
        public TouchTarget next;

        private TouchTarget() {
        }

        public static TouchTarget obtain(@NonNull View child, int pointerIdBits) {
            if (child == null) {
                throw new IllegalArgumentException("child must be non-null");
            }

            final TouchTarget target;
            synchronized (sRecycleLock) {
                if (sRecycleBin == null) {
                    target = new TouchTarget();
                } else {
                    target = sRecycleBin;
                    sRecycleBin = target.next;
                     sRecycledCount--;
                    target.next = null;
                }
            }
            target.child = child;
            target.pointerIdBits = pointerIdBits;
            return target;
        }

        public void recycle() {
            if (child == null) {
                throw new IllegalStateException("already recycled once");
            }

            synchronized (sRecycleLock) {
                if (sRecycledCount < MAX_RECYCLED) {
                    next = sRecycleBin;
                    sRecycleBin = this;
                    sRecycledCount += 1;
                } else {
                    next = null;
                }
                child = null;
            }
        }
    }
Activity中的dispatchTouchEvent方法:

接下來正式按照分發流程來閱讀源碼,從Activity的dispatchTouchEvent方法開始看起,事件產生時會先調用這個方法:

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

方法中先判斷事件類型是ACTION_DOWN事件會執行onUserInteraction方法,onUserInteraction方法在Activity中是一個空實現,在當前Activity下按下Home或者Back鍵時會調用此方法,這裏不是重點,這裏重點是關注下ACTION_DOWN事件,ACTION_DOWN類型事件的判斷,在事件傳遞的邏輯中非常重要,因爲每次點擊事件都是以ACTION_DOWN事件開頭,所以ACTION_DOWN事件又作爲一次新的點擊事件的標記。

緊接着看,在第二個if判斷中根據getWindow().superDispatchTouchEvent(ev)的返回值決定了整個方法的返回。

如果getWindow().superDispatchTouchEvent(ev)方法返回爲truedispatchTouchEvent方法返回true,否則則根據Activity中的onTouchEvent方法的返回值返回。

Activity中的onTouchEvent方法:

先來看Activity中的onTouchEvent方法:

  public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }
        return false;
    }

onTouchEvent方法中根據window的shouldCloseOnTouch方法決定返回的結果和是否finish當前Activity。進入抽象類Window查看shouldCloseOnTouch方法:

 /** @hide */
    public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
        if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN
                && isOutOfBounds(context, event) && peekDecorView() != null) {
            return true;
        }
        return false;
    }

這是個hide方法,判斷當前事件Event是否是ACTION_DOWN類型,當前事件點擊座標是否在範圍外等標誌位,如果爲true就會返回到onTouchEvent方法關閉當前Activity。

看完再回到dispatchTouchEvent方法中,只剩下getWindow().superDispatchTouchEvent(ev)方法,來看他啥時候返回true啥時候返回false。這裏的getWindow獲取到Activity中的Window對象,調用WidnowsuperDispatchTouchEvent(ev)方法,這個方法不在抽象類Window當中,這裏要去查看他的實現類PhoneWindow

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

superDispatchTouchEvent方法中又調用了mDecor.superDispatchTouchEvent方法,這裏的mDecor就是外層的DecorViewsuperDispatchTouchEvent方法:

public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }

方法中又調用了父類的dispatchTouchEvent方法,DecorView繼承自FrameLayout,而FrameLayout沒有重寫dispatchTouchEvent方法所以也就是調用了其父類ViewGroup的dispatchTouchEvent方法。

ViewGroup的dispatchTouchEvent方法:

通過以上這一系列的調用,事件終於從Activity到PhoneWindow再到DecorView最終走到了ViewGroup的dispatchTouchEvent方法中,接下來進入ViewGroup查看它的dispatchTouchEvent方法。

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        ......

        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
//-----------------代碼塊-1----------------------------------------------------------------
            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);
                resetTouchState();
            }
//------------------代碼塊-1--完------------------------------------------------------------
//------------------代碼塊-2----------------------------------------------------------------
            // Check for interception.
            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;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }
//------------------代碼塊-2--完----------------------------------------------------------
            // 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;
//------------------代碼塊-3--------------------------------------------------------------
            if (!canceled && !intercepted) {
             ......
                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;
                        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;
                            }
                            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);
                            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;
                    }
                }
            }
//------------------代碼塊-3--完----------------------------------------------------------
//------------------代碼塊-4--------------------------------------------------------------
            // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                //mFirstTouchTarget爲空說明沒有子View響應消費該事件
                // No touch targets so treat this as an ordinary view.
                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.
                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;
                }
            }
//------------------代碼塊-4--完----------------------------------------------------------
          ......

        return handled;
    }

ViewGroup的dispatchTouchEvent方法比較長,雖然已經省略了一部分代碼但代碼還是非常多,並且代碼中存在很多if-else判斷,容易看着看着就迷失在ifelse之間。所以這裏把他分成了四塊代碼來看。不過在看這四塊代碼之前先看dispatchTouchEvent方法中第一個if判斷:

        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)){
            ......
        }

這裏初始化的handled就是dispatchTouchEvent方法最後的返回值,onFilterTouchEventForSecurity這個方法過濾了認爲不安全的事件,方法裏主要是判斷了view和window是否被遮擋,dispatchTouchEvent方法中所有的分發邏輯都要在onFilterTouchEventForSecurity返回爲true的前提之下,否則直接返回handled即爲false
接下來看第一段代碼:

 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);
 resetTouchState();
 }

第一段比較少比較簡單,開始首先判斷事件類型ACTION_DOWN事件被認爲是一個新的事件序列開始,所以重置touch狀態,將mFirstTouchTarget鏈表置空。這裏可以進resetTouchState方法看下,方法中除了重置了一些狀態還調用了clearTouchTargets方法清空鏈表。

    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;
        }
    }    

接着看到代碼塊2:

            // Check for interception.
            //檢查是否攔截事件
            final boolean intercepted;
            //是ACTION_DOWN事件或者mFirstTouchTarget不爲空進入if
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                //繼續判斷是否在調用了requestDisallowInterceptTouchEvent(true)設置了禁止攔截標記
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                //設置禁止攔截設標記disallowIntercept爲true,!disallowIntercept即爲false
                if (!disallowIntercept) {
                    //根據ViewGroup的nInterceptTouchEvent(ev)方法返回是否攔截
                    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,就攔截
                intercepted = true;
            }

這段代碼中主要是判斷是否對事件進行攔截,intercepted是攔截標記,true代表攔截,false表示不攔截。這裏首先判斷是事件類型是DOWN或者mFirstTouchTarget不等於空(不等於空說明有子View消費了之前的DOWN事件),滿足這個條件,就進入if進一步判斷,否則直接設置interceptedfalse不攔截。在if中判斷FLAG_DISALLOW_INTERCEPT這個標記位,這個標記位就是在requestDisallowInterceptTouchEvent()方法中設置的。這裏跳到requestDisallowInterceptTouchEvent(true)方法來看一下:

@Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            // We're already in this state, assume our ancestors are too
            return;
        }

        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }

        // Pass it up to our parent
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }

看到requestDisallowInterceptTouchEvent方法里根據disallowIntercept進行不同位運算,mGroupFlags默認爲0,FLAG_DISALLOW_INTERCEPT0x80000,如果傳入設置爲true,則進行或運算,mGroupFlags結果爲0x80000,再回到代碼塊2裏和FLAG_DISALLOW_INTERCEPT做與運算結果仍爲0x80000,此時不等於0。反之傳入false,最終位運算結果爲0。也就是說調用requestDisallowInterceptTouchEvent方法傳入true導致disallowInterceptrue,進而導致if條件不滿足,使得interceptedfalse此時對事件進行攔截。反之,則進入if代碼塊調用onInterceptTouchEvent(ev)方法,根據返回值來決定是否攔截。

           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;
            //再判斷事件類型是DOWN事件繼續執行if代碼塊,這裏的三個標記分別對應單點觸摸DOWN多點觸摸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
                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);
                //這裏拿到子VIew個數
                final int childrenCount = mChildrenCount;
                //循環子View找到可以響應事件的子View將事件分發
                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;
                    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內就跳過這次循環
                        if (!canViewReceivePointerEvents(child)
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            ev.setTargetAccessibilityFocus(false);
                            continue;
                        }
                        //到這裏說明這個子View可以處理該事件,就到TochTarget鏈表裏去找對應的TochTarget,沒找到返回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.
                            //不爲空說明view已經處理過這個事件,說明是多點觸摸,就再加一個指針
                            newTouchTarget.pointerIdBits |= idBitsToAssign;
                            break;
                        }

                        resetCancelNextUpFlag(child);
                        //調用dispatchTransformedTouchEvent方法將事件分發給子View
                        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();
                            //dispatchTransformedTouchEvent返回true說明子View響應消費了這個事件
                            //於是調用addTouchTarget方法獲得包含這個View的TouchTarget節點並將其添加到鏈表頭
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            //將已經分發的標記設置爲true
                            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爲null且mFirstTouchTarget不爲null,說明沒找到子View來響應消費該事件,但是TouchTarget鏈表不爲空
                //則將newTouchTarget賦爲TouchTarget鏈表中mFirstTouchTarget.next
                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;
                }
            }
        }
webp


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章