View事件分發(fā)機制

前言

Android源碼分析之View系列之事件分發(fā)機制

同步至個人博客


正文


一. 概述

View的觸摸事件分發(fā)是View系列中的一個重難點, 主要需要掌握的是MotionEvent的傳遞規(guī)則和處理規(guī)則, 這是自定義View中沖突處理的理論來源~

觸摸事件分發(fā)的處理主要是對MotionEvent的處理, MotionEvent封裝了用戶的一系列行為, 如: ACTION_DOWN(手指剛觸摸屏幕), ACTION_MOVE(手指在屏幕上滑動), ACTION_UP(手指抬起)等; 以及事件發(fā)生的坐標(通過MotionEvent.getX(), MotionEvent.getY()可以得到)等

在開始講解之前需要明確的一些概念是:

  1. 一個事件序列: 指的是一次完整的觸摸過程, 即從ACTION_DOWN(手指觸摸屏幕)開始, 到中間的一系列ACTION_MOVE(手指滑動), 最后到ACTION_UP為止(手指抬起); 總結(jié)起來就是down...move...move..up

  2. 觸摸事件的分發(fā)其實是一個從上到下不斷遞歸傳遞和攔截的過程; 一個大致的傳遞流程是: Activity --> Window --> ViewGroup --> View, 當然如果向下傳遞但是MotionEvent又沒有消耗的話, 又會逐層返回, 最終將沒有消耗的MotionEvent交給Activity處理


二. 事件分發(fā)之源

觸摸事件產(chǎn)生和分發(fā)的源頭是在Activity中處理的, 即在ActivitydispatchTouchEvent()中; 如下; 處理思路也很簡單, 只是單純的向下分發(fā)而已, 如果事件沒有得到處理, 那么最終就交給ActivityonTouchEvent()處理; 另外, 這里還為用戶提供了一個監(jiān)聽和攔截事件的方法, 即onUserInteraction(), 該方法在Activity中是一個空實現(xiàn), 可以重寫該方法在事件向下傳遞之前進行特殊攔截和處理

    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();  // 自定義事件攔截
        }
        if (getWindow().superDispatchTouchEvent(ev)) {  // 通過Window向下分發(fā)事件
            return true;
        }
        return onTouchEvent(ev);  // 如果事件最終沒有被處理, 那么交給Activity自己的onTouchEvent()來處理
    }

Activity中的Window實際上是PhoneWindow, 這里通過PhoneWindow.superDispatchTouchEvent()傳遞實際上是只是簡單調(diào)用了mDecor.superDispatchTouchEvent(event), 而這里的mDecor實際上是DecorView, 是一個FrameLayout(ViewGroup), 在DecorViewsuperDispatchTouchEvent()方法中, 也只是簡單的將事件傳遞給ViewGroup進行分發(fā)(即ViewGroup.dispatchTouchEvent()); 到這里就將事件傳遞給ViewGroupView處理了, 也是事件分發(fā)處理中最主要的一部分


三. ViewGroup分發(fā)事件

ViewGroup.dispatchTouchEvent()中對事件的分發(fā)處理過程比較長, 實際上大致分成了三個部分來處理

3.1 事件攔截

首先, ViewGroup會判斷是否進行事件攔截, 如下; 從后面將事件分發(fā)給子View的條件可以看出, 如果ViewGroup進行了事件攔截, 那么該事件序列將不再向下分發(fā); 這里還需要注意的一點是, ViewGroup判斷是否進行事件攔截的條件一個是為ACTION_DOWN時, 另一個是mFirstTouchTarget != null時; 也就是說一個事件序列的在開始時, 即ACTION_DOWN時一定會調(diào)用ViewGrouponInterceptTouchEvent(當然, 還有一個影響因素是FLAG_DISALLOW_INTERCEPT, 我們稍后講解); 至于mFirstTouchTarget的賦值是在后面分發(fā)給子View時, 如果有子View處理了事件那么mFirstTouchTarget將會被賦值;

上面是ViewGroup進行事件攔截的基本思路, 簡單總結(jié)起來就是:

  1. ACTION_DOWN時, 如果ViewGroup進行了事件攔截(onInterceptTouchEvent()返回true), 那么同一事件序列將不再向下分發(fā)(因為之后的ACTION_MOVEACTION_UP到來時, 由于之前ACTION_DOWN時進行了事件攔截, mFirstTouchTarget沒有機會賦值, 所以仍然為null, 故直接走else語句, 即intercepted = true);

  2. ACTION_DOWN時, 如果ViewGroup不進行事件攔截, 并且在事件向下分發(fā)時, 有子View處理了事件, 那么mFirstTouchTarget將會被賦值, 即不為null, 此時仍然會繼續(xù)調(diào)用ViewGrouponInterceptTouchEvent判斷是否進行事件攔截, 需要注意的是此時仍然在同一事件序列中

  3. ACTION_DOWN時, 如果ViewGroup不進行事件攔截, 并且在事件向下分發(fā)時, 也沒有子View進行事件處理, 那么mFirstTouchTarget仍為null, 即走else, 交由ViewGroup處理事件

: 只有當ViewGroup攔截了事件或者子View不處理事件時, onInterceptTouchEvent才只會調(diào)用一次

            // ViewGroup是否進行事件攔截
            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;
            }

            if (!canceled && !intercepted) { // 如果攔截事件, 將不再分發(fā)給子View
                // 事件分發(fā)給子View
                ....
            }

另外, 上面還講了, 在ACTION_DOWN時, 一定會調(diào)用ViewGrouponInterceptTouchEvent, 這里還有一個影響因素是標志位FLAG_DISALLOW_INTERCEPT, 該標志位是通過requestDisallowInterceptTouchEvent()設(shè)置的, 作用是在子View中強制父ViewGroup不進行事件攔截, 但是該標志位不能影響ACTION_DOWN, 因為在一個事件序列開始之前會先進行狀態(tài)重置, 如下; 在resetTouchState()中會將該標志位重置, 所以就不會影響ACTION_DOWN啦~

            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);
                resetTouchState(); // 狀態(tài)重置
            }

3.2 事件分發(fā)

如果ViewGroup不進行事件攔截的話, 會將事件分發(fā)給子View處理; 事件分發(fā)的主要代碼如下; 邏輯也比較簡單, 就是遍歷所有的子View, 然后通過dispatchTransformedTouchEvent()進行將事件傳遞給子View

                    if (!canceled && !intercepted) {
                        ...
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                    // childIndex points into presorted list, find original index
                                    for (int j = 0; j < childrenCount; j++) {
                                        if (children[childIndex] == mChildren[j]) {
                                            mLastTouchDownIndex = j;
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                newTouchTarget = addTouchTarget(child, idBitsToAssign); // 設(shè)置mFirstTouchTarget的值
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
                        }
                        ...
                    }

我們繼續(xù)來看dispatchTransformedTouchEvent()的處理過程, 如下; 從上面的代碼中我們可以看出, 將事件分發(fā)給子View的時候, 調(diào)用dispatchTransformedTouchEvent()傳入的child非空, 所以應(yīng)該調(diào)用的是child.dispatchTouchEvent(event), 這樣就將事件傳遞到子View中去了; 這里關(guān)于子ViewdispatchTouchEvent()處理在后文繼續(xù)講解

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        ...
        if (newPointerIdBits == oldPointerIdBits) {
            if (child == null || child.hasIdentityMatrix()) {
                if (child == null) {
                    handled = super.dispatchTouchEvent(event); // child非null
                } else {
                    final float offsetX = mScrollX - child.mLeft;
                    final float offsetY = mScrollY - child.mTop;
                    event.offsetLocation(offsetX, offsetY);

                    handled = child.dispatchTouchEvent(event); // 調(diào)用child.dispatchTouchEvent(event)

                    event.offsetLocation(-offsetX, -offsetY);
                }
                return handled;
            }
            transformedEvent = MotionEvent.obtain(event);
        } else {
            transformedEvent = event.split(newPointerIdBits);
        }
        ...
}

上面我們說過, 如果子View處理了事件的話, 將會去設(shè)置mFirstTouchTarget的值, 該值的設(shè)置其實是在addTouchTarget()中, 也就是說, 當dispatchTransformedTouchEvent()返回true, 即有子View處理了事件的話, 就會去調(diào)用該函數(shù), 也就證明了我們前面所說的; 我們來看addTouchTarget(), 如下; 可以看出這里實際上相當于一個單鏈表

    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget; 
        mFirstTouchTarget = target; // 設(shè)置mFirstTouchTarget
        return target;
    }

3.3 ViewGroup處理事件

如果ViewGroup攔截了事件或者子View沒有進行事件處理, 那么ViewGroup將進行事件處理, 如下; 可以看出, ViewGroup進行事件處理也是調(diào)用dispatchTransformedTouchEvent(), 只是傳入的childnull, 那么從上面的dispatchTransformedTouchEvent()代碼中我們可以看出, 如果childnull調(diào)用的應(yīng)該就是super.dispatchTouchEvent(event)進行事件處理

            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            }

需要注意的是不管是super.dispatchTouchEvent(event)還是child.dispatchTouchEvent(event), 調(diào)用的其實都是View.dispatchTouchEvent(), 所以接下來我們要看的就是View中對事件的處理


四. View事件處理

需要注意的是, View中沒有onInterceptTouchEvent()方法來進行事件攔截; 我們這里關(guān)注的, 主要是View對事件的處理, 這里的View包括ViewGroup進行事件攔截之后對事件的處理以及子View對事件的處理; 因為從前面我們已經(jīng)說了, 不管是調(diào)用的super.dispatchTouchEvent()(ViewGroup處理事件)還是child.dispatchTouchEvent()(子View處理事件), 其實都是調(diào)用的View.dispatchTouchEvent(); 所以二者對事件的處理實際上是一樣的, 同時需要注意的是, 這一節(jié)不包括事件的分發(fā)了,
事件分發(fā)在上一節(jié)中已經(jīng)講解完啦~

觸摸事件的處理主要涉及到OnTouchListener, onTouchEventonClick的處理優(yōu)先級

主要代碼如下; 可以看出先處理的是OnTouchListener, 如果View沒有設(shè)置OnTouchListener(View.setOnTouchListener())的話, 再去處理onTouchEvent(), 所以OnTouchListener的優(yōu)先級比onTouchEvent高; 同時還要注意的一點是, 如果設(shè)置了OnTouchListener的話, ViewonTouchEvent將不再調(diào)用

public boolean dispatchTouchEvent(MotionEvent event) {
    ...
    if (li != null && li.mOnTouchListener != null
            && (mViewFlags & ENABLED_MASK) == ENABLED
            && li.mOnTouchListener.onTouch(this, event)) {  // OnTouchListener
        result = true;
    }
    if (!result && onTouchEvent(event)) { // onTouchEvent
        result = true;
    }
    ...
}

這里還有一個onClick()其實是在onTouchEvent()中處理的; 如下; onClick是在performClickInternal()中觸發(fā)的, 可以看出, 要觸發(fā)onClick需要的條件是: View是可以點擊的(clickable), 這里的可點擊包括了CLICKABLELONG_CLICKABLE, 注意Viewenable屬性不影響onTouchEvent的返回值, 只要它可點擊, 那么onTouchEvent()就會處理該點擊事件

public boolean onTouchEvent(MotionEvent event) {
    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                ...
                if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                    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();
                        }
                    }
                }
                ...
    }
}

而在performClickInternal()中, 則是去調(diào)用了performClick()進行處理, 在performClick()會判斷, 如果設(shè)置了OnClickListener, 則會去調(diào)用OnClickListener, 代碼比較簡單, 就不貼啦~


五. 總結(jié)

到這里, View的事件分發(fā)和處理流程就分析結(jié)束啦~; 我們最開始講事件分發(fā)之源時講Activity對事件的傳遞的時候, 如果getWindow().superDispatchTouchEvent()返回false的話, 就最終將事件交給ActivityonTouchEvent()處理, 這種情況其實對應(yīng)的是ViewGroupView都不進行事件處理, 那么就逐層回傳咯~

最后將上述流程總結(jié)為下圖:

View事件分發(fā).png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末芹彬,一起剝皮案震驚了整個濱河市微王,隨后出現(xiàn)的幾起案子苟径,更是在濱河造成了極大的恐慌亲族,老刑警劉巖茫经,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蒸眠,死亡現(xiàn)場離奇詭異,居然都是意外死亡缚忧,警方通過查閱死者的電腦和手機体箕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進店門专钉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人累铅,你說我怎么就攤上這事跃须。” “怎么了娃兽?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵菇民,是天一觀的道長。 經(jīng)常有香客問我投储,道長第练,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任玛荞,我火速辦了婚禮娇掏,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘勋眯。我一直安慰自己婴梧,他們只是感情好下梢,可當我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著志秃,像睡著了一般怔球。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上浮还,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天竟坛,我揣著相機與錄音,去河邊找鬼钧舌。 笑死担汤,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的洼冻。 我是一名探鬼主播崭歧,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼撞牢!你這毒婦竟也來了率碾?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤屋彪,失蹤者是張志新(化名)和其女友劉穎所宰,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體畜挥,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡仔粥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了蟹但。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片躯泰。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖华糖,靈堂內(nèi)的尸體忽然破棺而出麦向,到底是詐尸還是另有隱情,我是刑警寧澤缅阳,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布磕蛇,位于F島的核電站,受9級特大地震影響十办,放射性物質(zhì)發(fā)生泄漏秀撇。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一向族、第九天 我趴在偏房一處隱蔽的房頂上張望呵燕。 院中可真熱鬧,春花似錦件相、人聲如沸再扭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽泛范。三九已至让虐,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間罢荡,已是汗流浹背赡突。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留区赵,地道東北人惭缰。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像笼才,于是被迫代替她去往敵國和親漱受。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內(nèi)容