Android 開發藝術探究V第三章之view的事件分發機制

                   在介紹點擊事件的傳遞機制,首先我們要分析的對象就是MOtionEvent,即點擊事件,(當點擊屏幕時由硬件傳遞過來,關於MotionEvent在View的基礎知識中做了介紹),所謂的點擊事件的分發就是MotionEvent的分發過程。即當一個MoTionEvent產生以後,系統需要把這個事件具體傳遞給一個具體的View,而這個傳遞過程就是分發過程,點擊事件傳遞過程有三個很重要的方法,下面先來介紹這幾個方法。

                 public boolean dispatchTouchEvent(MOtionEvent ev)

用來進行事件分發,如果事件能夠傳遞給當前的view,那麼此方法一定會被調用,返回結果受當前View的OnTouchEvent和下級View的dispatchEvent方法的影響,表示十分消耗當前事件,

              public boolean onInterceptTouchEvent(MOtionEvent ev)

上述方法內部調用,用來判斷是否攔截某個事件,如果當前View攔截了某個事件,那麼在同一個事件序列中,此方法不會再調用,返回結果表示是否攔截當前事件,(攔截Action_Down事件,或者在Acton_Down事件當中子VIew  dispatchTouchEvent(MotionEvent ev)返回爲false)下面關於這個方法什麼情況下會調用將會做詳細的介紹。

        public boolean onTouchEvent(MOtionEvent ev)

在dispathcTouchEvent(MotionEvent ev)方法的調用,用來處理點擊事件,返回結果是否消耗當前事件,如果不消耗,在同一個事件序列中,當前View無法再次接收到事件。

                  

              上面三個方法區別:

                     public boolena dispathcTouchEvent(MotionEvent ev){

boolean cosume=false;

if(onInterceptTouchEvent(ev)){

consume=onTouchEvent(ev);

}else{

consume=child.dispatchTouchEvent(ev);

}

}

                   


   上面代碼已經將三者的關係表現的淋漓盡致,通過上面的代碼實現,我們也可以大致瞭解事件的傳遞規則:對於一個根ViewGroup來說,點擊事件產生後,首先會傳遞給它 如果ViewGroup的  onInterceptTouchEvent 方法返回true,就表示它要攔截當前事件,接着事件就會交給ViewGroup的onTouchEvent(ev)方法處理即它的onTouchEvent方法就會被調用,如果這個ViewGroup的onInterceptTouchEvent方法返回false就表示它不攔截當前事件,這時當前事件就會繼續傳遞給它的子元素,接着子元素的dispatchTouchEvent(MotionEvent ev);方法被調用,如此反覆知道事件被最終結束。

  

    當一個View需要處理事件時,如果它設置了OnTouchListener,那麼onTouchListener中的onTouch方法會被回調。這時事件如何處理還要看onTouch的返回值,如果返回false,則當前View的OnTouchEvent方法會被調用;如果返回true,那麼onTouchEvent方法將不會被調用,這個dispatchTouchEvent將會返回true,由此可見,給View設置的有OnTouchListener,其優先級比OnTouchEvent要求高,在onTouchEvent方法中,如果設置的有OnClick,那麼它的onClick方法會被調用。後面分析:如果onClick被調用,dispatchonTouchEvent(ev)將會返回true,表示消耗了當前的事件


    當一個點擊事件產生後,它的傳遞過程如下:Activity->Window->View,即事件總是先傳遞給Activity,Activity再傳遞給Window,最後Window在傳遞給頂級View,頂級View接收到事件後,就會按照事件分發機制去分發事件,一種特殊情況,當View不消耗除ACIONT_DOWN以外的其他事件,那麼這個點擊事件會消失,那麼最終的傳遞給Activity處理,即Activity的onTouchEven方法會被調用。


有以下幾點:

1.   同一個事件不能由兩個View同時處理,但是通過特殊手段可以做到,

2.   某個View一旦開始處理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回false也就是disptchOnTouchEvent返回false,那麼同一事件都不會再交給它處理,即父容器的onTouchEvent回被調用,

        3.在第二點的基礎上,比較重要:如果View不消耗除ACTION_DOWN以外的其他事件,那麼這個點擊事件會消失,此時父元素的onTouchEvent並不會被調用。並且當前View可以持續收到後續的事件,最後這些消失的點擊事件會傳遞給Activity, (是在父View的OnInteceptTouchEent不被從寫的情況下,因爲父View默認不攔截事件,當前還有一種情況就是設置標誌位,控制自View是否需要點擊事件。)

       4.View的enable屬性影響onTouchEvent的默認返回值。哪怕一個View是disable狀態的,只要它的clickable或者longClickable有一個爲true那麼它onTouchEvent返回true,TextView和Button的onTouchEvent的chickable不相同,onTouchEvent方法返回值也就不同,如果setOnclickable方法或者setLongClickable 方法將會讓clickable和longclickable自動變爲true

      5.事件傳遞過程是由外向內的,即 事件總是先傳遞給父元素,然後在由父元素分發給子View,通過requestDisallowInterceptTouchEvent方法可以在干預子元素的事件分發過程。但是除了Action_Down以外。

     


   事件分發機制的源碼解析


      1.Activity對點擊事件的分發過程

            

   點擊事件用MotionEvent來表示,當一個點擊操作發生時,事件最先傳遞給當前Activity,有Activity的dispatchTouchEvent來進行事件派發,具體工作由Activity內部的Window來完成,Window會將事件傳遞給decorview,decorview一般就是當前界面的底層容器,(即setContextView所設置的View的父容器),通過Activity.getWindow.getDecorView()可以獲得。我們先從Activity的dispatchTouchEvent開始分析。


public boolean dispatchTouchEvent(MotionEvent ev){

if(ev.getAction==ACTON_DOWN){

onUserInteraction();

}

if(getWindow().superDispatchTouchEvent(ev)){

return true;

}

return onTouchEvent(ev);

}


分析上面的源碼,首先事件開始交給Activity所附屬的Window進行分發,如果返回true,這個事件循環結束,返回false意味着事件沒人處理,所有View的onTouchEvent都返回false,那麼Activity的OnTouchEvent就會被調用。

       

        接下來看Windown是如何將事件傳遞給ViewGroup的,通過源碼我們知道,Window是一個抽閒類,而Window的superDisPatchTouchEvent方法也是個抽象方法,因此我們找到Window的實現才行。


        public absract boolean superDispatchTouchEvent(MotionEvent event);


那麼Window的實現類是什麼?其實是PhoneWindow,這一點從Window的源碼中也可以看出來,在Window的說明中,Window類可以控制頂級View的行爲和策略,它的唯一實現位於Android.policy.Phonewindow中,當你要實例化這個Window類的時候,你並不會知道它的實現細節,因爲這個類被重構,只有一個工廠方法可以使用。儘管這看起來有點模糊,不過我們可以看依稀Android.policy.PhoneWindow這個類,儘管實例化的時候此類會被重構,僅是重構而已,功能是類似的。


        由於Window的唯一實現是PhoneWindow,因此接下來看一下PhoneWindow是如何處理點擊事件的,


           public boolean superDispathcTouchEvent(MotionEvent event){

return decor.superDispatchTouchEvent(event);

}


       到這裏邏輯就很清晰了,PhoneWindow將事件傳遞給了DecorView,我們知道通過(ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0)這個方式就可以獲取Activity所設置的View,這個子View很顯然是我們setContextView所設置的View。另外頂級View也叫根View,頂級View一般來說都是ViewGroup。由此爲止:上面分析Activity都頂級View的分發機制就介紹到這裏了:


     下面將 會分析ViewGroup的分發過程,上面我們對View進行理論上的分析,但還是沒有對源碼進行分析。首先先看ViewGroup的DispatchTouChEvent方法中,這個方法比較長,這裏分段說明。很顯然,它描述的是當前View是否攔截這個事件的邏輯。

   下面這段代碼很重要

    //check for interception

    final boolean intercepter;

    if(actionMasked==MotionEvent.Action.Down || mFirstTouchTarger!=null){

final boolean disallowIntercept=mGrouFlags& FLAG_DISALLOW_INTERCEPT!=0){//這個標誌位是在自View調用 requestDisallowInterceptTouchEvent方法來設置的。

if(!disallowIntercept){

intercepted=onInterceptTouchEvent(ev);

ev.setAction(action);//restore Action in case it was Changed

}else{

intercepter=false;

}

}else{

intercepter=true;

}

}


(上面這段代碼的說明)

從上面的代碼我們可以看出,ViewGroup在如下兩種情況下會判斷是否要攔截當前事件:事件類型爲Action_DOWN或者mFirstTouchETager!=null .Action_DOWN事件好理解,那麼mFirstTouchTaget!=null是什麼意思?這個從後面的代碼邏輯可以看出來,當事件由ViewGroup的子元素成功處理是,mFirstTouchTarget會被賦值並指向子元素,換種方式來說,當ViewGroup不攔截事件,並且交個子元素處理時,mFirstTouchTarget!=null。反過來,一旦事件由當前ViewGroup攔截時,mFirstTouchTarget!=null就不成立,那麼當Action_move和Action_up事件到來時,由於(actionMasked==MOtionEvent.ACION_DOWN||mFirstTouchTarget!=null)這個條件爲false,將導致ViewGroup的onInterceptTouchEvent不會再被調用,並且同一個序列的其他事件都會默認交給它處理。


       FLAG_DISALLOW_INTERCEPT標誌位一旦設置後,ViewGroup將無法攔截出了Action_down以外的其他事件,如果是Action_down事件,救回重置標記位,將導致子View中設置的這個標記位無效。當面對ACTION_DOWN事件時,ViewGroup總是會調用自己的onInterceptTouchEvent方法來詢問自己是否要攔截事件,


我們可以得到結論:當ViewGroup決定攔截事件後,那麼後續的點擊事件將會默認交給它處理並且不再調用它的onInterceptTouchEent方法,那麼制度單分析對總結起來有兩點:

              1.onInterceptTouchEvent方法不是每次都會被調用,如果我們想要處理所有的點擊事件必須到 dispatchTouchEvent方法,只有這個方法保證每次都會調用,當然前提是事件能夠傳遞到dispathcTouchEvent,

     2.FALG_DISALLOW_INTERCEPT標記位的作用給我們提供了思路,當面對滑動衝突時,我們可以考慮這種方法。


接着在看當ViewGroup不攔截事件的時候,事件會向下分發交由它的子View進行chul

final View[]  childern=mChildren;

for(int i=childlern-1;i>=0;i--){

final int childIndex=customOrder?getChildDrawingOrder(childdrenCount,i);i;?getChildDrawingOrder(childrenCount,i):i;

......

if(dispathcTrasfromedTouchEvent(ev,false,cihld,idBitsToAssing)){

//child wants to receive touch within ists bounds

mlastTouchDownTime=ev.getDownTime();

if(preorderedList!=null){

....

new TouchTarget=addTouchTarger(child,idBitsToAssign);

}

}

}

重上面這段代碼分析首先遍歷所有ViewGroup的所有子元素,然後判斷子元素是否能夠接受到點擊事件,是否能夠接受點擊事件主要由兩點來衡量:

子元素是否有動畫,和座標是否落在子元素上。


dispathcTrasfromedTouchEvent實際調用了子元素的dispatchTouchEvent方法,在它的內部有如下一段內容,而在上面的代碼中,child傳遞的不是null它會直接調用子元素的dispatchTouchEvent方法,這樣事件就交給子元素處理,從而完成一輪事件分發。

                      if(child==null){

handler=super.dispatchTouchEvent(event);//拋出給父View 的父容器

}else{

  handler=child.dispatchTouchEvent(event);

}


  從上面看出,如果子元素爲null,或者dispathcTrasfromedTouchEvent返回爲false,就會調用父View的父View的dispatchTouchEvent(evnet)

如果子元素的dispatchTouchEvent返回true,就會交給子元素分發,那麼mFirstTouchTarget就會被賦值,如果子元素的FirstTouchTarget是在addTouchTarger中進行賦值的  ,如果遍歷所有的子元素都沒有被合適的處理,將會交給父View自己處理

                if(mFirstTouchTarget==null){

handled=dispatchTransformTouchEvent(ev,canceled,nullTouchTarget,ALL_POINTER_IDS);

}



從上面ViewGroup 理論和源碼分析總,我總結了上述分析:

1.ViewGroup 最先調用的是dispatchTouchEvent(event); 然後判斷是不是Action_down 或者mFirstTouchTarget是不是爲null,如果兩者都不成立,事件將不會調用onInterceptTouchEvent(ev)進行判斷是否攔截事件,事件將會直接交給ViewGroup本身自己處理。如果兩者有一個不成立,將會判斷FLAG_DISALLOW_INTERCEPT標誌位,如果標誌位true,將會調用onInterceptTouchEvent方法判斷是否將事件交給自View處理,如果 onInterceptTouchEvent返回爲true,將會交給父View自己處理,如果返回false,事件有2種方式傳遞:

1.事件將會傳遞給子View,如果找到了合適的子View,如果子View返回true,也就是事件被消耗掉,那麼也就完成了整個事件的傳遞,

2.如果找不到合適子View 就會交個父View的的父容器處理。由於父View的父容器也找不到合適的子View進行分發事件,由此往上,所以會繼續向上拋出,一直拋出給Window,然後Window找不到合適的子View繼續拋出,拋出給Activity。所以會調用Activity的onTouchEvent方法;

             也就是說,如果ViewGroup一旦不處理事件,將會沒有機會再得到事件。事件不是給子View處理,就是向上拋出 ,當然,本身也就是上角是有最上面的ViewGroup不出的。同理,事件會直接交給Activity處理。



     通過上面的總結分析:我們大致知道了ViewGroup的事件分析機制。所有我們經常通過邏輯來改變標誌位,和通過重寫onInterceptTouchEvent方法,來進行事件傳遞。


  上面分析了ViewGroup的事件分析下面分析View的事件傳遞機制

        View的事件傳遞機制,相對來說比較簡單,注意這個View不包括ViewGroup,ViewGroup重寫了view的dispatchOnInterceptTouchEvent的方法。

                   當View調用的dispatchOnInterceptTouchEvent方法,會判斷有沒有註冊OnTOuch,如果註冊了,將會調用,OnTouch(this,enent);如果 OnTouch(this,event)返回true,也就是事件被消耗了,OnTouchEvent將不會再被調用,如果返回false,或者不註冊onTouch(this,event)將會都會調用onTouchEvent方法。也就是說,onTouch不決定當前事件是否被當前View消耗,因爲當前View如果OnTouch返回true,那麼dispatchOnInterceptTouchEvent方法也會返回true。事件將被消耗,如果onTouch返回false,將會調用TouchEvent,如果TouchEvent返回false,dispatchOnInterceptTouchEvent也會返回false,TouchEvent返回true,那麼dispatchOnInterceptTouchEvent返回爲true,另外,在TouchEvent在會判斷有沒有註冊OnClick方法和onLongClick,如果註冊了將會調用,在註冊Onclick,或者onLongClick的時候,CLICKABLE和CLICKLONGABEL會自動變成true,所有會被調用。



這裏提兩個問題:(包含了事件分發機制的精髓)

1.如果在Viewgroup,子View在ACTION_DOWN 中返回了false還會傳遞給子View嗎?(如果不會,通過設置標誌位或者起作用嗎)

2.如果子View值消耗了ACTION_DOWN事件,其餘事件都不消耗,那麼事件是分發過程是怎樣的?(可以通過設置標誌位來決定事件是否由誰來執行)

 

答案:

                    1.不會,當子View在ACTON_DOWN總返回false,也就是dispathcTrasfromedTouchEvent在ACTION中返回false,將不會給mFIrstTouchTarget進行賦值,然後在ViewGroup中將不會調用onInterceptTouchEvent進行判斷,直接將事件交給ViewGROP自己處理。

設置標誌位也不會起作用,因爲,標誌位的判斷,是先進行ACTION_DOWN判斷和mFirstTouchTarget是否爲null之後。

   

              2.通過上面的事件分發機制我們可以知道,ACION_DOWN傳遞給子View,並且子View消耗了ACTION_DOWN事件,dispatchOnInterceptTouchEvent返回true,mFirstTouchTarget將會被賦值,由於Viewgroup進行的onInterceptTouchEvent方法默認是不攔截任何事件,所有事件將會給子View進行執行。但是子View並不消耗Action_move,和action_up,所有ViewGroup會調用super.dispatchOnInterceptTouchEvent方法,事件將會一層一層向上面拋出。一直會調用Activity的事件OnTouchEvent方法

                   可以通過設置標誌位和重寫onInterceptTouchEvent方法來決定是否交給當前的ViewGroup;




View的滑動衝突


通過上面的分析,我們將進入一個深入的話題,滑動衝突,相信開發Android 的人深有體會,本來在網上下載的demo運行的好好的,但是由於組合在一起將會出現各種問題,滑動衝突的產生:其實在界面上只要內外兩層同時可以滑動,這個時候就會產生滑動衝突,如何解決滑動衝突,這既是一件困難的事又是一件簡單的事,說簡單是因爲解決滑動衝突有固定的套路,本節是View體系的核心章節,


常見的滑動衝突場景:

1.外部滑動方向和內部滑動方向不一致;

2.外部滑動方向和內部滑動方向一致;

3.兩種情況的嵌套;

 先說場景1:主要是ViewPager和Fragment配合使用所組成的滑動效果,主流應用幾乎都會使用這個效果,而每個頁面會嵌套一個listView,本來這種情況是有滑動衝突的,但是ViewPager處理了這種滑動衝突,如果我們不是採用的ViewPager而是採用的ScrollView等,那就必須手動處理滑動衝突,否則造成的後果就是就是內外兩層只有一層能夠滑動。

場景2:當內外兩層都在同一個方向上滑動的時候,顯然存在邏輯爲,因爲當手指開始滑動時,系統無法知道用戶到底想讓哪一層滑動,所以當手指滑動的時候就會出現問題。要麼只有一層呢能夠滑動,要麼就時內外兩層都能夠的很卡頓。

       場景3: 是場景1和場景2的嵌套,因此場景3的滑動衝突看起來更加的複雜,比如在許多應用中會有這麼一個效果,內層有一個場景1中滑動效果,然而外出又有一個場景2中的滑動效果,具體說,就是外部有slideMenu效果,然後內部有一個ViewPager的每一個頁面有一個ListView。雖然說場景3看起來很複雜,但它是幾個單一的滑動衝突的疊加,因此只需要分別處理內層.外層和中層之間的滑動衝突即可。



   滑動衝突的解決規則

場景1.讓內部View攔截點擊事件,這個時候我們可以根據他們的特徵來解決滑動衝突,具體來說:根據滑動時水平滑動還是豎直滑動來判斷是否是誰攔截事件。(判斷方式:1.可以根據滑動路徑和水平方向形成的夾角,也可以依據水平方向豎直方向上的距離差來判斷,某些特殊情況還可以根據水平方向和豎直方向的速度來判斷 比如豎直方向滑動距離大於水平方向滑動的距離)

                 場景2:比較特殊,它無法根據滑動的角度,距離差以及速度來判斷,這個時候一般都能在業務上找到突破點。

場景3:同樣也只能在業務上找到突破點;



   滑動衝突的解決方式

1.外部攔截法

所謂的外部攔截髮:是指所有的點擊事件都先經過父容器攔截處理,如果父容器需要此事件,就攔截,如果不需要就不攔截。就這樣解決滑動衝突問題

外部攔截法需要重新onInterceptTouchEvent方法

public boolean onInterceptTouchEvent(MOtionEvent evet){

boolean intercepter=false;

int  x=(int)evet.getx();

int y=(int)   evet.getY();

switch(evetn.getAction){

case MotionEvetn.ACTION_DOWN:

                 
//必須返回false,不消耗事件,交給子View去消耗,否則mfirsTouchTarger將不會被賦值,後續事件將不會傳遞給View

intercepter=false;

break;

case  MotionEvent.getMOVE;

if(父容器需要當前事件){

intercepter=true;//交給父view自己攔截

}else{

intercepter=false;

}

break;

  case Moton.ACTION_UP;

intercepter=false;//必須返回false;如果是true,子View將無法接收到ACTION_UP

break;

}

mlastXintercept=x;

mlaseYintercept=y;

return intercepted;

}


            上述代碼是外部攔截髮的經典邏輯



2.內部攔截法:

 內部攔截法是指View不攔截任何事件,所有的事件都傳遞給子元素,如果子元素需要此事件就直接消耗掉。否則劇交給父容器進行處理。這個方法和Android中的事件分發機制不一致,需要配合requestDisallowInterceptEvent方法才能正常工作,我們需要重寫子元素的dispatchTouchEvent方法。


public boolean dispatchTouchEvent(MotionEvent event){

int x=(int)event.getX();

int y=(int)event.getY();

switch(event.getAction){

case MotionEvent.ACTON_DOWN:

parent.requestDIsallowInterceptTouchEvent(true);//設置標誌位,子view處理事件。

break;

case MoTionEvent.ACTON_MOVE;

int detaX=x-mlastX;

int deltaY=y-mLastY;

if(父容器需要此類點擊事件){

parent.requestDisallowInterceptTouchEventet(false);

}

break;

case MotionEvent.ACTION_UP;

break;

}


                       mLastX=x;

mLastY=y;

}



除了子元素需要做處理以外,父元素也要默認攔截除了ACTION_DOWN以外的其他事件,這樣當子元素調用,parent.requestDisallowInterceptTouchEvent(false);方法時,父元素才能繼續攔截所需事件。

public  boolean onInterceptTouchEvent(MotionEvent event){

int action=event.getAction();

if(action==MotionEvent.ACTION_DOWN){

                 return false;

}

        return true;

}


上述代碼是內部攔截髮的經典代碼,當面對不同的滑動策略時,只需要修改裏面的的各種條件即可,這種方法和Android中的事件分發機制不一樣,其實內部攔截法,就是不讓父元素有判斷onInterceptTouchEvent方法的機會,只要判斷,就會給父View自己處理。  (當然,前提是父View在onInterceptTouchEvent沒有攔截ACTION_DOWN,讓mFIrstTouchTarget不等於null,讓父View先判斷標誌位,如果標誌位true,就不判斷onInterceptTouchEvent方法,直接將事件給子View,如果標誌爲false,就會去判斷onInterceptTouchEvent方法,而onInterceptTouchEvent方法除了Action_Down總是返回true,就會給父容器處理攔截。



  最後

    注意:在寫事件是否攔截事件時;

如果是外部攔截法 如果滑動事件父View慣性滑動,在父View 的OnInterceptTouchEvent 中的ACTION_DOWN中加上如下代碼,讓父View繼續處理事件,不讓讓其傳遞給子View


switch(event.getAction()){

case MotionEvent.ACITION_DOWN:                 

intercepted=false;

if(!mScroller.isFinished()){

mScroller.abortAnimation();

intercepted=true;

}

........

}

  如果是內部攔截法:也想上一樣在父View的onInterceptTouchEvent中加入上述代碼:



                 public boolean dispatchTouchEvent(MOtionEvent ev)
發佈了45 篇原創文章 · 獲贊 6 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章