Android 事件分發(fā)

一瘾婿,前言

事件分發(fā)的文章也看過很多,自己也寫過筆記文章烤咧,但都沒有從總體上真正理解過偏陪,最終也是一知半解。這次就從總體流程上歸納下煮嫌,更方便記憶笛谦。

二,必須知道的方法

2.1 ViewGroup

//分發(fā)事件
public boolean dispatchTouchEvent(MotionEvent ev) 
//事件攔截昌阿,可以攔截本該子View處理的事件
public boolean onInterceptTouchEvent(MotionEvent ev) 
//將事件分發(fā)至子View或交給自己處理
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits)
  1. 與View中的dispatchTouchEvent不同饥脑,ViewGroup中的dispatchTouchEvent還要兼顧對子View的事件分發(fā)。而處理自身的事件是用super.dispatchTouchEvent(event)懦冰,也就是View的灶轰,在dispatchTransformedTouchEvent中被調(diào)用。

  2. onInterceptTouchEvent是View中沒有的方法儿奶,當(dāng)子View處理了DOWN事件框往,獲得了事件流的處理權(quán),父ViewGroup可以用onInterceptTouchEvent返回true來攔截事件交予自己處理闯捎。

  3. dispatchTransformedTouchEvent中分情況調(diào)用用子View的dispatchTouchEvent來分發(fā)事件椰弊;或者調(diào)用super.dispatchTouchEvent(event)來自己處理事件。

2.2 View

與ViewGroup不同瓤鼻,View的主要實現(xiàn)事件的響應(yīng)秉版,不用管分發(fā)

//分發(fā)事件
public boolean dispatchTouchEvent(MotionEvent ev) 
//優(yōu)先onTouchEvent的觸摸響應(yīng),用戶自定義添加監(jiān)聽
public interface OnTouchListener {
    boolean onTouch(View v, MotionEvent event);
}
//系統(tǒng)響應(yīng)茬祷,長按和點擊事件在里面被調(diào)用
public boolean onTouchEvent(MotionEvent event)
  1. 先調(diào)用mOnTouchListener.onTouch(this, event)來處理事件清焕,如果onTouch沒有處理,則再交給onTouchEvent處理。
  2. OnTouchListener是對事件分發(fā)的回調(diào)秸妥,在onTouchEvent前被調(diào)用滚停。
  3. onTouchEvent主要用于響應(yīng)長按事件和點擊事件。

三粥惧,ViewGroup的dispatchTouchEvent

基于常用的幾個方法來分析其功能的實現(xiàn)原理键畴,也包括上面方法介紹時說的原理。==暫時只考慮單點觸摸事件==突雪。

  1. 父ViewGroup只要攔截了ACTION_DOWN,后續(xù)事件下層級的View無法搶奪了起惕。
  2. 在子容器中調(diào)用getParent().requestDisallowInterceptTouchEvent(true)來搶奪上一層容器的事件處理權(quán)。前提ACTION_DOWN沒有被攔截咏删。
  3. 在父容器中使onInterceptTouchEvent返回true來攔截本該交給它底下層級View的事件處理權(quán)惹想。但父容器的requestDisallowInterceptTouchEvent(true)被調(diào)用,onInterceptTouchEvent無效督函。
  4. onInterceptTouchEvent只有在ACTION_DOWN事件或者事件被自己下面層級的View處理時才會被調(diào)用嘀粱,也就是說ACTION_DOWN被自己處理了,這個ViewGroup的onInterceptTouchEvent在同一個事件序列中不會被調(diào)用辰狡。
  5. 一旦有View對down事件的觸摸響應(yīng)草穆,也就是使其dispatchTouchEvent返回true,一般事件后續(xù)的move搓译,up等都交予它處理了,除非搶奪處理權(quán)锋喜。
  6. 一旦子View搶奪權(quán)限成功些己,后續(xù)的一序列事件都交予子View處理了;父View搶奪成功嘿般,本改處理事件的View會收到ACTION_CANCE事件段标,被攔截的這次事件父View不會處理,但后續(xù)事件都會交給父View處理了炉奴,且因為事件被父View處理逼庞,它的onInterceptTouchEvent不會再被調(diào)用。

暫時根據(jù)ViewGroup的dispatchTouchEvent只想到這些了瞻赶。其功能主要處理事件分發(fā)赛糟,所以相關(guān)點都是這些方法在事件分發(fā)中的作用。

又要貼代碼了.....只貼重點代碼砸逊,上面說的點都在代碼中可以看出璧南。

3.1

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ......
    // 檢查是否自身是否攔截事件
    final boolean intercepted;
    //只有ACTION_DOWN或事件交由底下View處理了才走進(jìn)去
    if (actionMasked == MotionEvent.ACTION_DOWN
            || mFirstTouchTarget != null) {
        //FLAG_DISALLOW_INTERCEPT就是通過requestDisallowInterceptTouchEvent方法來設(shè)置的
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        //如果requestDisallowInterceptTouchEvent(true),就走else了
        if (!disallowIntercept) {
            //調(diào)用onInterceptTouchEvent詢問是否攔截
            intercepted = onInterceptTouchEvent(ev);
            ev.setAction(action); // restore action in case it was changed
        } else {
            intercepted = false;
        }
    } else {
        // 沒有下層級的View處理事件师逸,且不是Down事件司倚,就直接攔截,不詢問了。
        // 也就是說down被本ViewGroup攔截處理动知,后續(xù)不會詢問而直接給本ViewGroup處理.
        intercepted = true;
    }
    ......
}

上面代碼默認(rèn)為intercepted = true就是攔截了事件皿伺。解釋了第2,3盒粮,4點鸵鸥,注釋說得很清楚了。

當(dāng)然代碼不能割裂來看拆讯,下面解釋為什么intercepted = true就是攔截了事件和剩下幾點脂男。

3.2

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ......
    //如果事件沒有取消,且沒有被攔截
    if (!canceled && !intercepted) {
        //下面跳過多點觸控的代碼了
        ......
        //ACTION_DOWN事件才會進(jìn)入
        if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
            for (int i = childrenCount - 1; i >= 0; i--) {
                ......
                //判斷子View是否在點擊范圍內(nèi)
                if (!canViewReceivePointerEvents(child)
                        || !isTransformedTouchPointInView(x, y, child, null)) {
                    ev.setTargetAccessibilityFocus(false);
                    continue;
                }
                ......
                //dispatchTransformedTouchEvent分發(fā)給子View种呐,如果有子View處理了宰翅,就進(jìn)入if
                if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                    ...... 
                    //addTouchTarget中會存儲處理事件的子View,賦予mFirstTouchTarget
                    newTouchTarget = addTouchTarget(child, idBitsToAssign);
                    alreadyDispatchedToNewTouchTarget = true;
                    break;
                }
            }
        }
    }
}

上面這段主要實現(xiàn)Down事件如何分發(fā)給子View爽室。

3.3

    // Dispatch to touch targets.
    if (mFirstTouchTarget == null) {
        // 1.沒有子View處理事件汁讼,就交給自己處理
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
                TouchTarget.ALL_POINTER_IDS);
    } else {
        // 2.如果前提事件已經(jīng)有View處理了,走下面步驟
        TouchTarget predecessor = null;
        TouchTarget target = mFirstTouchTarget;
        //多點觸摸多條事件時阔墩,循環(huán)分發(fā)
        while (target != null) {
            final TouchTarget next = target.next;
            //3.Down事件被子View處理了嘿架,這部就直接結(jié)束了這次分發(fā)
            if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                handled = true;
            } else {
                final boolean cancelChild = resetCancelNextUpFlag(target.child)
                        || intercepted;
                //4.intercepted就決定了是否cancle事件,后續(xù)事件再交予自己處理啸箫,子View會收到ACTION_CANCEL耸彪。
                //否則正常分發(fā)
                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;
        }
    }
    
    ......
    
    return handled;

上面解釋了剩下幾點。

  1. 注釋第一點忘苛,mFirstTouchTarget為空蝉娜,就說明Down事件中下層View沒有處理這個事件,在這里交給自己處理扎唾。
  2. 注釋第二點召川,mFirstTouchTarget不為空,則走這里胸遇,進(jìn)行View的分發(fā)荧呐,因為mFirstTouchTarget保存了處理事件的子View,直接交予對應(yīng)子View處理纸镊。
  3. 注釋第三點倍阐,如果是Down事件,且找到處理事件的子View薄腻,handled = true;結(jié)束這個事件分發(fā)收捣。
  4. 注釋第四點,這個ViewGroup決定攔截本該交予子View處理的事件庵楷,這里cancelChild為true罢艾,在dispatchTransformedTouchEvent交予子View的是cancel事件而不是原本事件楣颠。mFirstTouchTarget中移除相應(yīng)的TouchTarget,這樣后續(xù)事件的mFirstTouchTarget就為空了咐蚯,交予這個ViewGroup處理童漩。

四,ViewGroup的dispatchTransformedTouchEvent

從上面的分析可以知道春锋,這個方法在dispatchTouchEvent有三次調(diào)用矫膨,也可以說有三種作用。

  1. 在3.2中期奔,在找到被觸摸的子View后侧馅,交予其分發(fā)給子View處理,調(diào)用child.dispatchTouchEvent(transformedEvent)分發(fā)事件呐萌。
  2. 在3.3的注釋第一點中馁痴,傳遞的child為空,會調(diào)用super.dispatchTouchEvent(transformedEvent)交予ViewGroup自身處理肺孤。
  3. 在3.3的第四點中罗晕,如果cancel參數(shù)為false,就和第一點一樣的情況赠堵;如果cancel為true小渊,會event.setAction(MotionEvent.ACTION_CANCEL),將事件統(tǒng)一變?yōu)閏ancel事件茫叭,再交予子View酬屉。
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
    final int oldAction = event.getAction();
    //如果ViewGroup攔截這個事件,或cancel事件
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        //設(shè)置傳遞的事件類型為cancel事件
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            //給自己的cancel事件
            handled = super.dispatchTouchEvent(event);
        } else {
            //給子View的cancel事件
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }
    
    //非傳遞cancel事件揍愁,正常分發(fā)
    if (child == null) {
        //叫給ViewGroup自己處理事件
        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());
        }

        //交給子View處理事件
        handled = child.dispatchTouchEvent(transformedEvent);
    }
    
    return handled;
}

五梆惯,View的dispatchTouchEvent

與ViewGroup作用為分發(fā)事件不同,View里它的作用為處理事件吗垮。

public boolean dispatchTouchEvent(MotionEvent event) {
    ......
    if (onFilterTouchEventForSecurity(event)) {
        ......
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        //調(diào)用onTouch
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }
        //如果onTouch返回true,則不會再調(diào)用onTouchEvent
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }
}

onTouch先于onTouchEvent調(diào)用凹髓,且onTouch處理事件烁登,就不會交予onTouchEvent處理了,也就是點擊事件和長按事件不會響應(yīng)了蔚舀。

六饵沧,View的onTouchEvent

  1. onTouchEvent中,主要調(diào)用了OnClick 和 OnLongClick赌躺。先調(diào)用
    OnLongClick再調(diào)用OnClick狼牺,如果OnLongClick返回true消耗了事件,onClick不會被調(diào)用礼患。
  2. 只要View可點擊的是钥,就算沒有注冊點擊事件和長按事件掠归,也會消費這個事件。
public boolean onTouchEvent(MotionEvent event) {
    
    //DISABLED狀態(tài)
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);
        }
        // 如果是DISABLED狀態(tài)悄泥,但同時還是CLICKABLE可點擊虏冻,雖然不會響應(yīng)點擊監(jiān)聽,但還是會消耗事件
        return (((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
    }
    
    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
            (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                    //mHasPerformedLongPress表示長按是否消費了事件 
                    //mIgnoreNextUpEvent是鼠標(biāo)移動事件相關(guān)參數(shù)弹囚,可以不管
                     if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                        // 移除長按
                        removeLongPressCallback();

                        // 要有焦點厨相,也就是在點擊狀態(tài)下,才響應(yīng)點擊事件
                        if (!focusTaken) {
                            // 使用Handler來實現(xiàn)回調(diào)鸥鹉,使視覺效果優(yōu)先于點擊響應(yīng)蛮穿。
                            if (mPerformClick == null) {
                                //創(chuàng)建的Runnable對象中,調(diào)用了performClick()來響應(yīng)點擊事件
                                mPerformClick = new PerformClick();
                            }
                            //發(fā)送消息執(zhí)行點擊監(jiān)聽
                            if (!post(mPerformClick)) {
                                performClick();
                            }
                        }
                    }
                    
                }
                
                break;
            case MotionEvent.ACTION_DOWN:
                // 所在容器是否可以滾動
                boolean isInScrollingContainer = isInScrollingContainer();

                //如果可以滾動毁渗,延遲ViewConfiguration.getTapTimeout()來進(jìn)行長按判斷践磅,防止為滾動觸摸事件。
                //但長按時間判斷會減去ViewConfiguration.getTapTimeout()祝蝠,所以最終長按監(jiān)聽被調(diào)用的時間不變
                if (isInScrollingContainer) {
                    mPrivateFlags |= PFLAG_PREPRESSED;
                    if (mPendingCheckForTap == null) {
                        //CheckForTap里會調(diào)用checkForLongClick方法進(jìn)行長按判斷
                        mPendingCheckForTap = new CheckForTap();
                    }
                    mPendingCheckForTap.x = event.getX();
                    mPendingCheckForTap.y = event.getY();
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                } else {
                    setPressed(true, x, y);
                    // 不是滾動音诈,進(jìn)行長按判斷,使用Handler進(jìn)行長按時間判斷
                    checkForLongClick(0, x, y);
                }
                break;
            case MotionEvent.ACTION_CANCEL:
                setPressed(false);
                //移除長按消息
                removeTapCallback();
                removeLongPressCallback();
                mInContextButtonPress = false;
                mHasPerformedLongPress = false;
                mIgnoreNextUpEvent = false;
                break;
            case MotionEvent.ACTION_MOVE:
                //按鈕之外的移動要容忍绎狭。
                if (!pointInView(x, y, mTouchSlop)) {
                    // 從Handler消息隊列中移除Down里面添加的mPendingCheckForTap,也就是移除滾動容器里的長按判斷
                    removeTapCallback();
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                        // 從Handler消息隊列中移除長按判斷Runnable细溅,
                        removeLongPressCallback();

                        setPressed(false);
                    }
                }
                break;
        }
        
        //只要設(shè)置了可點擊或可長按狀態(tài),都會消耗事件儡嘶,不管有沒有設(shè)置監(jiān)聽
        return true;
    }
}
  1. 可以看出喇聊,長按監(jiān)聽調(diào)用是通過延遲消息實現(xiàn)的;如果進(jìn)行了滑動蹦狂,且是按壓狀態(tài)(滿足down事件中長按消息發(fā)送)誓篱,移除長按消息。所以在down事件后發(fā)送長按延遲消息凯楔,如果沒有在move窜骄,cancel,up中移除消息摆屯,延遲時間過后就會執(zhí)行長按回調(diào)了邻遏。
  2. ACTION_UP中的PFLAG_PREPRESSED參數(shù),是在ACTION_DOWN的CheckForTap中被延遲賦值給mPrivateFlags的虐骑,在ACTION_CANCEL和ACTION_MOVE中會移除這個值准验。所以沒有move而執(zhí)行到ACTION_UP時,因為賦值了PFLAG_PREPRESSED一定會進(jìn)入廷没。
  3. ACTION_UP中的(mPrivateFlags & PFLAG_PRESSED) != 0判斷糊饱,非處于滾動容器,會在ACTION_DOWN的setPressed(true, x, y)設(shè)置PFLAG_PRESSED值颠黎,在move中setPressed(false)移除另锋。
  4. 上面2滞项,3兩點決定ACTION_DOWN的if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed)兩個條件是否滿足。滿足的要求都是只有按下砰蠢,而沒有執(zhí)行滑動就到抬起蓖扑。
  5. 長按和點擊回調(diào)都是通過發(fā)送消息來調(diào)用,在Runnable里調(diào)用對應(yīng)的方法台舱。長按為checkForLongClick(int delayOffset, float x, float y)律杠;點擊為performClick()。方法里面調(diào)用了回調(diào)監(jiān)聽竞惋。

在ACTION_MOVE中柜去,我們一般通過判斷滑動距離有沒有超過mTouchSlop來判斷是否屬于滑動事件。

參考

Android事件傳遞之子View和父View的那點事

安卓自定義View進(jìn)階-事件分發(fā)機(jī)制詳解

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末拆宛,一起剝皮案震驚了整個濱河市嗓奢,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌浑厚,老刑警劉巖股耽,帶你破解...
    沈念sama閱讀 219,589評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異钳幅,居然都是意外死亡物蝙,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,615評論 3 396
  • 文/潘曉璐 我一進(jìn)店門敢艰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來诬乞,“玉大人,你說我怎么就攤上這事钠导≌鸺担” “怎么了?”我有些...
    開封第一講書人閱讀 165,933評論 0 356
  • 文/不壞的土叔 我叫張陵牡属,是天一觀的道長票堵。 經(jīng)常有香客問我,道長逮栅,這世上最難降的妖魔是什么换衬? 我笑而不...
    開封第一講書人閱讀 58,976評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮证芭,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘担映。我一直安慰自己废士,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,999評論 6 393
  • 文/花漫 我一把揭開白布蝇完。 她就那樣靜靜地躺著官硝,像睡著了一般矗蕊。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上氢架,一...
    開封第一講書人閱讀 51,775評論 1 307
  • 那天傻咖,我揣著相機(jī)與錄音,去河邊找鬼岖研。 笑死卿操,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的孙援。 我是一名探鬼主播害淤,決...
    沈念sama閱讀 40,474評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼拓售!你這毒婦竟也來了窥摄?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,359評論 0 276
  • 序言:老撾萬榮一對情侶失蹤础淤,失蹤者是張志新(化名)和其女友劉穎崭放,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鸽凶,經(jīng)...
    沈念sama閱讀 45,854評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡币砂,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,007評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了吱瘩。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片道伟。...
    茶點故事閱讀 40,146評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖使碾,靈堂內(nèi)的尸體忽然破棺而出蜜徽,到底是詐尸還是另有隱情,我是刑警寧澤票摇,帶...
    沈念sama閱讀 35,826評論 5 346
  • 正文 年R本政府宣布拘鞋,位于F島的核電站,受9級特大地震影響矢门,放射性物質(zhì)發(fā)生泄漏盆色。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,484評論 3 331
  • 文/蒙蒙 一祟剔、第九天 我趴在偏房一處隱蔽的房頂上張望隔躲。 院中可真熱鬧,春花似錦物延、人聲如沸宣旱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,029評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽浑吟。三九已至笙纤,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間组力,已是汗流浹背省容。 一陣腳步聲響...
    開封第一講書人閱讀 33,153評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留燎字,地道東北人腥椒。 一個月前我還...
    沈念sama閱讀 48,420評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像轩触,于是被迫代替她去往敵國和親寞酿。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,107評論 2 356