Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 深入源碼理解Android Touch事件分發機制(下篇)

深入源碼理解Android Touch事件分發機制(下篇)

編輯:關於Android編程

上文我們徹底弄清楚了onTouch、onTouchEvent、onClick這三者的區別和聯系,也弄清楚Touch事件的傳遞原則以及事件在Activity、DecorView中的分發和傳遞。也給大家初步介紹了跟Touch事件分發息息相關的三個最重要的方法dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent,並給大家留下了一個疑惑:Touch事件在ViewGroup和View中是怎麼分發和傳遞的? 那麼,本篇我們將為大家解決這個疑惑,重點介紹Touch事件在ViewGroup和View中的分發機制。

首先要帶大家探究的就是ViewGroup對Touch事件的分發過程,其主要實現是在ViewGroup的dispatchTouchEvent方法中實現的,該方法可以說是相當長、相當復雜。我們一段段來分析,我們先來看一下第一段源碼:

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

很明顯這一段是對對當前ViewGroup是否該攔截Touch事件進行判斷。當當前的是DOWN事件或者當mFirstTouchTarget != null時會判斷是否攔截當前事件。那麼,這裡讀者可能有一個疑惑:mFirstTouchTarget是個什麼東東??? 看官別急,從後面的邏輯咱們可以看到,當ViewGroup的子View處理了Touch事件後,mFirstTouchTarget就會被賦值並指向該子View,換言之就是當ViewGroup不攔截事件並將其交給子View來處理是mFirstTouchTarget != null,一旦ViewGroup攔截事件那麼mFirstTouchTarget != null就不成立了,那麼當MOVE和UP事件到來的時候就不會走到ViewGroup的onInterceptTouchEvent方法中去了,整個一系列的事件將全部由該ViewGroup來處理。

當然也有一種特殊情況,那就是FLAG_DISALLOW_INTERCEPT這個標記位,它是由requestDisallowInterceptTouchEvent方法來設置的,一般是在子View中調用。一旦FLAG_DISALLOW_INTERCEPT設置後,ViewGroup將無法攔截除了DOWN事件外的其它事件。為什麼會是除了DOWN事件外的其它事件呢?因為如果是DOWN事件,那麼就會重置FLAG_DISALLOW_INTERCEPT標記位,從而使子View設置的該標記位失效。所以如果是DOWN事件的話,總會調用onInterceptTouchEvent方法來詢問是否攔截,這點從上述源碼也能看出。通過下面的這段源碼,我們能對ViewGroup處理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();
            }
如果是DOWN事件,會停止和清除所有的TouchTarget也就是說此時mFirstTouchTarget肯定為空,並調用resetTouchState方法對FLAG_DISALLOW_INTERCEPT進行重置。至此,我們對ViewGroup的dispatchTouchEvent方法基本上已經了解清楚了,當然這還是純粹從源碼的角度來分析的,我們還得寫個小demo來進行輔助驗證:同樣的,我們還是寫一個MainActivity,裡面有一個繼承LinearLayout的自定義ViewGroup——TestLayout,該ViewGroup中放一個自定義的子View——TestView。
/**
 * Created by leevi on 16/9/1.
 */
public class TestLayout extends LinearLayout{
    private Context mContext;
    public TestLayout(Context context) {
        this(context,null);
        mContext = context;
    }

    public TestLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.d("TestLayout", "dispatchTouchEvent!!!!!!");
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.d("TestLayout", "onInterceptTouchEvent!!!!!!");
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d("TestLayout", "onTouchEvent!!!!!!");
        return super.onTouchEvent(event);
    }
}
先只關注ViewGroup的情況,這種情況下,我們點擊TestView,能得到如下log:

\

很明顯可以看出能走到onInterceptTouchEvent方法,那我們再將dispatchTouchEvent方法的返回值改成true和false看看是什麼結果:

返回值改成true的情況:

\

很明顯可以看出,走了兩次dispatchTouchEvent方法,說明DOWN 和 UP事件都只走到ViewGroup的dispatchTouchEvent方法就不能往下走了,會父上傳遞給父控件的onTouchEvent方法(後面會聊到。)

那再來看看返回值是false的情況吧:

\

納尼!!!!!!!居然只打印了一次dispatchTouchEvent,這大大出乎我們的意料吧,意思是當我們DOWN事件到來後碰到這種情況整個Touch事件就結束了,後面的MOVE和UP事件都不會處理了。

所以我們可以得出結論,只有當ViewGroup的dispatchTouchEvent返回super.dispatchTouchEvent(event)的時候才能將事件傳遞下去。

那接下來我們就再以這個例子來探討一下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的onInterceptTouchEvent的源碼,我們可以理解成默認是返回false的,表示不攔截Touch事件。那麼默認情況下,Touch事件是怎麼傳遞的呢?在這裡我要強調一句:View是沒有onInterceptTouchEvent方法的,只有dispatchTouchEvent、onTouchEvent。

 

當ViewGroup的onInterceptTouchEvent返回super或者false的時候,我們會得到以下log:

\

從以上log我們可以看出,事件由ViewGroup的dispatchTouchEvent方法傳遞給onInterceptTouchEvent方法,再傳遞給子View的dispatchTouchEvent方法,最後傳遞給子View的onTouchEvent方法來處理。

那如果我們將TestLayout的onInterceptTouchEvent方法的返回值改成true來攔截該事件呢?我們又會得到什麼情況呢?

\

我們可以發現,該事件不會傳遞到子View,並且會由onInterceptTouchEvent傳遞給onTouchEvent,由於沒有任何部分能處理DOWN事件,所以MOVE和UP事件也不復存在了。這就是我們看到上述log並沒有打印兩次的原因。

至此,Touch事件在ViewGroup中的傳遞我們基本上已經完全弄清楚了。那接下來我們看看當事件傳遞給子View後是怎麼分發和傳遞的呢?首先我們也是來研究一下View的dispatchTouchEvent方法。同樣還是從源碼入手:

public boolean dispatchTouchEvent(MotionEvent event) {
        // If the event should be handled by accessibility focus first.
        if (event.isTargetAccessibilityFocus()) {
            // We don't have focus or no virtual descendant has it, do not handle the event.
            if (!isAccessibilityFocusedViewOrHost()) {
                return false;
            }
            // We have focus and got the event, then use normal event dispatch.
            event.setTargetAccessibilityFocus(false);
        }

        boolean result = false;

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }

        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }

        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            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;
            }
        }

        if (!result && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }

        // Clean up after nested scrolls if this is the end of a gesture;
        // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
        // of the gesture.
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }

        return result;
    }

相比較ViewGroup的dispatchTouchEvent方法,View的dispatchTouchEvent方法要簡便得多,主要是判斷了有沒有注冊onTouchListener,如果onTouchListener中的onTouch方法返回true則不會走onTouchEvent方法。這也解釋了我們在前面一篇所得到的onTouch方法的優先級高於onTouchEvent。從上面的log我們也能看出,當View 的dispatchTouchEvent返回super.dispatchTouchEvent時,事件是會傳遞給View的onTouchEvent的。那我們將該返回值改成true和false再來看看log的情況:

 

先將返回值改為true。

\

改成false的情況如下:

\

我們可以看出,跟ViewGroup的情況非常相似,無論是返回true還是false,事件都沒辦法傳遞到View的TouchEvent中來。而這裡我們會發現一個特別有意思的地方,當返回值為false的時候,事件會向上傳遞給父容器的onTouchEvent方法。這就是Touch事件的傳遞規則,先由外至內,如果內部不消化的話再由內傳遞至外。

我們還是可以得出一個結論,只有當dispatchTouchEvent返回值是super.dispatchTouchEvent(ev)時,Touch事件才能向下傳遞。

那最後我們再來看看View的onTouchEvent方法,默認返回值是false,代表不處理,不處理的話會傳遞給父控件的onTouchEvent方法。那我們如果將onTouchEvent的返回值改成true呢?

\

我們可以看到,onTouchEvent方法返回true就將該事件消費了,事件不會再由內向外傳遞。那我們就通過一張流程圖來總結一下Touch事件在ViewGroup和View中的分發和傳遞。

\

 

 

通過上圖,我們已經非常清楚地了解到了事件的分發機制。那最後我們就來了解一下,解決實際開發中沖突的兩類方法:

(1)、外部攔截法。所謂外部攔截法,就是Touch事件先傳遞給父容器,由父容器來決定攔截處理。如果父容器需要該事件,那就在onInterceptTouchEvent中返回true進行攔截,如果不需要該事件那就在onInterceptTouchEvent中返回false,將事件傳遞給子View。

(2)、內部攔截法。內部攔截法顧名思義就是父控件默認不攔截任何事件,所有事件全部傳遞給子View,如果子View需要事件則在onTouchEvent中返回true消費,否則返回false交給父容器處理。這裡需要配合requestDisallowInterceptTouchEvent方法使用,才能達到想要的效果。我們要重寫子View的dispatchTouchEvent方法,根據具體情況調用requestDisallowInterceptTouchEvent來對請求父控件對事件進行攔截和不攔截。這裡需要特別注意的是,父控件一定不能攔截DOWN事件,要不然所有事件都不會傳遞到子View了(前面已經驗證了),內部攔截法也起不了作用了。

至此,Touch事件分發機制基本上全部講完了,相信大家結合上下兩篇文章,對事件分發機制都有較為深入的了解了。後續還會帶來更多高質量的博客,如果大家有什麼需要深入了解的知識點,也可以再留言裡跟我說,我可以根據你們的留言寫相應的博客。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved