Android事件處理機(jī)制(2)-事件分發(fā)

首先針對上篇文章Android事件處理機(jī)制(1)-輸入事件做一個(gè)簡短的總結(jié)。

  • onTouch方法優(yōu)先于onClick執(zhí)行
  • 常見的MotionEvent的四種動作。
    MotionEvent.ACTION_DOWN:手指按下屏幕的瞬間澜薄。
    MotionEvent.ACTION_MOVE:手指在屏幕上移動
    MotionEvent.ACTION_UP:手指離開屏幕瞬間
    MotionEvent.ACTION_CANCEL:取消手勢
  • onClick、onLongClick、onScroll等方法,是由多個(gè)Touch事件組成立镶。
    用戶按下手指——>ACTION_DOWN——>用戶移動手指——>[ACTION_MOVE.. ACTION_MOVE]——>用戶抬起手指——>ACTION_UP。如果手指劃出View邊界還會出現(xiàn)ACTION_CANCEL类早。

Android中的事件處理一般包括事件分發(fā)->事件攔截->事件響應(yīng)三個(gè)步驟媚媒。其中上篇文章主要涉及的事件響應(yīng)的部分,本篇文章則主要講事件分發(fā)和事件攔截的流程涩僻。

事件分發(fā)

Android的視圖一般由Activity缭召、ViewGroup、和View3類組件構(gòu)成逆日,事件MotionEvent的傳遞順序是Activity->ViewGroup->View嵌巷。觸摸事件發(fā)生后,MotionEvent先傳到Activity室抽、再傳到ViewGroup搪哪、最終再傳到 View。Activity和ViewGroup通過dispatchTouchEvent(MotionEvent)方法分發(fā)觸摸事件坪圾,其內(nèi)部調(diào)用onTouchEvent(MotionEvent)方法處理點(diǎn)擊事件晓折。

Activity.dispatchTouchEvent解析

首先事件從Activity開始分發(fā),首先它讓W(xué)indow進(jìn)行事件分發(fā)兽泄,如果事件未被消費(fèi)漓概,則直接由Activity的onTouchEvent()消費(fèi)。
Window事件分發(fā)病梢,Window的實(shí)現(xiàn)類為PhoneWindow胃珍,PhoneWindow將事件直接傳遞給頂級DecorView進(jìn)行分發(fā)。

ViewGroup.dispatchTouchEvent解析

ViewGroup事件分發(fā)有兩種情況蜓陌。

  • 不攔截事件觅彰,事件將沿著View層次嵌套結(jié)構(gòu)繼續(xù)向下分發(fā),直到事件被消費(fèi)护奈。如果向下分發(fā)過程中事件未被消費(fèi)缔莲,則事件將沿著原來的傳遞路徑向上傳遞,直到事件被消費(fèi)霉旗。
  • 事件被攔截痴奏,將由攔截事件的ViewGroup來消費(fèi)事件,如果未消費(fèi)將事件向上傳遞厌秒,直到被消費(fèi)读拆。

這里需要注意的是,對于每次接收到ACTION_DOWN事件鸵闪,mFirstTouchTarget會置為空檐晕,F(xiàn)LAG_DISALLOW_INTERCEPT標(biāo)志位被重置,ViewGroup總是調(diào)用onInterceptTouchEvent()來判斷是否進(jìn)行攔截。因此如果ACTION_DOWN被攔截辟灰,那么后續(xù)其它事件都由它自身來處理个榕。如果ACTION_DOWN未被攔截,那么mFirstTouchTarget不為空芥喇,對于后續(xù)其它事件西采,子View通過調(diào)用ViewParent的requestDisallowInterceptTouchEvent()方法來控制對onInterceptTouchEvent()的調(diào)用。如果后續(xù)某個(gè)事件被攔截继控,子View會接收到ACTION_CANCEL事件械馆,該事件后續(xù)事件將由攔截該事件的ViewGroup來消費(fèi)。如果未攔截武通,則按照正常分發(fā)流程處理霹崎。

部分源碼如下:

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        ...//省略部分代碼
        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

           ...//省略部分代碼
            
            //關(guān)鍵點(diǎn)——ViewGroup進(jìn)行事件分發(fā)時(shí),需進(jìn)行條件判斷冶忱。判斷值1:disallowIntercept = 是否禁用事件攔截的功能(默認(rèn)是false)尾菇,子View可通過調(diào)用requestDisallowInterceptTouchEvent修改双仍。 判斷值2: !onInterceptTouchEvent(ev) = 對onInterceptTouchEvent()返回值取反
            // Check for interception.
            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;
            }

          ... //省略部分代碼
      
            // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                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;
                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;
                        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;
                }
            }

            // 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);
            }
        }

        if (!handled && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
        }
        return handled;
    }



    /**
     * Transforms a motion event into the coordinate space of a particular child view,
     * filters out irrelevant pointer ids, and overrides its action if necessary.
     * If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
     */
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

        ...//省略部分代碼
       
        //關(guān)鍵點(diǎn)——如果child為null崭倘,如點(diǎn)擊界面空白處,則調(diào)用父類即View.dispatchTouchEvent方法。如果child不為null眶拉,則調(diào)用child.dispatchTouchEvent。
        // Perform any necessary transformations and dispatch.
        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);
        }

        // Done.
        transformedEvent.recycle();
        return handled;
    }

View.dispatchTouchEvent解析

如果OnTouchListener不為空并且enabled為true憔儿,事件由OnTouchListener消費(fèi)忆植,否則由onTouchEvent消費(fèi)。如果OnTouchListener的onTouch方法返回false谒臼,事件將也由onTouchEvent消費(fèi)朝刊。
具體可參考[Android事件處理機(jī)制(1)-輸入事件]

事件攔截

ViewGroup可通過onInterceptTouchEvent(MotionEvent)來監(jiān)視分派給ViewGroup的子視圖的事件,且可以進(jìn)行攔截蜈缤。
默認(rèn)情況下拾氓,ViewGroup的onInterceptTouchEvent方法返回false,則點(diǎn)擊事件優(yōu)先被分派給子試圖處理底哥;如果子試圖不能處理或者onInterceptTouchEvent方法返回true咙鞍,則交給父類View的dispatchTouchEvent來處理。

總結(jié)來說趾徽,Android事件處理機(jī)制由“由外到內(nèi)”分發(fā)续滋;“由內(nèi)到外”處理兩種形式構(gòu)成。

  1. Android事件分發(fā)是先傳遞到ViewGroup孵奶,再由ViewGroup傳遞到View的疲酌。

  2. 在ViewGroup中可以通過onInterceptTouchEvent方法對事件傳遞進(jìn)行攔截,onInterceptTouchEvent方法返回true代表不允許事件繼續(xù)向子View傳遞,返回false代表不對事件進(jìn)行攔截朗恳,默認(rèn)返回false湿颅。

  3. 子View中如果將傳遞的事件消費(fèi)掉,ViewGroup中將無法接收到任何事件粥诫。

參考文檔:
Android事件分發(fā)機(jī)制完全解析肖爵,帶你從源碼的角度徹底理解(上)
Android事件分發(fā)機(jī)制完全解析,帶你從源碼的角度徹底理解(下)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末臀脏,一起剝皮案震驚了整個(gè)濱河市劝堪,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌揉稚,老刑警劉巖秒啦,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異搀玖,居然都是意外死亡余境,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進(jìn)店門灌诅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來芳来,“玉大人,你說我怎么就攤上這事猜拾〖瓷啵” “怎么了?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵挎袜,是天一觀的道長顽聂。 經(jīng)常有香客問我,道長盯仪,這世上最難降的妖魔是什么紊搪? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮全景,結(jié)果婚禮上耀石,老公的妹妹穿的比我還像新娘。我一直安慰自己爸黄,他們只是感情好滞伟,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著馆纳,像睡著了一般诗良。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上鲁驶,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天鉴裹,我揣著相機(jī)與錄音,去河邊找鬼。 笑死径荔,一個(gè)胖子當(dāng)著我的面吹牛督禽,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播总处,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼狈惫,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了鹦马?” 一聲冷哼從身側(cè)響起胧谈,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎荸频,沒想到半個(gè)月后菱肖,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡旭从,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年稳强,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片和悦。...
    茶點(diǎn)故事閱讀 38,163評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡退疫,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出鸽素,到底是詐尸還是另有隱情褒繁,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布付鹿,位于F島的核電站澜汤,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏舵匾。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一谁不、第九天 我趴在偏房一處隱蔽的房頂上張望坐梯。 院中可真熱鬧,春花似錦刹帕、人聲如沸吵血。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蹋辅。三九已至,卻和暖如春挫掏,著一層夾襖步出監(jiān)牢的瞬間侦另,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留褒傅,地道東北人弃锐。 一個(gè)月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像殿托,于是被迫代替她去往敵國和親霹菊。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評論 2 344