View滑動衝突

View和ViewGroup的事件分發機制

概述

  1. 所謂點擊事件的事件分發,其實就是對MotionEvent事件的分發過程。即當一個MotionEvent產生了以後,系統需要把這個事件傳遞給一個具體的View,而這個傳遞的過程就是分發過程。點擊事件的分發過程由三個很重要的方法來共同完成:dispatchTouchEvent、onlnterceptTouchEvent和onTouchEvent。
  2. public boolean dispatchTouchEvent(MotionEvent ev)
    用來進行事件的分發。如果事件能夠傳遞給當前View,那麼此方法一定會被調用,返回結果受當前View的onTouchEvent和下級View的dispatchTouchEvent方法的影響,表示是否消耗當前事件。
    public boolean onInterceptTouchEvent(MotionEvent event)
    在上述方法內部調用,用來判斷是否攔截某個事件,如果當前View攔截了某個事件,那麼在同一個事件序列當中,此方法不會被再次調用,返回結果表示是否攔截當前事件。
    public boolean onTouchEvent(MotionEvent event)
    在dispatchTouchEvent方法中調用,用來處理點擊事件,返回結果表示是否消耗當前事件,如果不消耗,則在同一個事件序列中,當前View無法再次接收到事件。
  3. 當一個點擊事件產生後,它的傳遞過程遵循如下順序:Activity->Window->View,即事件總是先傳遞給Activity,Activity再傳遞給Window,最後Window再傳遞給頂級View。頂級View接收到事件後,就會按照事件分發機制去分發事件。考慮一種情況,如果一個View的onTouchEvent返回false,那麼它的父容器的onTouchEvent將會被調用,依此類推。如果所有的元素都不處理這個事件,那麼這個事件將會最終傳遞給Activity處理,即Activity的onTouchEvent方法會被調用。

Activity對點擊事件的分發過程

  1. 當一個點擊操作發生時,事件最先傳遞給當前Activity,由Activity的dispatchTouchEvent來進行事件派發,Activity先交由所附屬的Window進行分發,如果返回true,整個事件循環就結束了,返回false意味着事件沒人處理,所有View的onTouchEvent都返回了false,那麼Activity的onTouchEvent就會被調用。
  2. Window是個抽象類,而Window的superDispatchTouchEvent方法也是個抽象方法,Window類可以控制頂級View的外觀和行爲策略,它的唯一實現位於android.policy.PhoneWindow中,PhoneWindow將事件直接傳遞給了DecorView。
  3. 我們通過setContentView設置的View是DecorView的一個子View。目前事件傳遞到了DecorView這裏,由於DecorView繼承自FrameLayout且是父View,所以最終事件會傳遞給View。

頂級View對點擊事件的分發過程

  1. ViewGroup在如下兩種情況下會判斷是否要攔截當前事件:事件類型爲ACTION_DOWN或者mFirstTouchTarget!=null
    。當事件由ViewGroup的子元素成功處理時,mFirstTouchTarget會被賦值並指向子元素。反過來,一旦事件由當前ViewGroup攔截時,mFirstTouchTarget!=null就不成立。那麼當ACTION_MOVE和ACTION_UP事件到來時,將導致ViewGroup的onInterceptTouchEvent不會再被調用,並且同一序列中的其他事件都會默認交給它處理。
  2. 這裏有一種特殊情況,那就是FLAG_DISALLOW_INTERCEPT標記位,這個標記位是通過requestDisallowInterceptTouchEvent方法來設置的,一般用於子View中。FLAG_DISALLOW_INTERCEPT一旦設置後,ViewGroup將無法攔截除了ACTION_DOWN以外的其他點擊事件。ViewGroup在分發事件時,如果是ACTION_DOWN就會重置FLAG_DISALLOW_INTERCEPT這個標記位,將導致子View中設置的這個標記位無效。因此,當面對ACT1ON_DOWN事件時,ViewGroup總是會調用自己的onInterceptTouchEvent方法來詢問自己是否要攔截事件。
  3. 當ViewGroup不攔截事件的時候,事件會向下分發交由它的子View進行處理。首先遍歷ViewGroup的所有子元素,然後判斷子元素是否能夠接收到點擊事件。是否能夠接收點擊事件主要由兩點來衡量:子元素是否在播動畫和點擊事件的座標是否落在子元素的區域內。如果某個子元素滿足這兩個條件,那麼事件就會傳遞給它來處理。


    dispatchTransformedTouchEvent實際上調用的就是子元素的dispatchTouchEvent方法,在上面的代碼中child傳遞的不是null,它會直接調用子元素的dispatchTouchEvent方法,這樣事件就交由子元素處理了,從而完成了一輪事件分發。如果子元素的dispatchTouchEvent返回true,那麼mFirstTouchTarget就會被賦值同時跳出for循環,如果子元素的dispatchTouchEvent返回間false,ViewGroup就會把事件分發給下一個子元素(如果還有下一個子元素的話)。
    [image:07D301F3-9A0D-485F-85A4-62C10CA0D2F1-547-000000AD1C3173DD/E81A3A89-2324-4502-AFCF-7DC645E0F6B1.png]
    其實mFirstTouchTarget真正的賦值過程是在addTouchTarget內部完成的,mFirstTouchTarget其實是一種單鏈表結構。mFirstTouchTarget是否被賦值,將直接影響到ViewGroup對事件的攔截策略,如果遍歷所有的子元素後事件都沒有被合適地處理,ViewGroup會自己處理點擊事件。


    這裏第三個參數child爲null,它會調用super.dispatchTouchEvent(event)。很顯然,這裏就轉到了View的dispatchTouchEvent方法,即點擊事件開始交由View來處理。

View對點擊事件的處理過程

  1. 因爲View是一個單獨的元素,它沒有子元素因此無法向下傳遞事件,所以它只能自己處理事件。它首先會判斷有沒有設置OnTouchListener,如果OnTouchListener中的onTouch方法返回true,那麼onTouchEvent就不會被調用,可見OnTouchListener的優先級高於onTouchEvent,這樣做的好處是方便在外界處理點擊事件。

    在onTouchEvent中,不可用狀態下的View照樣會消耗點擊事件,儘管它看起來不可用。

    接着,如果View設置有代理,那麼還會執行TouchDelegate的onTouchEvent方法,這個onTouchEvent的工作機制看起來和OnTouchListener類似。

    只要View的CLICKABLE和LONG_CLICKABLE有一個爲true,那麼它就會消耗這個事件,即onTouchEvent方法返回true,不管它是不是DISABLE狀態。當ACTION_UP事件發生時,會觸發performClick方法,如果View設置了OnClickListener,那麼performClick方法內部會調用它的onClick方法。

    、


    View的LONG_CLICKABLE屬性默認爲false,而CLICKABLE屬性是否爲false和具體的View有關,確切來說是可點擊的View其CLICKABLE爲true,不可點擊的View其CLICKABLE爲false。比如Button是可點擊的,TextView是不可點擊的。通過setClickable和setLongClickable可以分別改變View的CLICKABLE和LONG_CLICKABLE屬性。另外,setOnClickListener會自動將View的CLICKABLE設爲true,setOnLongClickListener則會自動將View的LONG_CLICKABLE設爲true。

View的滑動衝突

滑動衝突的場景

在界面中只要內外兩層同時可以滑動,這個時候就會產生滑動衝突。常見的滑動衝突場景可以簡單分爲如下三種:外部滑動方向和內部滑動方向不一致;外部滑動方向和內部滑動方向一致;上面兩種情況的嵌套。

  1. 場景1主要是將ViewPager和Fragment配合使用所組成的頁面滑動效果,在這種效果中,可以通過左右滑動來切換頁面,而每個頁面內部往往又是一個ListView。本來這種情況下是有滑動衝突的,但是ViewPager內部處理了這種滑動衝突,因此採用ViewPager時我們無須關注這個問題,如果我們採用的不是ViewPager而是ScrollView等,那就必須手動處理滑動衝突了。除了這種典型情況外,還存在其他情況,比如外部上下滑動、內部左右滑動等。
  2. 場景2稍微複雜一些,這種場景主要是指內外兩層同時能上下滑動或者內外兩層同時能左右滑動。
  3. 場景3是場景1和場景2兩種情況的嵌套,雖然說場景3的滑動衝突看起來更復雜,但是它是幾個單一的滑動伸突的疊加,因此只需要分別處理內層和中層、中層和外層之間的滑動衝突即可,而具體的處理方法其實是和場景1、場景2相同的。

滑動衝突的處理規則

  1. 場景1的處理規則是:當用戶左右滑動時,需要讓外部的View攔截點擊事件,當用戶上下滑動時,需要讓內部View攔截點擊事件。可以依據滑動路徑和水平方向所形成的夾角,也可以依據水平方向和豎直方向上的距離差來判斷是水平滑動還是豎直滑動。
  2. 對於場景2,一般都能在業務上找到突破點,比如業務上有規定:當處於某種狀態時需要外部View響應用戶的滑動,而處於另外—種狀態時則需要內部View來響應View的滑動,根據這種業務上的需求我們也能得出相應的處理規則。
  3. 對於場景3,同樣還是隻能從業務上找到突破點。

滑動衝突的解決方法

拋開滑動規則不說,我們需要找到一種不依賴具體的滑動規則的通用的解決方法。

  1. 外部攔截法:所謂外部攔截法是指點擊事情都先經過父容器的攔截處理,如果父容器需要此事件就攔截,如果不需要此事件就不攔截,這樣就可以解決滑動衝突的問題,這種方法比較符合點擊事件的分發機制。外部攔截法需要重寫父容器的onInterceptTouchEvent方法,在內部做相應的攔截即可,這種方法的僞代碼如下所示。


    上述代碼是外部攔截法的典型邏輯,針對不同的滑動衝突,只需要修改父容器需要當前點擊事件這個條件即可,其他均不需做修改並且也不能修改。在onlnterceptTouchEvent方法中,首先是ACTION_DOWN這個事件,父容器必須返回false,即不攔截ACTION_DOWN事件,這是因爲一旦父容器攔截了ACTION_DOWN,那麼後續的ACTION_MOVE和ACTION_UP事件都會直接交由父容器處理,這個時候事件沒法再傳遞給子元素了;其次是ACTION_MOVE事件,這個事件可以根據需要來決定是否攔截,如果父容器需要攔截就返回true,否則返回false;最後是ACTION_UP事件,這裏必須要返回false。
    假設事件交由子元素處理,如果父容器在ACTION_UP時返回了true,就會導致子元素無法接收到ACTION_UP事件,這個時候子元素中的onClick事件就無法觸發,但是父容器比較特殊,一旦它開始攔截任何一個事件,那麼後續的事件都會交給它來處理,而ACTION_UP作爲最後一個事件也必定可以傳遞給父容器,即便父容器的onlnterceptTouchEvent方法在ACTION_UP時返回了false。
  2. 內部攔截法:內部攔截法是指父容器不攔截任何事件,所有的事件都傳遞給子元素,如果子元素需要此事件就直接消耗掉,否則就交由父容器進行處理,這種方法和Android中的事件分發機制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,使用起來較外部攔截法稍顯複雜。它的僞代碼如下,我們需要重寫子元素的dispatchTouchEvent方法。


    當面對不同的滑動策略時只需要修改裏面的條件即可,其他不需要做改動而且也不能有改動。除了子元素需要做處理以外,父元素也要默認攔截除了ACTION_DOWN以外的其他事件,這樣當子元素調用parent.requestDisallowInterceptTouchEvent(false)方法時,父元素才能繼續攔截所需的事件。

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