View的事件分發(fā)機(jī)制

1.前言

View的事件分發(fā)機(jī)制是面試的要點(diǎn),也是必須要吃透的基礎(chǔ)知識(shí)。雖然平時(shí)用到的地方不是那么頻繁,但是一旦要用,如果這個(gè)不夠扎實(shí),就會(huì)卡手。就獨(dú)立一篇來(lái)整理,日常開(kāi)發(fā)中有發(fā)現(xiàn)有要注意的事情也在此補(bǔ)充歸納。
??與View做交互操作會(huì)產(chǎn)生事件,事件起點(diǎn)是誰(shuí),系統(tǒng)是如何將一個(gè)事件從起點(diǎn)傳到目標(biāo)View的。帶著疑問(wèn)去探索。

2.點(diǎn)擊事件的傳遞規(guī)則

起始與流通:

從Android應(yīng)用層看,事件起于Activity,Activity也有dispatchTouchEvent,onTouchEvent方法。
??Activity會(huì)傳遞給Window,Window會(huì)傳給頂層View,頂層View分發(fā)給子View,一級(jí)級(jí)向下分發(fā),如果某層不消費(fèi),則返給父層處理。
Activity -》 Window ->Decor View -> ContentView
??形象地來(lái)看,就如上級(jí)分派任務(wù)給下級(jí)。

三個(gè)核心方法:

  1. public boolean dispatchTouchEvent(MotionEvent ev)
    View都有。事件能傳給當(dāng)前View,則一定會(huì)調(diào)用.在這個(gè)方法會(huì)根據(jù)不同條件去調(diào)用onInterceptTouchEvent和onTouchEvent.返回值表示當(dāng)前view是否消費(fèi)了事件。
    true消費(fèi)了;false沒(méi)消費(fèi),事件交給父View處理.所有的父View都不處理,則傳回Activity,在Activity中消亡。
  2. public boolean onInterceptTouchEvent(MotionEvent ev)
    僅ViewGroup有。返回值表示是否攔截事件。默認(rèn)false不攔截,攔截則交給onTouch處理.不攔截交給子View處理(調(diào)用子View的dispatchTouchEvent())。
  3. public boolean onTouchEvent(MotionEvent ev)
    View都有。返回值表示對(duì)事件的處理。true表示處理了,false表示不處理。

改變這三個(gè)方法的返回值可以改變事件分發(fā)傳遞的過(guò)程,需要注意這樣做默認(rèn)的父類(lèi)處理邏輯就不會(huì)執(zhí)行了。

onTouchListener與方法優(yōu)先級(jí):

View可以設(shè)置onTouchListener,這個(gè)監(jiān)聽(tīng)能干擾View的事件分發(fā)過(guò)程。它會(huì)先于onTounchEvent執(zhí)行,也就是它的優(yōu)先級(jí)高。
??優(yōu)先級(jí)onTouchListener > onTouchEvent > onClickListener(具體View交互回調(diào))
??onTouchListener中方法onTouch默認(rèn)返回false,onTouchEvent會(huì)調(diào)用;true,不會(huì)調(diào)用OnTouchEvent,認(rèn)為是當(dāng)前View消費(fèi)事件,即dispatchTouchEvent 返回true。
??無(wú)論onTouch怎樣,dispatchTouchEvent最終都會(huì)調(diào)用,可以認(rèn)為dispatchTouchEvent的優(yōu)先級(jí)要高于onTouchListener。
View的dispatchTouchEvent()源碼直觀地體現(xiàn)了這一點(diǎn).

事件與事件序列:

一個(gè)事件序列指手指從接觸屏幕一刻起到離開(kāi)屏幕這個(gè)過(guò)程產(chǎn)生的一系列事件。事件序列完整才會(huì)有相應(yīng)的View交互回調(diào)方法執(zhí)行。有ACTION_DOWN起。。。ACTION_UP止。
??正常情況下,一個(gè)事件序列交由一個(gè)View處理。一些特殊的手段可以讓兩個(gè)View都響應(yīng),比如在一個(gè)View的onTouchEvent中強(qiáng)行傳遞給其他View。

ViewGroup攔截ACTION_DOWN,ACTION_Down會(huì)傳給自身的onTouchEvent.事件序列中之后事件不會(huì)再向下分發(fā),傳給自身的onTouchEvent.
ViewGroup攔截除ACTION_DOWN之外的事件,會(huì)產(chǎn)生一個(gè)ACTION_CANCEL的事件分發(fā)給子View.事件序列中之后的事件傳給自身的onTouchEvent.此時(shí)mFirstTouchTarget為null,dispatchTouchEvent()不會(huì)調(diào)用onIntercepte()判斷是否攔截,直接將intercepted賦值true.

View如果不消耗ACTION_DOWN事件(在onTounch或dispatchTouchEvent返回false),ACTION_DOWN事件和事件序列中之后的事件都交給父View處理.
View如果不消耗除ACTION_DOWN事件之外的事件,當(dāng)前事件交給父View處理,之后還是會(huì)繼續(xù)接收到事件序列中之后的事件.

子View用requestDisallowInterceptTouchEvent設(shè)置標(biāo)記量mGroupFlags是否允許父View攔截事件,默認(rèn)值是允許.每當(dāng)有ACTION_DOWN事件來(lái)時(shí)會(huì)重置此標(biāo)記量允許.所以面對(duì)ACTION_DOWN事件時(shí),ViewGroup總會(huì)調(diào)用自己的InterceptTouchEvent方法來(lái)詢(xún)問(wèn)自己是否要攔截事件。當(dāng)此標(biāo)記量設(shè)置成不允許的時(shí)候,除ACTION_DOWN事件,dispatchTouchEvent()不會(huì)調(diào)用onIntercepte()判斷是否攔截,直接將intercepted賦值false.

longClickable與clickable對(duì)View事件消耗的影響

View的onTouchEvent默認(rèn)都會(huì)消耗事件,除非是不可點(diǎn)擊的,即longClickable與clickable都為false.
??View的enable不影響事件分發(fā).點(diǎn)擊事件不會(huì)響應(yīng).
源碼分析可以參考:
http://blog.csdn.net/weixin_37077539/article/details/54895485

可點(diǎn)擊這個(gè)屬性對(duì)View的事件消費(fèi)有影響,在處理View的事件分發(fā)邏輯,記得檢查View是否可點(diǎn)擊.

dispatchTouchEvent()源碼分析

原理在源碼體現(xiàn).
傳送門(mén):http://www.itdecent.cn/p/93a060053cbc

3.滑動(dòng)沖突實(shí)訓(xùn)

場(chǎng)景一:外部滑動(dòng)方向與內(nèi)部不一致。(外橫內(nèi)豎,viewpage已做外部攔截)
A:外部攔截法(常用)
重寫(xiě)父容器的onInterceptTouchEvent().當(dāng)事件為ACTION_MOVE的時(shí)候。根據(jù)條件判斷外部是否要攔截。ACTION_DOWN只在外層滑動(dòng)將結(jié)束,優(yōu)化滑動(dòng)體驗(yàn)的時(shí)候才加上.

    boolean intercepted = false;
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                intercepted = false;
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                    intercepted = true;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastXIntercept;
                int deltaY = y - mLastYIntercept;
                intercepted = Math.abs(deltaX) > Math.abs(deltaY);
                break;
            case MotionEvent.ACTION_UP:
               //給子View響應(yīng)點(diǎn)擊事件
                intercepted = false;
                break;
        }
        mLastX = x;
        mLastY = y;
        mLastXIntercept = x;
        mLastYIntercept = y;
        return intercepted;

B:內(nèi)部攔截法
內(nèi)部攔截要重寫(xiě)父View的onInterceptTouchEvent與子View的dispatchTouchEvent().利用的是子View可以用getParent.requestDisallowInterceptTouchEvent()方法改變父View的攔截標(biāo)記位mGroupFlags,達(dá)到事件按照業(yè)務(wù)規(guī)則給自己或者父View處理的目的.
使用這個(gè)方法需要注意
1.父View不能將ACTION_DOWN攔截掉,ACTION_DOWN一旦被攔截,子View得不到事件序列后續(xù)的事件.
2.較外部攔截的效果需要處理內(nèi)部滑動(dòng)后不再將事件給父View.

 //內(nèi)部攔截父View需要的代碼
        mLastX = (int) ev.getRawX();
        mLastY = (int) ev.getRawY();
        if (MotionEvent.ACTION_DOWN == ev.getAction()) {
            if (!mScroller.isFinished()) {
                mScroller.abortAnimation();
                return true;
            }
            return false;
        } else {
            return true;
        }
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        float x = ev.getRawX();
        float y = ev.getRawY();
        if (MotionEvent.ACTION_DOWN == ev.getAction()) {
            getParent().requestDisallowInterceptTouchEvent(true);
            slop = false;
        }
        if (MotionEvent.ACTION_MOVE == ev.getAction()) {
            if (Math.abs(mLastY - y) < Math.abs(mLastX - x)) {
                if (!slop) getParent().requestDisallowInterceptTouchEvent(false);
            } else {
                int touchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
                if (Math.abs(y - mLastY) > touchSlop) {
                    //已經(jīng)開(kāi)始了豎向滑動(dòng)
                    slop = true;
                }
            }
        }
        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(ev);
    }

場(chǎng)景二:外部滑動(dòng)方向與內(nèi)部一致。
同場(chǎng)景一處理方式,區(qū)別在判斷條件由業(yè)務(wù)定,看業(yè)務(wù)什么時(shí)候需要交給外層,什么時(shí)候需要交給內(nèi)層。

場(chǎng)景三:場(chǎng)景一場(chǎng)景二交替同時(shí)出現(xiàn)
剝繭法層層解決.
可以加一些回彈或過(guò)渡效果使滑動(dòng)沖突處理的過(guò)程顯得更加圓滑。

后記

分發(fā)機(jī)制比較靈活,比較細(xì)膩,花我老長(zhǎng)時(shí)間去分析理解鞏固,源碼風(fēng)格不怎么利于閱讀...233.有些情況套用公式是不行的,要多動(dòng)手結(jié)合真實(shí)項(xiàng)目代碼思考練習(xí).
如果碰到不懂的地方,翻源碼思考是一個(gè)比較好的選擇.
Action_Cancle事件怎么產(chǎn)生與如何影響分發(fā)的,待做.
接下來(lái)會(huì)想對(duì)項(xiàng)目中RecyclerView左滑的實(shí)例進(jìn)行思考探究.在這之前也需要有一定的View的繪制機(jī)制基礎(chǔ).

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容