天天看點

Android事件分發機制,淺談解析

通過簡單的demo來解析事件分發機制,主要通過簡單的三層Activity–>ViewGrop–>View來分析事件分發機制。先通過一張簡單的事件分發流程圖來看看事件分發的基本流程。

Android事件分發機制,淺談解析

Activity與View是沒有onInterceptTouchEvent這個方法的,也就是說這兩者沒法使用該方法攔截事件,我們先按照上面流程圖來建一個demo,看看事件分發是否與我們流程圖所畫的一樣。如下圖:

Android事件分發機制,淺談解析
在沒有攔截任何事件的情況下可以看到列印的日志,從Activity開始一層一層的把事件傳遞給最底層的View,最底層的View的onTouchEvent()沒有消耗事件的情況下,又向上依次按照層級去觸發onTouchEvent()(在沒有消費事件的情況下,也就是傳回true),最終還是傳遞給了最上的Activity的onTouchEvent()來處理事件。然後我們來依次驗證其它流程
驗證dispatchTouchEvent
  1. ViewGroup 的dispatchTouchEvent() 傳回true,其它方法傳回值不變保持super看看事件流程
    Android事件分發機制,淺談解析
    可以看到dispatchTouchEvent()傳回true,事件就直接結束了不會往下傳遞。Activity或者View的也是一樣的,可以自行驗證。
  2. ViewGroup 的dispatchTouchEvent() 傳回false或者View的dispatchTouchEvent()傳回false的日志圖分别如下
    Android事件分發機制,淺談解析
    Android事件分發機制,淺談解析
    可以看到兩者的日志都如流程圖所示,如果傳回的是false則會像上一級的onTouchEvent傳遞。(ps:細心的可能會看到上面的圖的日志列印,在日志最後總會多出幾個不像流程圖裡所畫的日志,比如MainActivity-----------dispatchTouchEvent(),MainActivity-----------onTouchEvent() 可能會問不是到onTouchEvent()就為止了?這裡就需要從源碼解釋了,由于篇幅問題這裡暫時不作詳細說明,可以自己去檢視源碼,事件分發,當你阻止他繼續分發後,自身這一層沒有消費掉這個事件,還往上層傳遞的話,下一次判斷就直接忽略了,是以直接就是最上層的dispatchTouchEvent()後,就直接onTouchEvent,因為ViewGroup或View的dispatchTouchEvent傳回false,沒法傳遞事件。 除非,你擡起手指在點選一次,它才會重新來開始判斷,有興趣的可以從源碼分析,這裡我截圖都是截所有列印的日志)

dispatchTouchEvent()的總結

  1. 當dispatchTouchEvent()傳回false,不像下分發事件時,無論是ViewGroup還是View都會向它的上一層的onTouchEvent()傳遞(會逆向向上傳遞),不會執行自身的onTouchEvent()。Activity的則會直接事件結束。
  2. 當都是預設的super. 則事件将會繼續向下分發,直到事件被消費為止。
  3. 當傳回true時,表示事件直接被消費,這個事件也就停止分發且不會逆向向上傳遞,直接結束了。
驗證onInterceptTouchEvent()

1.ViewGroup的onInterceptTouchEvent()傳回true,其它方法保持初始狀态值

Android事件分發機制,淺談解析

2.ViewGroup的onInterceptTouchEvent()傳回false,或者super.onInterceptTouchEvent(ev),則事件會依次向下傳遞直到被消費為止,就跟初始化的日志圖是一樣的,這裡就不上圖了。

onInterceptTouchEvent()的總結

onInterceptTouchEvent()是ViewGroup特有的方法,View和Activity中沒有這個方法,當傳回false/super時事件将會正常向下分發,分發至下級的dispatchTouchEvent方法;傳回true時,則表示ViewGroup容器攔截後續事件,會執行該ViewGroup的onTouchEvent()方法,如果自身onTouchEvent()沒有消費掉該事件,則會通過onTouchEvent()向上傳遞,直到事件被消費

驗證onTouchEvent()

1.ViewGroup的onTouchEvent()傳回true,自身消耗掉該事件,其它方法不攔截也不消耗事件

Android事件分發機制,淺談解析

2.View的onTouchEvent()傳回true

Android事件分發機制,淺談解析

3.ViewGroup或者View的onTouchEvent()傳回false,其它保持原樣,則日志輸出圖如正常流程

(ps:這紅色框框框起來的部分呢,上面幾個圖也是,忘記框起來了,就如前面的ps所說。最終消費掉該事件的在哪一層,下次判斷就會省去那些沒有意義的判斷,比如第一個框框圖,雖然第一次判斷走到了View的onTouchEvent,但是它沒有消耗掉該事件,而是向上傳遞給了ViewGroup的onTouchEvent()消費掉了,源碼裡面的的判斷下一次就會直接省去View裡面的判斷,直接就是Activity–>Group。也可以這樣說就是一個事件一旦交給一個View處理,那麼它就必須消耗處理掉,否則同一事件序列中剩下的事件就不再交給它來處理了,短時間内。這裡需要自己看源碼分析,是以我這裡的截圖,沒必要糾結紅色框裡面的列印周期)

onTouchEvent()的總結

傳回true則立即消費掉事件,事件将不會向上傳遞,事件到此終止。傳回false/super則不消費掉此次事件,事件将會層層向上傳遞,直到被消費。

總結一下事件分發中這幾個方法的傳回不同值時的表現
  1. dispatchTouchEvent和onTouchEvent這兩個方法呢無論是Activity,ViewGroup還是View,隻要傳回了true就表示,該事件到此就終止了,不會往下或者上傳遞事件了,什麼都結束了。
  2. dispatchTouchEvent和onTouchEvent這兩個方法傳回false/super時呢,對于ViewGroup或者View來說,事件都回傳給父控件的onTouchEvent處理。如果是Activity呢就直接結束了,因為它是最上層了到此就終止了。
  3. onInterceptTouchEvent方法預設是不會去攔截事件的,因為子View也需要這個事件,是以onInterceptTouchEvent()攔截器傳回 super.onInterceptTouchEvent() 和 false是一樣的,是不會攔截的,事件會繼續往子View的dispatchTouchEvent()傳遞;如果需要攔截呢就要傳回true。

    (ps:最好自己根據上面的流程圖寫代碼跟蹤一下日志,就清晰了)

關于onTouch(),onClick()的優先級和影響

給view設定setOnTouchListener和setOnClickListener事件,其它方法預設super,初始化看一下日志輸出

view.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.i("TAG","EventView-----------OnTouchListener()");
                return false;
            }
        });

        view.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.i("TAG","EventView-----------OnClickListener()");
            }
        });
           
Android事件分發機制,淺談解析

在預設都不攔截的情況下,onTouch的優先級高于onTouchEvent,onTouchEvent高于onClick.

我們将onTouch傳回值改為true看看

Android事件分發機制,淺談解析

可以看到onTouch傳回true以後View的包含onTouchEvent()事件的後面方法都不會執行了。被onTouch()消耗完了。onClick()是優先級最低的。在源碼裡面我們可以看到,這裡貼上的是View裡面的部分源碼,ViewGroup的也差不多。在dispatchTouchEvent()的方法中

public boolean dispatchTouchEvent(MotionEvent event) {
        ......

        boolean result = false;//是否消耗事件
        if (onFilterTouchEventForSecurity(event)) {
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            //若設定了OnTouchListener,則先調用onTouch()。
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            //若onTouch()沒有消耗事件則調用onTouchEvent()
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

        ......
        return result;
    }
           

onTouchEvent()中則包含onClick的代碼塊事件中的performClickInternal()就能找到點選事件

public boolean onTouchEvent(MotionEvent event) {
        ......
       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)) {
                performClickInternal();
            }
        }
    }
 }
           

可以看到mOnTouchListener.onTouch的優先級是高于onTouchEvent的。

onClick(),setOnClickListener()時,如果該View的onTouchEvent()不是super.onTouchEvent(event)時,無論設定是return false還是true該事件不會響應。如下圖

@Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i("TAG","EventView-----------onTouchEvent()");
//        return super.onTouchEvent(event);
        return false;
    }
           

設定false

Android事件分發機制,淺談解析

設定true

Android事件分發機制,淺談解析

隻有當View的onTouchEvent()傳回super.onTouchEvent(event)時,setOnClickListener()才會生效,預設為消費了該事件,不會往上在傳遞給ViewGroup的onTouchEvent()處理。

Android事件分發機制,淺談解析
ViewGroup中的requestDisallowInterceptTouchEvent() 設定是否允許攔截

在ViewGroup中如果在onInterceptTouchEvent()中攔截了事件,但是子View在某些情況下又需要該事件怎麼辦?在ViewGroup的dispatchTouchEvent()的源碼中我們可以發現disallowIntercept 設定是否攔截,跟随這個我們可以看到requestDisallowInterceptTouchEvent()設定為true的時候,可以不攔截onInterceptTouchEvent()

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
            ......
             // 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;
            }
          ......
    }
    
 @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);
        }
    }
           

我們修改一下EventView和EventGroup中的代碼,使其在滑動的時候攔截事件Group自己消耗

EventView

@Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
//                getParent().requestDisallowInterceptTouchEvent(true);
                Log.i("TAG", "You down EventView");
                break;
            case MotionEvent.ACTION_UP:
                Log.i("TAG", "You up EventView");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.i("TAG", "You move EventView");
        }
        return true;
    }
           

EventGroup

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                return false;
            case MotionEvent.ACTION_MOVE:   //表示父類需要
                return true;
            case MotionEvent.ACTION_UP:
                return true;
            default:
                break;
        }
        return false;    //如果設定攔截,除了down,其他都是父類處理
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return true;
    }
           

由于我們EventGroup在滑動的時候,return true自己消耗了事件,則EventView不會響應事件輸出日志

Android事件分發機制,淺談解析

但是我們要在滑動的時候EventView也要響應自己的事件,把上面我們注釋的代碼塊解除看看日志

@Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);
                Log.i("TAG", "You down EventView");
                break;
            case MotionEvent.ACTION_UP:
                Log.i("TAG", "You up EventView");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.i("TAG", "You move EventView");
        }
        return true;
    }
           
Android事件分發機制,淺談解析

很明顯getParent().requestDisallowInterceptTouchEvent(true); 這樣即使ViewGroup在MOVE的時候return true,子View依然可以捕獲到MOVE以及UP事件。也就是說子元素能夠通過調用requestDisallowIntercept(boolean b)來控制父容器能否調用onInterceptTouchEvent(),是否進行事件攔截。

繼續閱讀