Android ViewGroup事件分發(fā)機(jī)制

參考資料

鴻洋版ViewGroup事件分發(fā)機(jī)制
郭霖版ViewGroup事件分發(fā)機(jī)制
Android開發(fā)藝術(shù)探索

上一篇已經(jīng)分析了Android View的事件分發(fā)機(jī)制虑省,本篇將根據(jù)源碼講解ViewGroup的事件分發(fā)機(jī)制螟凭,View的一大難題是滑動(dòng)沖突称龙,滑動(dòng)沖突的理論基礎(chǔ)就是事件分發(fā)機(jī)制启摄,所以了解事件分發(fā)機(jī)制凌外,也有益于大家了解沖突產(chǎn)生的原因蹦玫,以及對(duì)沖突進(jìn)行處理。
關(guān)于事件傳遞機(jī)制只锻,這里先給出一些結(jié)論玖像,根據(jù)這些結(jié)論可以更好的理解整個(gè)傳遞機(jī)制:
(1)同一事件序列是指從手指觸摸屏幕的那一刻起,最手指離開屏幕的那一刻結(jié)束炬藤,在整個(gè)過程中
所產(chǎn)生的一系列事件,這個(gè)事件序列以down事件開始碴里,中間含有不定數(shù)量的move沈矿,最終以u(píng)p事件結(jié)束;
(2)事件傳遞都是由外向內(nèi)傳遞的咬腋,即事件總是先傳遞給父view羹膳,然后再由父View傳遞給子view;
通過requestDisallowInterceptTouchEvent可以在子元素中干預(yù)父View的時(shí)間分發(fā)過程根竿,但是ACTION_DOWN除外陵像;
(3)如果某個(gè)View一旦開始攔截某個(gè)事件,那么同一事件序列都只能由他來處理寇壳,
并且它的onInterceptTouchEvent不會(huì)再次被調(diào)用醒颖。
(4)View沒有onInterceptTouchEvent方法,一旦有事件傳遞給它壳炎,那么它的onTouchEvent就會(huì)被調(diào)用泞歉,
View的ontouchEvent默認(rèn)都會(huì)消耗事件,viewGroup默認(rèn)不攔截事件匿辩,Android源碼中可以看到onInterceptTouchEvent
默認(rèn)返回值為false腰耙;
(5)子元素是否可以接受點(diǎn)擊事件?第一铲球,子元素是否在做動(dòng)畫挺庞;第二,點(diǎn)擊事件的坐標(biāo)是否落在子元素區(qū)域中稼病。
(6)如果所有子元素都沒有處理事件选侨,這里包含兩種情況,第一ViewGroup沒有子元素然走,第二子元素處理了點(diǎn)擊事件侵俗,但是在dispatchTouchEvent中返回false,一般是因?yàn)樽釉卦趏nTouchEvent返回false丰刊,這兩種情況ViewGroup會(huì)自己處理點(diǎn)擊事件
(7)事件大體傳遞流程:VeiwGroup的dispatchTouchEvent -> VeiwGroup的onInterceptTouchEvent ->View的dispatchTouchEvent ->View的onTouchEvent

源碼解析

ViewGroup.dispatchTouchEvent():

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // Handle an initial down.
            // 處理原始的DOWN時(shí)間
            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.
                // 需要在新事件開始時(shí)處理完上一個(gè)事件隘谣,并且調(diào)用resetTouchState重置狀態(tài)
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

            // Check for interception.
            // 檢查當(dāng)前View是否攔截事件
            // ViewGroup在如下兩種情況下會(huì)判斷是否攔截當(dāng)前事件:事件類型為down或者mFirstTouchTarget != null。
            // 當(dāng)ViewGroup不攔截事件并將事件交由子元素處理時(shí),mFirstTouchTarget會(huì)被賦值也就是mFirstTouchTarget != null寻歧。
            // 這樣當(dāng)move事件和up事件到來時(shí)掌栅,并且事件已經(jīng)被分發(fā)下去,那么onInterceptTouchEvent這個(gè)方法將不會(huì)再被調(diào)用码泛。
            //所以當(dāng)前ViewGroup攔截事件之后就不會(huì)再次調(diào)用onInterceptTouchEvent方法猾封;
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    //調(diào)用onInterceptTouchEvent方法判斷是否攔截當(dāng)前事件,ViewGroup默認(rèn)返回false噪珊;
                    //如果攔截事件將intercepted置為true晌缘;
                    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;
            }

            // 如果事件未被取消且未被攔截,如果攔截事件會(huì)將intercepted置為true痢站;
            if (!canceled && !intercepted) {
                //ACTION_DOWN事件 
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    // 當(dāng)ViewGroup不攔截事件的時(shí)候磷箕,事件會(huì)向下分發(fā)交由它的子View進(jìn)行處理
                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        // Find a child that can receive the event.
                        // Scan children from front to back.
                        // 從上至下去尋找一個(gè)可以接收該事件的子View
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        // 遍歷ViewGroup的所有子元素,然后判斷子元素是否能夠收到點(diǎn)擊事件
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);

                            // If there is a view that has accessibility focus we want it
                            // to get the event first and if not handled we will perform a
                            // normal dispatch. We may do a double iteration but this is
                            // safer given the timeframe.
                            // 是否能夠收到點(diǎn)擊事件主要由兩點(diǎn)來衡量:
                            // 子元素是否在播放動(dòng)畫和點(diǎn)擊事件的坐標(biāo)是否落在子元素的區(qū)域內(nèi)阵难。
                            if (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }

                            // 子元素是否能夠接收PointerEvent岳枷,或事件有沒有落在子元素的邊界范圍
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }
                            resetCancelNextUpFlag(child);
                            // 如果某個(gè)子元素滿足條件,那么事件就會(huì)傳遞給它處理呜叫,
                            // dispatchTransformedTouchEvent這個(gè)方法實(shí)際上就是調(diào)用子元素的dispatchTouchEvent方法空繁,
                            // 如果子元素仍然是一個(gè)ViewGroup,則遞歸調(diào)用重復(fù)此過程朱庆。
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                // 子View在其邊界范圍內(nèi)接收事件
                                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();
                                // 如果子元素的dispatchTouchEvent返回true盛泡,表示子元素已經(jīng)處理完事件,
                                // 那么mFirstTouchTarget就會(huì)被賦值同時(shí)跳出for循環(huán)娱颊。
                                // mFirstTouchTarget的賦值在addTouchTarget內(nèi)部完成
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }

                            // The accessibility focus didn't handle the event, so clear
                            // the flag and do a normal dispatch to all children.
                            ev.setTargetAccessibilityFocus(false);
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }
                }
            }

            // Dispatch to touch targets.
            // 如果遍歷完所有的子元素事件沒有被合適處理饭于,有兩種情況:
            // 1. ViewGroup沒有子元素
            // 2. 子元素處理了點(diǎn)擊事件,但是dispatchTouchEvent返回false
            // 這時(shí)ViewGroup會(huì)自己處理點(diǎn)擊事件维蒙。
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                // 這里的第三個(gè)參數(shù)child為null掰吕,此時(shí)會(huì)調(diào)用handled = super.dispatchTouchEvent(event);
                //最終會(huì)調(diào)用ViewGroup自身的onTouchEvent來處理事件;
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                TouchTarget predecessor = null;
                //將處理該事件的子view復(fù)制給target颅痊,由子元素來處理該事件
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        //該方法最終會(huì)調(diào)用子元素的dispatchTouchEvent殖熟,傳給給子元素來處理事件    
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                    }
                }
            }

            // Update list of touch targets for pointer up or cancel, if needed.
            if (canceled
                    || actionMasked == MotionEvent.ACTION_UP
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                //UP事件到來,重置狀態(tài)斑响,例如將處理該事件的子view mFirstTouchTarget置為null菱属;   
                resetTouchState();
            }
        }
        return handled;
    }

說明:
(1)如果onInterceptTouchEvent返回true,表示ViewGroup將攔截事件舰罚,會(huì)將intercepted設(shè)置true纽门,這樣就不會(huì)遍歷子view尋找事件接受者;這樣mFirstTouchTarget為null营罢,會(huì)調(diào)用dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        if (child == null) {
            //因?yàn)閭魅氲膮?shù)為null赏陵,所以調(diào)用ViewGroup自身的dispatchTouchEvent方法來處理事件饼齿;
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            //當(dāng)有子元素處理事件時(shí),會(huì)調(diào)用子元素的dispatchTouchEvent
            handled = child.dispatchTouchEvent(transformedEvent);
        }
        return handled;
    }

(2)當(dāng)子元素處理事件時(shí)會(huì)調(diào)用addTouchTarget()蝙搔;

    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        // 由子元素處理事件時(shí)缕溉,將子元素賦值給mFirstTouchTarget
        //這樣通過mFirstTouchTarget是否為null,就可以知道事件時(shí)由子view還是ViewGroup來處理事件吃型;
        mFirstTouchTarget = target;
        return target;
    }

(3)如果ViewGroup找到了能夠處理該事件的View证鸥,則直接交給子View處理,自己的onTouchEvent不會(huì)被觸發(fā)勤晚;
(4)可以通過復(fù)寫onInterceptTouchEvent(ev)方法枉层,攔截子View的事件(即return true),把事件交給自己處理赐写,則會(huì)執(zhí)行自己對(duì)應(yīng)的onTouchEvent方法
(5)子View可以通過調(diào)用getParent().requestDisallowInterceptTouchEvent(true); 阻止ViewGroup對(duì)其MOVE或者UP事件進(jìn)行攔截鸟蜡;
(6)父View可以通過onInterceptTouchEvent來攔截事件,但是如果父view不攔截down事件血淌,子view如果調(diào)用requestDisallowInterceptTouchEvent方法矩欠,那么即使父View在move和up的時(shí)候return true财剖,也不會(huì)將事件攔截掉悠夯,也只會(huì)調(diào)用子view的onTouchEvent;當(dāng)面對(duì)滑動(dòng)沖突時(shí)躺坟,我們可以考慮通過requestDisallowInterceptTouchEvent設(shè)置FLAG_DISALLOW_INTERCEPT標(biāo)志位來解決滑動(dòng)沖突沦补;如果父View攔截Down事件,那么子View將不會(huì)收到事件咪橙;
(7)滑動(dòng)沖突的理論基礎(chǔ)是事件分發(fā)機(jī)制夕膀,所以熟悉事件分發(fā)機(jī)制有助于我們解決滑動(dòng)沖突相關(guān)問題;
以上就是事件分發(fā)機(jī)制的全部?jī)?nèi)容美侦。最后产舞,本文部分內(nèi)容直接從其他地方文章直接Copy而來,感謝本文內(nèi)容所參考文章的作者菠剩;

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末易猫,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子具壮,更是在濱河造成了極大的恐慌准颓,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,888評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件棺妓,死亡現(xiàn)場(chǎng)離奇詭異攘已,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)怜跑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門样勃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事彤灶】从祝” “怎么了?”我有些...
    開封第一講書人閱讀 168,386評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵幌陕,是天一觀的道長(zhǎng)诵姜。 經(jīng)常有香客問我,道長(zhǎng)搏熄,這世上最難降的妖魔是什么棚唆? 我笑而不...
    開封第一講書人閱讀 59,726評(píng)論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮心例,結(jié)果婚禮上宵凌,老公的妹妹穿的比我還像新娘。我一直安慰自己止后,他們只是感情好瞎惫,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,729評(píng)論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著译株,像睡著了一般瓜喇。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上歉糜,一...
    開封第一講書人閱讀 52,337評(píng)論 1 310
  • 那天乘寒,我揣著相機(jī)與錄音,去河邊找鬼匪补。 笑死伞辛,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的夯缺。 我是一名探鬼主播蚤氏,決...
    沈念sama閱讀 40,902評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼踊兜!你這毒婦竟也來了竿滨?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,807評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤润文,失蹤者是張志新(化名)和其女友劉穎姐呐,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體典蝌,經(jīng)...
    沈念sama閱讀 46,349評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡曙砂,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,439評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了骏掀。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鸠澈。...
    茶點(diǎn)故事閱讀 40,567評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡柱告,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出笑陈,到底是詐尸還是另有隱情际度,我是刑警寧澤,帶...
    沈念sama閱讀 36,242評(píng)論 5 350
  • 正文 年R本政府宣布涵妥,位于F島的核電站乖菱,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏蓬网。R本人自食惡果不足惜窒所,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,933評(píng)論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望帆锋。 院中可真熱鬧吵取,春花似錦、人聲如沸锯厢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,420評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽实辑。三九已至捺氢,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間徙菠,已是汗流浹背讯沈。 一陣腳步聲響...
    開封第一講書人閱讀 33,531評(píng)論 1 272
  • 我被黑心中介騙來泰國打工郁岩, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留婿奔,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,995評(píng)論 3 377
  • 正文 我出身青樓问慎,卻偏偏與公主長(zhǎng)得像萍摊,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子如叼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,585評(píng)論 2 359

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