Android觸摸事件-02ViewGroup觸摸事件及源碼分析

參考文檔

API

  • ViewGroup的觸摸事件處理,很多繼承于view,一方面它重載了dispatchTouchEvent,另外一個主要的區(qū)別在于新添加了函數(shù)onInterceptTouchEvent

dispatchTouchEvent

  • 如果ViewGroup的某個孩子沒有接受ACTION_DOWN事件丈攒;那么蜒程,ACTION_MOVE和ACTION_UP等事件也一定不會分發(fā)給這個孩子
  @Override
  public boolean dispatchTouchEvent(MotionEvent ev) {
    // 調(diào)試用
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
    }

    // If the event targets the accessibility focused view and this is it, start
    // normal event dispatch. Maybe a descendant is what will handle the click.
    if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
        ev.setTargetAccessibilityFocus(false);
    }

    boolean handled = false;
    // 第1步:根據(jù)遮擋情況,判斷是否需要分發(fā)該觸摸事件
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;

        // 第2步:檢測是否需要清空目標和狀態(tài)
        //
        // 如果是ACTION_DOWN,說明是一個新的觸摸事件序列,則清空之前的觸摸事件處理target和狀態(tài)。
        // 這里的情況狀態(tài)包括:
        // (01) 清空mFirstTouchTarget鏈表乖寒,并設置mFirstTouchTarget為null。
        //      mFirstTouchTarget是"接受觸摸事件的View"所組成的單鏈表
        // (02) 清空mGroupFlags的FLAG_DISALLOW_INTERCEPT標記
        //      如果設置了FLAG_DISALLOW_INTERCEPT院溺,則不允許ViewGroup對觸摸事件進行攔截宵统。
        // (03) 清空mPrivateFlags的PFLAG_CANCEL_NEXT_UP_EVEN標記
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            cancelAndClearTouchTargets(ev);
            resetTouchState();
        }    

        // 第3步:檢查當前ViewGroup是否想要攔截觸摸事件
        //
        // 如果是ACTION_DOWN,也就是一個事件序列的開始,當然要重新判斷當前ViewGroup是否攔截了事件
        // 而如果不是ACTION_DOWN事件覆获,并且mFirstTouchTarget為null的話马澈,也就是說前面的ACTION_DOWN事件不是由子view去處理的,
        // 因而以后的ACTION_UP,ACTION_MOVE等事件也不會交由子view去處理,所以就相當于直接攔截了事件弄息,直接返回了true
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
            // 檢查禁止攔截標記:FLAG_DISALLOW_INTERCEPT
            // 如果調(diào)用了requestDisallowInterceptTouchEvent()標記的話痊班,則FLAG_DISALLOW_INTERCEPT會為true,不允許攔截
            // 例如,ViewPager在處理scroll事件的時候摹量,就會調(diào)用requestDisallowInterceptTouchEvent()涤伐,
            // 禁止它的父類對觸摸事件進行攔截
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                // 允許自身攔截的話馒胆,返回onInterceptTouchEvent()
                intercepted = onInterceptTouchEvent(ev);
                // restore action in case it was changed
                ev.setAction(action);
            } else {
                //不允許攔截的話,當然也就沒有攔截
                intercepted = false;
            }    
        } else {
            //up, move事件凝果,并且沒有子view可以處理down,當然也沒有子view處理up,move,直接相當于攔截掉了
            intercepted = true;
        }    

        // 第4步:檢查當前的觸摸事件是否被取消
        //
        // (01) 對于ACTION_DOWN而言祝迂,mPrivateFlags的PFLAG_CANCEL_NEXT_UP_EVENT位肯定是0;因此器净,canceled=false型雳。
        // (02) 當前的View或ViewGroup要被從父View中detach時,PFLAG_CANCEL_NEXT_UP_EVENT就會被設為true山害;
        //      此時纠俭,它就不再接受觸摸事情。
        final boolean canceled = resetCancelNextUpFlag(this)|| actionMasked == MotionEvent.ACTION_CANCEL;

        final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;

        // 第5步:將觸摸事件分發(fā)給"當前ViewGroup的子View和子ViewGroup"
        //
        // 如果觸摸"沒有被取消"浪慌,同時也"沒有被攔截"的話冤荆,則將觸摸事件分發(fā)給它的子View和子ViewGroup。
        // 整一個if(!canceled && !intercepted){ … }代碼塊所做的工作就是對ACTION_DOWN事件的特殊處理权纤。
        // 因為ACTION_DOWN事件是一個事件序列的開始钓简,所以我們要先找到能夠處理這個事件序列的一個子View,
        // 如果一個子View能夠消耗事件汹想,那么mFirstTouchTarget會指向子View涌庭,
        // 如果所有的子View都不能消耗事件,那么mFirstTouchTarget將為null
        if (!canceled && !intercepted) {
            //MotionEvent.ACTION_HOVER_MOVE一般指鼠標事件欧宜,鼠標在view上面
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                // 這是獲取觸摸事件的序號 以及 觸摸事件的id信息坐榆。
                // 對于down事件,一般是0
                final int actionIndex = ev.getActionIndex();
                final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                        : TouchTarget.ALL_POINTER_IDS;

                // 清空這個手指之前的TouchTarget鏈表冗茸。
                // 一個TouchTarget席镀,相當于一個可以被觸摸的對象;它中記錄了接受觸摸事件的View
                removePointersFromTouchTargets(idBitsToAssign);

                // 獲取該ViewGroup包含的View和ViewGroup的數(shù)目夏漱,
                // 然后遞歸遍歷ViewGroup的孩子豪诲,對觸摸事件進行分發(fā)。
                // 遞歸遍歷ViewGroup的孩子:是指對于當前ViewGroup的所有孩子挂绰,都會逐個遍歷屎篱,并分發(fā)觸摸事件;
                //   對于逐個遍歷到的每一個孩子葵蒂,若該孩子是ViewGroup類型的話交播,則會遞歸到調(diào)用該孩子的孩子,...
                final int childrenCount = mChildrenCount;
                if (newTouchTarget == null && childrenCount != 0) {
                    final float x = ev.getX(actionIndex);
                    final float y = ev.getY(actionIndex);
                    final View[] children = mChildren;

                    final boolean customOrder = isChildrenDrawingOrderEnabled();
                    //倒序遍歷践付,一般都是希望最上面的反饋
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        final int childIndex = customOrder ?
                                getChildDrawingOrder(childrenCount, i) : i;
                        final View child = children[childIndex];
                        // 如果child不能夠接受觸摸事件秦士,又或者觸摸坐標(x,y)在child的可視范圍之外
                        // 就繼續(xù)循環(huán)查找可以分發(fā)事件的子View
                        if (!canViewReceivePointerEvents(child)
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            continue;
                        }

                        // getTouchTarget()的作用是查找child是否存在于mFirstTouchTarget的單鏈表中。
                        // 是的話永高,則更新相應的值
                        newTouchTarget = getTouchTarget(child);
                        if (newTouchTarget != null) {
                            newTouchTarget.pointerIdBits |= idBitsToAssign;
                            break;
                        }

                        // 重置child的mPrivateFlags變量中的PFLAG_CANCEL_NEXT_UP_EVENT位隧土。
                        resetCancelNextUpFlag(child);

                        // 將事件嘗試分發(fā)給子View,如果找到了一個可以處理觸摸事件的子View提针,就直接跳出循環(huán),不用繼續(xù)遍歷
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            mLastTouchDownTime = ev.getDownTime();
                            mLastTouchDownIndex = childIndex;
                            mLastTouchDownX = ev.getX();
                            mLastTouchDownY = ev.getY();
                            // 如果child能夠接受該觸摸事件曹傀,即child消費或者攔截了該觸摸事件的話辐脖;
                            // 則調(diào)用addTouchTarget()將child添加到mFirstTouchTarget鏈表的表頭,并返回表頭對應的TouchTarget
                            // 同時還設置alreadyDispatchedToNewTouchTarget為true皆愉。
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            alreadyDispatchedToNewTouchTarget = true;
                            break;
                        }
                    }
                }

                // 如果newTouchTarget為null嗜价,并且mFirstTouchTarget不為null;
                // 則設置newTouchTarget為mFirstTouchTarget鏈表中第一個不為空的節(jié)點亥啦。
                if (newTouchTarget == null && mFirstTouchTarget != null) {
                    // Did not find a child to receive the event.
                    // Assign the pointer to the least recently added target.
                    newTouchTarget = mFirstTouchTarget;
                    while (newTouchTarget.next != null) {
                        newTouchTarget = newTouchTarget.next;
                    }
                    newTouchTarget.pointerIdBits |= idBitsToAssign;
                }
            }
        }

        // 第6步:進一步的對觸摸事件進行分發(fā),實際上是對攔截的了炭剪,取消了的练链,或其它非ACTION_DOWN事件的處理
        //

        if (mFirstTouchTarget == null) {
            // 如果mFirstTouchTarget為null翔脱,意味著還沒有任何View來接受該觸摸事件;
            // 此時媒鼓,將當前ViewGroup看作一個View届吁;
            // 將會調(diào)用"當前的ViewGroup的父類View的dispatchTouchEvent()"對觸摸事件進行分發(fā)處理。
            // 注意:這里的第3個參數(shù)是null绿鸣,也就是直接將事件交由這個ViewGroup處理
            // 即疚沐,會將觸摸事件交給當前ViewGroup的onTouch(), onTouchEvent()進行處理。
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
            // 如果mFirstTouchTarget不為null潮模,說明ACTION_DOWN事件已經(jīng)有子View處理了亮蛔,
            // 那么對于其它類型的事件,直接嘗試分發(fā)給mFirstTouchTarget鏈表中的就可以了
            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;
                // 這里實際上區(qū)分開了ACTION_DOWN與其它類型的事件
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    if (cancelChild) {
                        if (predecessor == null) {
                            mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                    }
                }
                predecessor = target;
                target = next;
            }
        }

        // 第7步:再次檢查取消標記擎厢,并進行相應的處理
        //
        // Update list of touch targets for pointer up or cancel, if needed.
        if (canceled
                || actionMasked == MotionEvent.ACTION_UP
                || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
            resetTouchState();
        } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
            final int actionIndex = ev.getActionIndex();
            final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
            removePointersFromTouchTargets(idBitsToRemove);
        }
    }

    // 調(diào)試用
    if (!handled && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
    }
    return handled;
}

onInterceptTouchEvent

  • onInterceptTouchEvent是否攔截觸摸事件究流,默認情況下是不攔截的

  • 常見的例子就是ViewPager,當發(fā)生滑動事件的時候,ViewPager攔截了事件动遭,用來左右滑動item

    public boolean onInterceptTouchEvent(MotionEvent ev) {
      return false;
    }
    
  • 當ViewGroup攔截了down事件芬探,或者沒有子view去處理down事件,后續(xù)的up,move事件就不會調(diào)用 onInterceptTouchEvent()方法了厘惦,所以該方法并不是每次事件都會調(diào)用的

圖解

viewgroup.png

總結

  • ACTION_DOWN事件為一個事件序列的開始偷仿,中間有若干個ACTION_MOVE,最后以ACTION_UP結束。
  • ViewGroup默認不攔截任何事件宵蕉,所以事件能正常分發(fā)到子View處(如果子View符合條件的話)酝静,如果沒有合適的子View或者子View不消耗ACTION_DOWN事件,那么接著事件會交由ViewGroup處理羡玛,并且同一事件序列之后的事件不會再分發(fā)給子View了形入。如果ViewGroup的onTouchEvent也返回false,即ViewGroup也不消耗事件的話缝左,那么最后事件會交由Activity處理亿遂。即:逐層分發(fā)事件下去浓若,如果都沒有處理事件的View,那么事件會逐層向上返回蛇数。
  • 如果某一個View攔截了事件挪钓,那么同一個事件序列的其他所有事件都會交由這個View處理,此時不再調(diào)用View(ViewGroup)的onIntercept()方法去詢問是否要攔截了
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末耳舅,一起剝皮案震驚了整個濱河市碌上,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌浦徊,老刑警劉巖馏予,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異盔性,居然都是意外死亡霞丧,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進店門冕香,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蛹尝,“玉大人,你說我怎么就攤上這事悉尾⊥荒牵” “怎么了?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵构眯,是天一觀的道長愕难。 經(jīng)常有香客問我,道長惫霸,這世上最難降的妖魔是什么猫缭? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮它褪,結果婚禮上饵骨,老公的妹妹穿的比我還像新娘。我一直安慰自己茫打,他們只是感情好居触,可當我...
    茶點故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著老赤,像睡著了一般轮洋。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上抬旺,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天弊予,我揣著相機與錄音,去河邊找鬼开财。 笑死汉柒,一個胖子當著我的面吹牛误褪,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播碾褂,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼兽间,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了正塌?” 一聲冷哼從身側響起嘀略,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎乓诽,沒想到半個月后帜羊,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡鸠天,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年讼育,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片粮宛。...
    茶點故事閱讀 38,577評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡窥淆,死狀恐怖卖宠,靈堂內(nèi)的尸體忽然破棺而出巍杈,到底是詐尸還是另有隱情,我是刑警寧澤扛伍,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布筷畦,位于F島的核電站,受9級特大地震影響刺洒,放射性物質(zhì)發(fā)生泄漏鳖宾。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一逆航、第九天 我趴在偏房一處隱蔽的房頂上張望鼎文。 院中可真熱鬧,春花似錦因俐、人聲如沸拇惋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽撑帖。三九已至,卻和暖如春澳眷,著一層夾襖步出監(jiān)牢的瞬間胡嘿,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工钳踊, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留衷敌,地道東北人勿侯。 一個月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像缴罗,于是被迫代替她去往敵國和親罐监。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,452評論 2 348

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