你真的搞懂了事件分發?

寫這篇文章實際上是有緣由的,說實話此次面試真的很失敗,看着身邊的人都拿到了高薪的工資,感受本身仍是有些慚愧。也更說明本身在不少方面的知識點仍是不夠紮實,因而再一次拿起了view的事件分發的源碼給看了一遍。java

面試官:說說view中的事件分發?

android中事件分發機制是android中常見的問題,通常你們都知道view的分發事件是從view的Viewgroup(Parent)#dispatchTouchEventViewgroup(Parent)#onInterceptTouchEvent再到View#dispatchTouchEvent,而後到view的onTouchEvent,最後又回到了Viewgroup(Parent)#onTouchEvent。若是你們記不住方法名,能夠直接說先是parent的分發到攔截再到view的分發,再到view的消費,最後到parent的消費android

view分發圖

viewgroup分發

這樣回答確定是很淺顯的,由於沒有說出是否攔截、是否分發、是否消費的各類條件,沒有涉及到各類action的分發狀況,上面說的默認分發只是針對action_down的,由於view/viewgroup各類super調用都是不進行分發、攔截、消費的,因此在沒找處處理touch事件的view時候,是一直往上層view傳遞的,一直傳到activity裏面,下面咱們再來整理一下:面試

若是viewgroup不進行分發,那麼action_downaction_moveaction_up只會執行到viewgroup的dispatchTouchEvent,不分發的條件是dispatchTouchEvent直接返回true或false,true和false的區別是true會執行action_downaction_moveaction_up,而若是直接返回false只會執行到action_down。而且後續的viewgroup的onInterceptTouchEvent後續方法都不會被執行到。bash

關於爲何view/Viewgroup的dispatchTouchEvent返回true的時候三個action都能執行到,而返回false的話,只能執行到action_down,這個須要到view/Viewgroup的父類中dispatchTouchEvent找答案,該方法中會在action_down的時候調用dispatchTransformedTouchEvent方法,而該方法是經過子view的dispatchTouchEvent方法的返回值來決定父類的dispatchTransformedTouchEvent方法的返回值,而dispatchTransformedTouchEvent的返回值會決定mFirstTouchTarget是否爲空,因此在action_down的過程當中實際中經過子view的dispatchTouchEvent方法返回值來肯定mFirstTouchTarget是否爲空。這裏貼出viewgroup中dispatchTransformedTouchEvent方法的刪減代碼:學習

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    ------------------
    //省略了cancel部分的代碼
    ------------------------
    //若是child爲空,直接調用本身的dispatchTouchEvent方法,此時本身就至關於一個view,touch事件走本身的
    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        final float offsetX = mScrollX - child.mLeft;
        final float offsetY = mScrollY - child.mTop;
        transformedEvent.offsetLocation(offsetX, offsetY);
        if (! child.hasIdentityMatrix()) {
            transformedEvent.transform(child.getInverseMatrix());
        }
        //返回值直接經過孩子來獲取返回值
        handled = child.dispatchTouchEvent(transformedEvent);
    }
    transformedEvent.recycle();
    return handled;
}
複製代碼

因此若是view/viewgroup的dispatchTouchEvent方法返回false,表示在action_down的時候,父類的dispatchTransformedTouchEvent方法返回false;若是返回true會調用addTouchTarget方法,給mFirstTouchTarget設置值:ui

private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
    final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;
}
複製代碼

緊接着在在後面又會調用了:spa

這句只有在view/viewgroup的 dispatchTouchEvent返回false的時候,纔會走這裏,因此後面的 action_moveaction_up都會走這裏,而此時傳入的child=null,從上面代碼能夠看到,直接調用了父類的 dispatchTouchEvent方法。因此從這裏不難看出在view/viewgroup的 dispatchTouchEvent返回false的時候直接調用了父類的 dispatchTouchEvent方法,所以只有action_down事件。

面試官:若是我只想有view的拖拽事件,而不想要view的點擊事件,讓你重寫這個view的拖拽怎麼設計

其實這道題考察你們對view的dispatchTouchEvent和view的onTouchEvent事件的處理流程,上面已經分析了想要view能執行到view的touch事件,那麼必需要求view的dispatchTouchEvent返回true,而dispatchTouchEvent返回true要麼是dispatchTouchEvent直接返回true或者view的onTouchEvent返回true。若是從效率上看,直接將dispatchTouchEvent返回true就ok,而不須要再去關心onTouchEvent方法。設計

viewgroup攔截

關於攔截無非就是攔截或不攔截,而攔截的條件是返回true,不攔截是返回false或返回super.onInterceptTouchEvent,默認的super是返回false的,所以能夠用super表示不攔截日誌

viewgroup攔截實際是經過在dispatchTouchEvent方法中,設置intercepted變量,若是在攔截方法裏面返回true,那麼intercepted爲true,若是爲true則在action_down的時候mFirstTouchTarget=null,那麼此時是直接調用dispatchTransformedTouchEvent傳入的child=null,所以將事件交給了super.dispatchTouchEvent,此時把它當成一個view來處理了。code

面試官:有個viewgroup,裏面有個view,若是view在dispatchTouchView中不分發事件,而且只在action_move中攔截touch事件向下分發,說說viewgroup到view的各個action是如何分發的?

分析

先貼出事例代碼:

testView在testViewgroup裏面,testViewgroup在action_move的時候攔截(onInterceptTouchEvent在move返回true),testView不進行分發(dispatchTouchEvent返回true) 咋們經過log來看結果:

這裏執行到TestViewgroup#dispatchTouchEvent的action_move以後就執行了TestView#dispatchTouchEvent的action_cancel,而後後面執行TestViewgroup#dispatchTouchEvent和TestViewgroup#onTouchEvent的action_move和action_up。 從前面viewgroup的dispatchTouchEvent分析知道,若是viewgroup在action_down中發現有子view的dispatchTouchEvent返回true,則 mFirstTouchTarget不爲空,緊接着在action_move的時候進行了攔截,則intercepted=true, 既然在move過程當中肯定了intercepted=true,mFirstTouchTarget不爲空,則能夠看viewgroup.dispatchTouchEvent部分代碼:

TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
    final TouchTarget next = target.next;
    //alreadyDispatchedToNewTouchTarget=false
    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
        handled = true;
    } else {
    	//因爲move過程當中intercepted=true,則cancelChild=true
        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                || intercepted;
        //看到了沒這裏就是觸發child的dispatchTouchEvent的action_cancel
        if (dispatchTransformedTouchEvent(ev, cancelChild,
                target.child, target.pointerIdBits)) {
            handled = true;
        }
        if (cancelChild) {
            if (predecessor == null) {
            	//因爲next=null,所以mFirstTouchTarget=null,因此在action_move剛進來的時候mFirstTouchTarget=null了,
            	//待會咱們經過反射看下該變量
                mFirstTouchTarget = next;
            } else {
                predecessor.next = next;
            }
            target.recycle();
            target = next;
            continue;
        }
    }
    predecessor = target;
    target = next;
}
複製代碼

上面也說明了在action_move進來的時候先是觸發了testView#dispatchTouchEvent的action_cancel,緊接着mFirstTouchTarget=null了,因爲mFirstTouchTarget是viewgroup類中私有的變量,咱們能夠經過反射調用該變量看下是否爲空:

//反射代碼,關於反射能夠看我以前的文章java反射整理

接着在testViewgroup#dispatchTouchEvent中獲取 mFirstTouchTarget屬性:

經過上面能夠驗證剛纔move過程當中mFirstTouchTarget爲空的判斷,日誌以下:

看到了沒,第一次move的時候 mFirstTouchTarget還不是null,第二次move的時候就是null了,所以在後續的move和up過程當中,只會此處:

看到了沒,這裏傳進去的child=null,根據上面分析可知,當爲null的時候,只會觸發super.dispatchTouchEvent,因此到了第二次的move以後,只能看到TestViewgroup的action_move和action_up了。

因此針對上面面試官提的問題,你們知道怎麼說了吧,仍是針對該問題作個小結:

小結

先是down事件會通過viewgroup的dispatchTouchEvent,再到viewgroup的onInterceptTouchEvent,最後到view的dispatchTouchEvent,此時mFirstTouchTarget不爲空,緊接着到了move首先到viewgroup的dispatchTouchEvent,再到viewgroup的onInterceptTouchEvent,因爲在move過程當中攔截了,緊接着走view的dispatchTouchEvent的action_cancel,此時接着把mFirstTouchTarget至爲null,所以後續的move和up事件只會走viewgroup的dispatchTouchEvent和onTouchEvent。 畫出一張圖來給你們看下:

好了,關於這個問題告一段落了,若是分析有問題,你們能夠提出疑問。

面試官:裏面的view在onTouchEvent中消費,而後拖動手指,直到手指從其餘他viewgroup上挪開手指。

其實這個問題在上面分析中已經分析過了,testview的onTouchEvent中消費,因此在action_down中mFirstTouchTarget不爲空,所以在action_move和action_up中mFirstTouchTarget仍是不爲空,因此無論手指是否已經離開了testview,action_move和action_up仍是會走testview的dispatchTouchEvent和onTouchEvent。

小結

首先肯定action_down過程當中mFirstTouchTarget是否爲空,若是不爲空,因此無論手指是否已經不在testView上了,action_move和action_up仍是會在testView的onTouchEvent上進行消費的。

面試官:view的onTouch和onTouchEvent事件的區別

這個問題就沒涉及viewgroup到view的事件傳遞,onTouch指setOnTouchListener的回調方法,它是優先於onTouchEvent事件的,你們能夠看下view的dispatchTouchEvent中有以下代碼:

我想這個地方不用多說吧,若是onTouch方法返回true,是不會觸發onTouchEvent事件的,因此在開篇第二個問題:若是想屏蔽掉view的點擊事件,只想要view的拖拽事件,該怎麼處理,其實這裏徹底能夠重寫setOnTouchListener的onTouch方法,而且onTouch裏面返回true就會屏蔽掉onClickListener事件。

面試官:view的onClick事件在何時觸發的,和onTouch有什麼區別

onClick事件是在onTouchEvent消費事件中的action_up觸發的,onTouch是在dispatchTouchEvent中觸發的,因此onTouch要先於onClick事件,咱們也能夠經過onTouch返回true來屏蔽掉onClick事件。

好了,關於此次我面試中遇到的事件分發主要是上面這幾個問題,你們有什麼其餘的問題,能夠在評論區互動。

總結

其實android事件分發核心是在viewgroupdispatchTouchEventaction_down過程當中找到mFirstTouchTarget是否爲空,經過反序遍歷子view的dispatchTouchEvent的方法,若是發現有一個子view的dispatchTouchEvent方法返回true,那麼mFirstTouchTarget就不爲空,不然爲空。若是mFirstTouchTarget不爲空,那麼action_moveaction_up纔會往下傳遞,若是在action_moveaction_up過程當中有viewgroup攔截了事件,則此時先向子view的dispatchTouchEvent傳遞一個action_cancel,而且將mFirstTouchTarget至爲null,因此此時action_moveaction_up只會走viewgroupdispatchTouchEventonTouchEvent;若是mFirstTouchTargetaction_down過程當中就已經null的話,則從action_down一直向上層view傳遞,不會有後續的action_moveaction_up了。

其實看到身邊不少朋友抱怨本身的工資很低,包括筆者也是同樣的,其緣由是在面試過程當中沒有給面試官一個很好的答案。因此筆者會持續更新面試過程當中遇到的問題,也但願你們和筆者一塊兒進步,一塊兒學習。好了,此次文章就到這裏,有什麼問題,歡迎你們互動。

相關文章
相關標籤/搜索