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

一、前言

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

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

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

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

如下是這三個方法在Activity、ViewGroup和View中的存在狀況:api

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

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

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

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

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

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

  • public void requestDisallowInterceptTouchEvent(boolean disallowIntercept)

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

簡單的日誌的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佈局層級關係:

除去外層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方法也不執行了?

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

三、事件分發機制源碼

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

咱們常說的事件,通常是指從手指觸摸到屏幕在到離開屏幕這麼一個過程。在這個過程當中其實會產生多個事件,通常是以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; } } } 複製代碼

接着看代碼塊3,在這段很長的代碼裏,首先一個if中判斷了該事件是否知足沒有被攔截和被取消,以後第二個if判斷了事件類型是否爲DOWN,知足了沒有被攔截和取消的DOWN事件,接下來ViewGroup纔會循環其子View找到點擊事件在其內部而且可以接受該事件的子View,再經過調用dispatchTransformedTouchEvent方法將事件分發給該子View處理,返回true說明子View成功消費事件,因而調用addTouchTarget方法,方法中經過TouchTarget.obtain方法得到一個包含這View的TouchTarget節點並將其添加到鏈表頭,並將已經分發的標記設置爲true
接下來看代碼塊4:

// Dispatch to touch targets.
			//走到這裏說明在循環遍歷全部子View後沒有找到接受該事件或者事件不是DOWN事件或者該事件已被攔截或取消	
            if (mFirstTouchTarget == null) {
				//mFirstTouchTarget爲空說明沒有子View響應消費該事件
				//全部調用dispatchTransformedTouchEvent方法分發事件
				//注意這裏第三個參數傳的是null,方法裏會調用super.dispatchTouchEvent(event)即View.dispatchTouchEvent(event)方法
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
				 // mFirstTouchTarget不爲空說明有子View能響應消費該事件,消費過以前的DOWN事件,就將這個事件還分發給這個View
                // 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;
							//這裏傳入的是target.child就是以前響應消費的View,把該事件還交給它處理
                        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;
                }
            }
複製代碼

以前在代碼塊3中處理分發了沒被攔截和取消的DOWN事件,那麼其餘MOVEUP等類型事件怎麼處理呢?還有若是遍歷完子View卻沒有能接受這個事件的View又怎麼處理呢?代碼塊4中就處理分發了這些事件。首先判斷mFirstTouchTarget是否爲空,爲空說明沒有子View消費該事件,因而就調用dispatchTransformedTouchEvent方法分發事件,這裏注意dispatchTransformedTouchEvent方法第三個參數View傳的null,方法裏會對於這種沒有子View能處理消費事件的狀況,就調用該ViewGroup的super.dispatchTouchEvent方法,即View的dispatchTouchEvent,把ViewGroup當成View來處理,把事件交給ViewGroup處理。具體看dispatchTransformedTouchEvent方法中的這段代碼:

if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
複製代碼

dispatchTransformedTouchEvent方法中child即傳入的View爲空則調用super.dispatchTouchEvent方法分發事件,就是View類的分發方法,不爲空則調用子View方法,即child.dispatchTouchEvent分發事件,因此歸根結底都是調用了View類的dispatchTouchEvent方法處理。

至此,ViewGroup中的分發過流程結束,再來總結一下這個過程:首先過濾掉不安全的事件,接着若是事件類型是DOWN事件認爲是一個新的事件序列開始,就清空TouchTarget鏈表重置相關標誌位(代碼塊一),而後判斷是否攔截該事件,這裏有兩步判斷:一是若是是DOWN事件或者不是DOWN事件可是mFirstTouchTarget不等於null(這裏mFirstTouchTarget若是等於null說明以前沒有View消費DOWN事件,在代碼塊三末尾,能夠看到若是有子View消費了DOWN事件,會調用addTouchTarget方法,得到一個保存了該子View的TouchTarget,並將其添加到mFirstTouchTarget鏈表頭),則進入第二步禁止攔截標記的判斷,不然直接設置爲須要攔截,進入第二步判斷設置過禁止攔截標記爲true的就不攔截,不然調用ViewGroup的onInterceptTouchEvent方法根據返回接過來決定是否攔截(代碼塊二)。接下來若是事件沒被攔截也沒被取消並且仍是DOWN事件,就循環遍歷ViewGroup中的子View找到事件在其範圍內而且能接受事件的子View,經過dispatchTransformedTouchEvent方法將事件分發給該子View,而後經過addTouchTarget方法將包含該子View的TouchTarget插到鏈表頭(代碼塊三)。最後若是沒有找到可以接受該事件的子View又或者是MOVEUP類型事件等再判斷mFirstTouchTarget是否爲空,爲空說明以前沒有View能接受消費該事件,則調用dispatchTransformedTouchEvent方法將事件交給自身處理,不爲空則一樣調用dispatchTransformedTouchEvent方法,可是是將事件分發給該子View處理。

ViewGroup的onInterceptTouchEvent方法:
public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                && ev.getAction() == MotionEvent.ACTION_DOWN
                && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                && isOnScrollbarThumb(ev.getX(), ev.getY())) {
            return true;
        }
        return false;
    }
複製代碼

在ViewGroup的dispatchTouchEvent中沒設置過禁止攔截的事件默認都會經過onInterceptTouchEvent方法來決定是否攔截,onInterceptTouchEvent方法裏能夠看到默認是返回false,只有在事件源類型是鼠標而且是DOWN事件是鼠標點擊按鈕和是滾動條的手勢時才返回true。因此默認通常ViewGroup的onInterceptTouchEvent方法返回都爲false,也就是說默認不攔截事件。

ViewGroup的onTouchEvent方法:

ViewGroup中沒有覆蓋onTouchEvent方法,因此調用ViewGroup的onTouchEvent方法實際上調用的仍是它的父類View的onTouchEvent方法。

View的dispatchTouchEvent方法:

在ViewGroup中將事件不管是分發給子View的時候仍是本身處理的,最終都會執行默認的View類的dispatchTouchEvent方法:

public boolean dispatchTouchEvent(MotionEvent event) {
        ......
        boolean result = false;
        ......
        if (onFilterTouchEventForSecurity(event)) {
        
          ......
          
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
         ......
        return result;
    }
複製代碼

這裏一樣省略一些代碼只看關鍵的,首先一樣和ViewGroup同樣,作了事件安全性的過濾,接着先判斷了mOnTouchListener是否爲空,不爲空而且該View是ENABLED可用的,就會調用mOnTouchListeneronTouch方法,若是onTouch方法返回true說明事件已經被消費了,就將result標記修改成true,這樣他就不會走接下來的if了。若是沒有設置mOnTouchListener或者onTouch方法返回false,則會繼續調用onTouchEvent方法。這裏能夠發現mOnTouchListeneronTouch方法的優先級是在onTouchEvent以前的,若是在代碼中設置了mOnTouchListener監聽,而且onTouch返回true,則這個事件就被在onTouch裏消費了,不會在調用onTouchEvent方法。

//這個mOnTouchListener就是常常在代碼裏設置的View.OnTouchListener
mMyView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                //這裏返回true事件就消費了,不會再調用onTouchEvent方法
                return true;
            }
        });
複製代碼
View的onTouchEvent方法:
public boolean onTouchEvent(MotionEvent event) {
 /---------------代碼塊-1-------------------------------------------------------------------
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();

        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them. return clickable; } /---------------代碼塊-1------完------------------------------------------------------------- /---------------代碼塊-2------------------------------------------------------------------- if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } /---------------代碼塊-2------完------------------------------------------------------------- /---------------代碼塊-3------------------------------------------------------------------- if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { switch (action) { case MotionEvent.ACTION_UP: mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; if ((viewFlags & TOOLTIP) == TOOLTIP) { handleTooltipUp(); } if (!clickable) { removeTapCallback(); removeLongPressCallback(); mInContextButtonPress = false; mHasPerformedLongPress = false; mIgnoreNextUpEvent = false; break; } boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed.  Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            setPressed(true, x, y);
                        }

                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {
                                // Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
									//調用了OnClickListener
                                    performClick();
                                }
                            }
                        }

                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }

                        if (prepressed) {
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            // If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }

                        removeTapCallback();
                    }
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_DOWN:
                    if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                        mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                    }
                    mHasPerformedLongPress = false;

                    if (!clickable) {
                        checkForLongClick(0, x, y);
                        break;
                    }

                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }

                    // Walk up the hierarchy to determine if we're inside a scrolling container. boolean isInScrollingContainer = isInScrollingContainer(); // For views inside a scrolling container, delay the pressed feedback for // a short period in case this is a scroll. if (isInScrollingContainer) { mPrivateFlags |= PFLAG_PREPRESSED; if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } mPendingCheckForTap.x = event.getX(); mPendingCheckForTap.y = event.getY(); postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { // Not inside a scrolling container, so show the feedback right away setPressed(true, x, y); checkForLongClick(0, x, y); } break; case MotionEvent.ACTION_CANCEL: if (clickable) { setPressed(false); } removeTapCallback(); removeLongPressCallback(); mInContextButtonPress = false; mHasPerformedLongPress = false; mIgnoreNextUpEvent = false; mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; break; case MotionEvent.ACTION_MOVE: if (clickable) { drawableHotspotChanged(x, y); } // Be lenient about moving outside of buttons if (!pointInView(x, y, mTouchSlop)) { // Outside button // Remove any future long press/tap checks removeTapCallback(); removeLongPressCallback(); if ((mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; } break; } return true; } /---------------代碼塊-3------完------------------------------------------------------------- return false; } 複製代碼

onTouchEvent方法裏的代碼也很多,不過大部分都是響應事件的一些邏輯,與事件分發流程關係不大。仍是分紅三塊,先看第一個代碼塊:

final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();
        //這裏CLICKABLE、CONTEXT_CLICKABLE和CONTEXT_CLICKABLE有一個知足,clickable就爲true
        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
        //這裏先判斷當前View是否可用,若是是不可用進入if代碼塊
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
        //若是是UP事件而且View處於PRESSED狀態,則調用setPressed設置爲false
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them. //這裏若是View是不可用狀態,就直接返回clickable狀態,不作任何處理 return clickable; } 複製代碼

代碼塊1中首先得到View是否可點擊clickable,而後判斷View若是是不可用狀態就直接返回clickable,可是沒作任何響應。View默認的clickablefalseEnabledture,不一樣的View的clickable默認值也不一樣,Button默認clickabletrueTextView默認爲false

if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }
複製代碼

代碼塊2中會對一個mTouchDelegate觸摸代理進行判斷,不爲空會調用代理的onTouchEvent響應事件而且返回true

if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    if ((viewFlags & TOOLTIP) == TOOLTIP) {
                        handleTooltipUp();
                    }
                    if (!clickable) {
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        break;
                    }
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        // take focus if we don't have it already and we should in // touch mode. boolean focusTaken = false; if (isFocusable() && isFocusableInTouchMode() && !isFocused()) { focusTaken = requestFocus(); } if (prepressed) { // The button is being released before we actually // showed it as pressed. Make it show the pressed // state now (before scheduling the click) to ensure // the user sees it. setPressed(true, x, y); } if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { // This is a tap, so remove the longpress check removeLongPressCallback(); // Only perform take click actions if we were in the pressed state if (!focusTaken) { // Use a Runnable and post this rather than calling // performClick directly. This lets other visual state // of the view update before click actions start. if (mPerformClick == null) { mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { //調用了OnClickListener performClick(); } } } if (mUnsetPressedState == null) { mUnsetPressedState = new UnsetPressedState(); } if (prepressed) { postDelayed(mUnsetPressedState, ViewConfiguration.getPressedStateDuration()); } else if (!post(mUnsetPressedState)) { // If the post failed, unpress right now mUnsetPressedState.run(); } removeTapCallback(); } mIgnoreNextUpEvent = false; break; case MotionEvent.ACTION_DOWN: if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) { mPrivateFlags3 |= PFLAG3_FINGER_DOWN; } mHasPerformedLongPress = false; if (!clickable) { checkForLongClick(0, x, y); break; } if (performButtonActionOnTouchDown(event)) { break; } // Walk up the hierarchy to determine if we're inside a scrolling container.
                    boolean isInScrollingContainer = isInScrollingContainer();

                    // For views inside a scrolling container, delay the pressed feedback for
                    // a short period in case this is a scroll.
                    if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        // Not inside a scrolling container, so show the feedback right away
                        setPressed(true, x, y);
                        checkForLongClick(0, x, y);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    if (clickable) {
                        setPressed(false);
                    }
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    break;

                case MotionEvent.ACTION_MOVE:
                    if (clickable) {
                        drawableHotspotChanged(x, y);
                    }

                    // Be lenient about moving outside of buttons
                    if (!pointInView(x, y, mTouchSlop)) {
                        // Outside button
                        // Remove any future long press/tap checks
                        removeTapCallback();
                        removeLongPressCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            setPressed(false);
                        }
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    }
                    break;
            }

            return true;
        }
複製代碼

代碼塊3中首先判斷了 clickable || (viewFlags & TOOLTIP) == TOOLTIP 知足了這個條件就返回true消費事件。接下來的switch中主要對事件四種狀態分別作了處理。這裏稍微看下在UP事件中會調用一個performClick方法,方法中調用了OnClickListeneronClick方法。

public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        notifyEnterOrExitForAutoFillIfNeeded(true);
        return result;
    }
複製代碼

最後看到onTouchEvent的最後一行默認返回的仍是false,就是說只有知足上述的條件之一纔會返回ture
至此事件分發的相關源碼就梳理完了,我畫了幾張流程圖,能更清晰的理解源碼邏輯。

ViewGroup的dispatchTouchEvent邏輯:

ViewGroup的dispatchTouchEvent邏輯
View的dispathTouchEvent邏輯:
View的dispathTouchEvent邏輯
事件分發總體邏輯

四、事件分發機制相關問題

閱讀了源碼以後,先來解決以前提到的三個問題。

Q1:爲何日誌Demo中只有ACTION_DOWN事件有完整的從Activity到ViewGroup再到View的分發攔截和響應的運行日誌,爲何ACTION_MOVEACTION_UP事件沒有?

A1:日誌Demo代碼全部事件傳遞方法都是默認調用super父類對應方法,因此根據源碼邏輯可知當事件序列中的第一個DOWN事件來臨時,會按照Activity-->MyViewGroupA-->MyViewGroupB-->MyView的順序分發,ViewGroup中onInterceptTouchEvent方法默認返回false不會攔截事件,最終會找到合適的子View(這裏即MyView)dispatchTransformedTouchEvent方法,將事件交給子View的dispatchTouchEvent處理,在dispatchTouchEvent方法中默認會調用View的onTouchEvent方法處理事件,這裏由於MyView是繼承View的,因此默認clickablefalse,而onTouchEvent方法中當clickablefalse時默認返回的也是false。最終致使ViewGroup中dispatchTransformedTouchEvent方法返回爲false。進而致使mFirstTouchTarget爲空,因此後續MOVEUP事件到來時,由於mFirstTouchTarget爲空,事件攔截標記直接設置爲true事件被攔截,就不會繼續向下分發,最終事件無人消費就返回到Activity的onTouchEvent方法。因此就會出現這樣的日誌輸出。

if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action);  
                } else {
                    intercepted = false;
                }
            } else {
                //mFirstTouchTarget爲空intercepted爲true且不會調用onInterceptTouchEvent方法
                intercepted = true;
            }
複製代碼
Q2:爲何將設置clickable="true"以後ACTION_MOVEACTION_UP事件就會執行了?

A2:如A1中所說,clickable設置爲true,View的onTouchEvent方法的返回就會爲true,消費了DOWN事件,就會建立一個TouchTarget插到單鏈表頭,mFirstTouchTarget就不會是空了,MOVEUP事件到來時,就會由以前消費了DOWN事件的View來處理消費MOVEUP事件。

Q3:requestDisallowInterceptTouchEvent方法是怎樣通知父View不攔截事件,爲何連onInterceptTouchEvent方法也不執行了?

A3:源碼閱讀是有看到,requestDisallowInterceptTouchEvent方法時經過位運算設置標誌位,在調用傳入參數爲true後,事件在分發時disallowIntercept會爲true!disallowIntercept即爲false,致使事件攔截標記interceptedfalse,不會進行事件攔截。

Q4:View.OnClickListeneronClick方法與View.OnTouchListeneronTouch執行順序?

A4::View.OnClickListeneronClick方法是在View的onTouchEventperformClick方法中調用的。 而View.OnTouchListeneronTouch方法在View的dispatchTouchEvent方法中看到是比onTouchEvent方法優先級高的,而且只要OnTouchListener.Touch返回爲true,就只會調用OnTouchListener.onTouch方法不會再調用onTouchEvent方法。因此View.OnClickListeneronClick方法順序是在View.OnTouchListeneronTouch以後的。

五、滑動衝突

關於滑動衝突,在《Android開發藝術探索》中有詳細說明,我這裏把書上的方法結論與具體實例結合起來作一個總結。

1.滑動衝突的場景

常見的場景有三種:

  • 外部滑動與內部滑動方向不一致
  • 外部滑動與內部滑動方向一致
  • 前兩種狀況的嵌套
2.滑動衝突的處理規則

不一樣的場景有不一樣的處理規則,例如上面的場景一,規則通常就是當左右滑動時,外部View攔截事件,當上下滑動時要讓內部View攔截事件,這時候處理滑動衝突就能夠根據滑動是水平滑動仍是垂直滑動來判斷誰來攔截事件。場景而這種同個方向上的滑動衝突通常要根據業務邏輯來處理規則,何時要外部View攔截,何時要內部View攔截。場景三就更加複雜了,可是一樣是根據具體業務邏輯,來判斷具體的滑動規則。

3.滑動衝突的解決方法
  • 外部攔截法
  • 內部攔截法

外部攔截法是從父View着手,全部事件都要通過父View的分發和攔截,何時父View須要事件,就將其攔截,不須要就不攔截,經過重寫父View的onInterceptTouchEvent方法來實現攔截規則。

private int mLastXIntercept;
    private int mLastYIntercept;
    public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean intercepted = false;
        int x = (int)event.getX();
        int y = (int)event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                intercepted = false;
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                if (知足父容器的攔截要求) {
                    intercepted = true;
                } else {
                    intercepted = false;
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                intercepted = false;
                break;
            }
            default:
                break;
        }
        mLastXIntercept = x;
        mLastYIntercept = y;
        return intercepted;
    }
複製代碼

按照以上僞代碼,根據不一樣的攔截要求進行修改就能夠解決滑動衝突。

內部攔截法的思想是父View不攔截事件,由子View來決定事件攔截,若是子View須要此事件就直接消耗掉,若是不須要就交給父View處理。這種方法須要配合requestDisallowInterceptTouchEvent方法來實現。

private int  mLastX;
private int  mLastY;
@Override
 public boolean dispatchTouchEvent(MotionEvent event) {
     int x = (int) event.getX();
     int y = (int) event.getY();

     switch (event.getAction()) {
     case MotionEvent.ACTION_DOWN: {
         parent.requestDisallowInterceptTouchEvent(true);
         break;
     }
     case MotionEvent.ACTION_MOVE: {
         int deltaX = x - mLastX;
         int deltaY = y - mLastY;
         if (父容器須要此類點擊事件) {
             parent.requestDisallowInterceptTouchEvent(false);
         }
         break;
     }
     case MotionEvent.ACTION_UP: {
         break;
     }
     default:
         break;
     }
     mLastX = x;
     mLastY = y;
     return super.dispatchTouchEvent(event);
 }  
 
 //父View的onInterceptTouchEvent方法
  @Override
 public boolean onInterceptTouchEvent(MotionEvent event) {
     int action = event.getAction();
     if (action == MotionEvent.ACTION_DOWN) {
         return false;
     } else {
         return true;
     }
 }  
複製代碼

這裏父View不攔截ACTION_DOWN方法的緣由,根據以前的源碼閱讀可知若是ACTION_DOWN事件被攔截,以後的全部事件就都不會再傳遞下去了。

4.滑動衝突實例

實例一:ScrollView與ListView嵌套
這個實例是同向滑動衝突,先看佈局文件:

<?xml version="1.0" encoding="utf-8"?>
<cScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/scrollView1"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ScrollDemo1Activity">

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <com.example.sy.eventdemo.MyView
            android:layout_width="match_parent"
            android:layout_height="350dp"
            android:background="#27A3F3"
            android:clickable="true" />

        <ListView
            android:id="@+id/lv"
            android:layout_width="match_parent"
            android:background="#E5F327"
            android:layout_height="300dp"></ListView>

        <com.example.sy.eventdemo.MyView
            android:layout_width="match_parent"
            android:layout_height="500dp"
            android:background="#0AEC2E"
            android:clickable="true" />
    </LinearLayout>
</cScrollView>
複製代碼

這裏MyView就是以前打印日誌的View沒有作任何其餘處理,用於佔位使ScrollView超出一屏能夠滑動。
運行效果:

能夠看到ScrollView與ListView發生滑動衝突,ListView的滑動事件沒有觸發。接着來解決這個問題,用內部攔截法。

首先自定義ScrollView,重寫他的onInterceptTouchEvent方法,攔擊除了DOWN事件之外的事件。

public class MyScrollView extends ScrollView {

    public MyScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onTouchEvent(ev);
            return false;
        }
        return true;
    }

}
複製代碼

這裏沒有攔截DOWN事件,因此DOWN事件沒法進入ScrollView的onTouchEvent事件,又由於ScrollView的滾動須要在onTouchEvent方法中作一些準備,因此這裏手動調用一次。接着再自定義一個ListView,來決定事件攔截,重寫dispatchTouchEvent方法。

package com.example.sy.eventdemo;

import android.content.Context;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.ListView;

/**
 * Create by SY on 2019/4/22
 */
public class MyListView extends ListView {
    public MyListView(Context context) {
        super(context);
    }

    public MyListView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyListView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    private float lastY;

    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            getParent().getParent().requestDisallowInterceptTouchEvent(true);
        } else if (ev.getAction() == MotionEvent.ACTION_MOVE) {
            if (lastY > ev.getY()) {
                // 這裏判斷是向上滑動,並且不能再向上滑了,說明到頭了,就讓父View處理
                if (!canScrollList(1)) {
                    getParent().getParent().requestDisallowInterceptTouchEvent(false);
                    return false;
                }
            } else if (ev.getY() > lastY) {
                // 這裏判斷是向下滑動,並且不能再向下滑了,說明到頭了,一樣讓父View處理
                if (!canScrollList(-1)) {
                    getParent().getParent().requestDisallowInterceptTouchEvent(false);
                    return false;
                }
            }
        }
        lastY = ev.getY();
        return super.dispatchTouchEvent(ev);
    }
}

複製代碼

判斷是向上滑動仍是向下滑動,是否滑動到頭了,若是滑到頭了就讓父View攔截事件由父View處理,不然就由本身處理。將佈局文件中的空間更換。

<?xml version="1.0" encoding="utf-8"?>
<com.example.sy.eventdemo.MyScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/scrollView1"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ScrollDemo1Activity">

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <com.example.sy.eventdemo.MyView
            android:layout_width="match_parent"
            android:layout_height="350dp"
            android:background="#27A3F3"
            android:clickable="true" />

        <com.example.sy.eventdemo.MyListView
            android:id="@+id/lv"
            android:layout_width="match_parent"
            android:background="#E5F327"
            android:layout_height="300dp"></com.example.sy.eventdemo.MyListView>

        <com.example.sy.eventdemo.MyView
            android:layout_width="match_parent"
            android:layout_height="500dp"
            android:background="#0AEC2E"
            android:clickable="true" />
    </LinearLayout>
</com.example.sy.eventdemo.MyScrollView>
複製代碼

運行結果:

實例二:ViewPager與ListView嵌套
這個例子是水平和垂直滑動衝突。使用V4包中的ViewPager與ListView嵌套並不會發生衝突,是由於在ViewPager中已經實現了關於滑動衝突的處理代碼,因此這裏自定義一個簡單的ViewPager來測試衝突。佈局文件裏就一個ViewPager:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ScrollDemo2Activity">

    <com.example.sy.eventdemo.MyViewPager
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"></com.example.sy.eventdemo.MyViewPager>
</LinearLayout>
複製代碼

ViewPager的每一個頁面的佈局也很簡單就是一個ListView:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ScrollDemo2Activity">

    <com.example.sy.eventdemo.MyViewPager
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"></com.example.sy.eventdemo.MyViewPager>

</LinearLayout>
複製代碼

開始沒有處理滑動衝突的運行效果是這樣的:

看到如今只能上下滑動響應ListView的滑動事件,接着咱們外部攔截髮解決滑動衝突,核心代碼以下:

case MotionEvent.ACTION_MOVE:
                int gapX = x - lastX;
                int gapY = y - lastY;
                //當水平滑動距離大於垂直滑動距離,讓父view攔截事件
                if (Math.abs(gapX) > Math.abs(gapY)) {
                    intercept = true;
                } else {
                    //不然不攔截事件
                    intercept = false;
                }
                break;
複製代碼

onInterceptTouchEvent中當水平滑動距離大於垂直滑動距離,讓父view攔截事件,反之父View不攔截事件,讓子View處理。
運行結果:

這下衝突就解決了。這兩個例子分別對應了上面的場景一和場景二,關於場景三的解決方法其實也是同樣,都是根據具體需求判斷事件須要由誰來響應消費,而後重寫對應方法將事件攔截或者取消攔截便可,這裏就再也不具體舉例了。

六、總結

  • Android事件分發順序:Activity-->ViewGroup-->View
  • Android事件響應順序:View-->ViewGroup-->Activity
  • 滑動衝突解決,關鍵在於找到攔截規則,根據操做習慣或者業務邏輯肯定攔截規則,根據規則從新對應攔截方法便可。
相關文章
相關標籤/搜索