Android View體系(五)從源碼解析View的事件分發(fā)機制

相關(guān)文章
Android View體系(一)視圖坐標系
Android View體系(二)實現(xiàn)View滑動的六種方法
Android View體系(三)屬性動畫
Android View體系(四)從源碼解析Scroller

前言

三年前寫過事件分發(fā)機制的文章但是寫的不是很好顿肺,所以重新再寫一篇,關(guān)于事件分發(fā)機制的文章已經(jīng)有很多,但是希望我這篇是最簡潔、最易懂的一篇。

1.處理點擊事件的方法

View的層級

我們知道View的結(jié)構(gòu)是樹形的結(jié)構(gòu)灾部,View可以放在ViewGroup中,這個ViewGroup也可以放到另一個ViewGroup中惯退,這樣層層的嵌套就組成了View的層級赌髓。

什么是點擊事件分發(fā)

當我們點擊屏幕,就產(chǎn)生了觸摸事件催跪,這個事件被封裝成了一個類:MotionEvent锁蠕。而當這個MotionEvent產(chǎn)生后,那么系統(tǒng)就會將這個MotionEvent傳遞給View的層級懊蒸,MotionEvent在View的層級傳遞的過程就是點擊事件分發(fā)荣倾。

點擊事件分發(fā)的重要方法

點擊事件有三個重要的方法它們分別是:

  • dispatchTouchEvent(MotionEvent ev):用來進行事件的分發(fā)
  • onInterceptTouchEvent(MotionEvent ev):用來進行事件的攔截,在dispatchTouchEvent()中調(diào)用骑丸,需要注意的是View沒有提供該方法
  • onTouchEvent(MotionEvent ev):用來處理點擊事件舌仍,在dispatchTouchEvent()方法中進行調(diào)用

為了了解這三個方法的關(guān)系鳖孤,我們先來看看ViewGroup的dispatchTouchEvent()方法的部分源碼:

 @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
       ...省略
            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;
            }

           ...省略
        return handled;
    }

很明顯在dispatchTouchEvent()方法中調(diào)用了onInterceptTouchEvent()方法來判斷是否攔截事件,來看看onInterceptTouchEvent()方法:

 public boolean onInterceptTouchEvent(MotionEvent ev) {
        return false;
    }

onInterceptTouchEvent()方法默認返回false抡笼,不進行攔截,接著來看看dispatchTouchEvent()方法剩余的部分源碼:

 public boolean dispatchTouchEvent(MotionEvent ev) {
 ...省略
              final View[] children = mChildren;
              for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = customOrder
                                    ? getChildDrawingOrder(childrenCount, i) : i;
                            final View child = (preorderedList == null)
                                    ? children[childIndex] : preorderedList.get(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.
                            if (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }

                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }

                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                // Child is already receiving touch within its bounds.
                                // Give it the new pointer in addition to the ones it is handling.
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            resetCancelNextUpFlag(child);
                            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);
                                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);
                        }

 ...省略

}

我們看到了for循環(huán)黄鳍,首先遍歷ViewGroup的子元素推姻,判斷子元素是否能夠接收到點擊事件,如果子元素能夠接收到則交由子元素來處理框沟。接下來看看37行的dispatchTransformedTouchEvent()方法中實現(xiàn)了什么:

 private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

        // Canceling motions is a special case.  We don't need to perform any transformations
        // or filtering.  The important part is the action, not the contents.
        final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }
  ...省略      
 }       

如果有子View則調(diào)用子View的dispatchTouchEvent(event)方法藏古。如果ViewGroup沒有子View則調(diào)用super.dispatchTouchEvent(event),ViewGroup是繼承View的忍燥,我們再來看看View的dispatchTouchEvent(event):

 public boolean dispatchTouchEvent(MotionEvent event) {
       ...省略
        boolean result = false;
        if (onFilterTouchEventForSecurity(event)) {
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
     ...省略
        return result;
    }

我們看到如果OnTouchListener不為null并且onTouch()方法返回true拧晕,則表示事件被消費,就不會執(zhí)行onTouchEvent(event)梅垄,否則就會執(zhí)行onTouchEvent(event)厂捞。再來看看onTouchEvent()方法的部分源碼:

 public boolean onTouchEvent(MotionEvent event) {
      ...省略
        final int action = event.getAction();
        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) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                       
                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            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)) {
                                    performClick();
                                }
                            }
                        }
       ...省略    
        }
        return true;
       }          
       return false;
    }

上面可以看到只要View的CLICKABLE和LONG_CLICKABLE一個為true,那么onTouchEvent就會返回true消耗這個事件匙姜。CLICKABLE和LONG_CLICKABLE代表View可以被點擊和長按點擊逃默,可以通過View的setClickable和setLongClickable方法來設(shè)置旨剥,也可以通過View的setOnClickListenter和setOnLongClickListener來設(shè)置,他們會自動將View的設(shè)置為CLICKABLE和LONG_CLICKABLE臭墨。
接著在ACTION_UP事件會調(diào)用performClick()方法:

    public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        return result;
    }

如果View設(shè)置了點擊事件OnClickListener,那么它的onClick()方法就會被執(zhí)行膘盖。

2.點擊事件分發(fā)的傳遞規(guī)則

看到這里我們就可以知道點擊事件分發(fā)的這三個重要方法的關(guān)系胧弛,用偽代碼來簡單表示就是:

 public boolean dispatchTouchEvent(MotionEvent ev) {
 boolean result=false;
 if(onInterceptTouchEvent(ev)){
       result=super.onTouchEvent(ev);

  }else{
       result=child.dispatchTouchEvent(ev);
 }
 return result;

點擊事件由上而下的傳遞規(guī)則

當點擊事件產(chǎn)生后會由Activity來處理在傳遞給Window再傳遞給頂層的ViewGroup,一般在事件傳遞中只考慮ViewGroup的onInterceptTouchEvent()方法侠畔,因為一般情況我們不會去重寫dispatchTouchEvent()方法结缚。
對于根ViewGroup,點擊事件首先傳遞給它的dispatchTouchEvent()方法践图,如果該ViewGroup的onInterceptTouchEvent()方法返回true掺冠,則表示它要攔截這個事件,這個事件就會交給它的onTouchEvent()方法處理码党,如果onInterceptTouchEvent()方法返回false德崭,則表示它不攔截這個事件,則交給它的子元素的dispatchTouchEvent()來處理揖盘,如此的反復(fù)下去眉厨。如果傳遞給最底層的View,View是沒有子View的兽狭,就會調(diào)用View的dispatchTouchEvent()方法憾股,一般情況下最終會調(diào)用View的onTouchEvent()方法鹿蜀。

舉個現(xiàn)實的例子,就是我們的應(yīng)用產(chǎn)生了重大的bug服球,這個bug首先會匯報給技術(shù)總監(jiān)那:

技術(shù)總監(jiān)(頂層ViewGroup)→技術(shù)經(jīng)理(中層ViewGroup)→工程師(底層View)
技術(shù)總監(jiān)不攔截茴恰,把bug分給了技術(shù)經(jīng)理,技術(shù)經(jīng)理不攔截把bug分給了工程師斩熊,工程師沒有下屬只有自己處理了往枣。
事件由上而下傳遞返回值規(guī)則為:true,攔截粉渠,不繼續(xù)向下傳遞分冈;false,不攔截霸株,繼續(xù)向下傳遞雕沉。

點擊事件由下而上的傳遞規(guī)則

點擊事件傳給最底層的View,如果他的onTouchEvent()方法返回true去件,則事件由最底層的View消耗并處理了坡椒,如果返回false則表示該View不做處理,則傳遞給父View的onTouchEvent()處理箫攀,如果父View的onTouchEvent()仍舊返回返回false肠牲,則繼續(xù)傳遞給改父View的父View處理,如此的反復(fù)下去靴跛。

再返回我們現(xiàn)實的例子缀雳,工程師發(fā)現(xiàn)這個bug太難搞不定(onTouchEvent()返回false),他只能交給上級技術(shù)經(jīng)理處理梢睛,如果技術(shù)經(jīng)理也搞不定(onTouchEvent()返回false)肥印,那就把bug傳給技術(shù)總監(jiān),技術(shù)總監(jiān)一看bug很簡單就解決了(onTouchEvent()返回true)绝葡。

事件由下而上傳遞返回值規(guī)則為:true深碱,處理了,不繼續(xù)向上傳遞藏畅;false敷硅,不處理,繼續(xù)向上傳遞愉阎。

點擊事件傳遞時的其他問題

  • 上面源碼我們看到:如果我們設(shè)置了OnTouchListener并且onTouch()方法返回true绞蹦,則onTouchEvent()方法不會被調(diào)用,否則則會調(diào)用onTouchEvent()方法榜旦,可見OnTouchListener的優(yōu)先級要比onTouchEvent()要高幽七。在OnTouchEvent()方法中,如果當前設(shè)置了OnClickListener則會執(zhí)行它的onClick()方法溅呢。
  • View的OnTouchEvent()方法默認都會返回true澡屡,除非它是不可點擊的也就是CLICKABLE和LONG_CLICKABLE都為false猿挚。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市驶鹉,隨后出現(xiàn)的幾起案子绩蜻,更是在濱河造成了極大的恐慌,老刑警劉巖室埋,帶你破解...
    沈念sama閱讀 217,084評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件辜羊,死亡現(xiàn)場離奇詭異,居然都是意外死亡词顾,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,623評論 3 392
  • 文/潘曉璐 我一進店門碱妆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來肉盹,“玉大人,你說我怎么就攤上這事疹尾∩先蹋” “怎么了?”我有些...
    開封第一講書人閱讀 163,450評論 0 353
  • 文/不壞的土叔 我叫張陵纳本,是天一觀的道長窍蓝。 經(jīng)常有香客問我,道長繁成,這世上最難降的妖魔是什么吓笙? 我笑而不...
    開封第一講書人閱讀 58,322評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮巾腕,結(jié)果婚禮上面睛,老公的妹妹穿的比我還像新娘。我一直安慰自己尊搬,他們只是感情好叁鉴,可當我...
    茶點故事閱讀 67,370評論 6 390
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著佛寿,像睡著了一般幌墓。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上冀泻,一...
    開封第一講書人閱讀 51,274評論 1 300
  • 那天常侣,我揣著相機與錄音,去河邊找鬼腔长。 笑死袭祟,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的捞附。 我是一名探鬼主播巾乳,決...
    沈念sama閱讀 40,126評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼您没,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了胆绊?” 一聲冷哼從身側(cè)響起氨鹏,我...
    開封第一講書人閱讀 38,980評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎压状,沒想到半個月后仆抵,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,414評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡种冬,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,599評論 3 334
  • 正文 我和宋清朗相戀三年镣丑,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片娱两。...
    茶點故事閱讀 39,773評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡莺匠,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出十兢,到底是詐尸還是另有隱情趣竣,我是刑警寧澤,帶...
    沈念sama閱讀 35,470評論 5 344
  • 正文 年R本政府宣布旱物,位于F島的核電站遥缕,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏宵呛。R本人自食惡果不足惜单匣,卻給世界環(huán)境...
    茶點故事閱讀 41,080評論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望宝穗。 院中可真熱鬧封孙,春花似錦、人聲如沸讽营。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,713評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽橱鹏。三九已至膜蠢,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間莉兰,已是汗流浹背挑围。 一陣腳步聲響...
    開封第一講書人閱讀 32,852評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留糖荒,地道東北人杉辙。 一個月前我還...
    沈念sama閱讀 47,865評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像捶朵,于是被迫代替她去往敵國和親蜘矢。 傳聞我的和親對象是個殘疾皇子狂男,可洞房花燭夜當晚...
    茶點故事閱讀 44,689評論 2 354

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