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

通過簡單的demo來解析事件分發機制,主要通過簡單的三層Activity–>ViewGrop–>View來分析事件分發機制。先通過一張簡單的事件分發流程圖來看看事件分發的基本流程。
事件分發的簡單流程圖
Activity與View是沒有onInterceptTouchEvent這個方法的,也就是說這兩者沒法使用該方法攔截事件,我們先按照上面流程圖來建一個demo,看看事件分發是否與我們流程圖所畫的一樣。如下圖:
在這裏插入圖片描述

在沒有攔截任何事件的情況下可以看到打印的日誌,從Activity開始一層一層的把事件傳遞給最底層的View,最底層的View的onTouchEvent()沒有消耗事件的情況下,又向上依次按照層級去觸發onTouchEvent()(在沒有消費事件的情況下,也就是返回true),最終還是傳遞給了最上的Activity的onTouchEvent()來處理事件。然後我們來依次驗證其它流程
驗證dispatchTouchEvent
  1. ViewGroup 的dispatchTouchEvent() 返回true,其它方法返回值不變保持super看看事件流程
    在這裏插入圖片描述
    可以看到dispatchTouchEvent()返回true,事件就直接結束了不會往下傳遞。Activity或者View的也是一樣的,可以自行驗證。
  2. ViewGroup 的dispatchTouchEvent() 返回false或者View的dispatchTouchEvent()返回false的日誌圖分別如下在這裏插入圖片描述在這裏插入圖片描述可以看到兩者的日誌都如流程圖所示,如果返回的是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,其它方法保持初始狀態值
在這裏插入圖片描述
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,自身消耗掉該事件,其它方法不攔截也不消耗事件在這裏插入圖片描述
2.View的onTouchEvent()返回true
在這裏插入圖片描述
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()");
            }
        });

在這裏插入圖片描述
在默認都不攔截的情況下,onTouch的優先級高於onTouchEvent,onTouchEvent高於onClick.
我們將onTouch返回值改爲true看看
在這裏插入圖片描述
可以看到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
在這裏插入圖片描述
設置true
在這裏插入圖片描述
只有當View的onTouchEvent()返回super.onTouchEvent(event)時,setOnClickListener()纔會生效,默認爲消費了該事件,不會往上在傳遞給ViewGroup的onTouchEvent()處理。
在這裏插入圖片描述

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不會響應事件輸出日誌
在這裏插入圖片描述
但是我們要在滑動的時候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;
    }

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

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