個(gè)人筆記---view的事件分發(fā)機(jī)制

前言

在如今這個(gè)拼顏值的社會(huì),app不光要運(yùn)行流暢,更要用優(yōu)美的界面來(lái)吸引用戶,有時(shí)候總感覺(jué)官方提供的控件遠(yuǎn)遠(yuǎn)不能滿足我們的需求,我們需要自己動(dòng)手去自定義一些view。

說(shuō)到自定義view,相信很多人都比較頭疼了,當(dāng)然我也不擅長(zhǎng)這個(gè)。最讓我頭疼的應(yīng)該就算是view的事件分發(fā)了(說(shuō)了這么多廢話,終于進(jìn)入正題了),廢話不多說(shuō),本文會(huì)對(duì)view的事件分發(fā)機(jī)制做一個(gè)詳細(xì)的說(shuō)明。

當(dāng)我們點(diǎn)擊了一個(gè)按鈕,系統(tǒng)內(nèi)部到底發(fā)生了什么

一個(gè)完整的點(diǎn)擊事件是有多個(gè)MotionEvent事件構(gòu)成的,當(dāng)手指按下屏幕,會(huì)伴隨著一個(gè)ACTION_DOWN事件;手指在屏幕上滑動(dòng),會(huì)伴隨一個(gè)或多個(gè)ACTION_MOVE事件;手指抬起則會(huì)產(chǎn)生一個(gè)ACTION_UP事件,從ACTION_DOWN到ACTION_UP成為一個(gè)事件序列。

Activity作為四大組件之一,我們與app的交互都要依賴與它,點(diǎn)擊事件自然是最先傳遞到Activity的dispatchTouchEvent中(talk is cheap,上代碼)

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

Activity中具體的分發(fā)工作交給了Window,如果返回true,整個(gè)事件循環(huán)就結(jié)束了,返回false則代表沒(méi)人處理,所有的onTouchEvent都返回false,那么activity的onTouchEvent會(huì)被調(diào)用。

那么看一下Window中是如何分發(fā)事件的

public abstract boolean superDispatchTouchEvent(MotionEvent event);

Window中的分發(fā)事件的方法是一個(gè)抽象方法,我們就要找到哪里實(shí)現(xiàn)了這個(gè)方法。而這個(gè)方法的實(shí)現(xiàn)是在PhoneWindow中,PhoneWindow是Window的唯一實(shí)現(xiàn),看PhoneWindow中的代碼

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}

代碼比較清晰,PhoneWindow將事件又傳遞給了DectorView,這里先看一下Activity,Window和Decorview之間的關(guān)系

每個(gè)Activity都對(duì)應(yīng)一個(gè)Window,這個(gè)Window窗口的唯一實(shí)例就是PhoneWindow,PhoneWindow對(duì)應(yīng)的布局則是DecorView,它繼承自FrameLayout,是一個(gè)ViewGroup,DecorView里面又分為兩部分,actionBar和contentView,而contentView就是我們?cè)贏ctivity中setContentView設(shè)置的布局。

通過(guò)這個(gè)圖,我們也了解了他們之間的關(guān)系,下面繼續(xù)說(shuō)事件的分發(fā)傳遞。

此時(shí)事件已經(jīng)傳遞給DecorView,它其實(shí)就是一個(gè)ViewGroup,

public class DecorView extends FrameLayout implements ... {
    public boolean superDispatchTouchEvent(MotionEvent event) {
            return super.dispatchTouchEvent(event);
    }
}

從這里開(kāi)始,事件就傳遞到了頂級(jí)View,而我們最關(guān)注的點(diǎn)就是從頂級(jí)View開(kāi)始到各個(gè)子view之間的事件是如何分發(fā)的。一般頂級(jí)View都是ViewGroup,會(huì)調(diào)用ViewGroup的dispatchTouchEvent方法,這里只看一下關(guān)鍵代碼

if (actionMasked == MotionEvent.ACTION_DOWN) {
       cancelAndClearTouchTargets(ev);
       resetTouchState();
}
final boolean intercepted;
//分兩種情況攔截事件,事件類型為ACTION_DOWN或者mFirstTouchTarget != null
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 {
       intercepted = true;
}

當(dāng)事件為ACTION_DOWN時(shí),首先會(huì)重設(shè)一些狀態(tài),resetTouchState()方法會(huì)將mFirstTouchTarget置為空,F(xiàn)LAG_DISALLOW_INTERCEPT重置,
ViewGroup是否攔截事件分為兩種情況,當(dāng)事件為ACTION_DOWN或mFirstTouchTarget != null時(shí),這個(gè)mFirstTouchTarget是什么?mFirstTouchTarget是判斷是否有子view消費(fèi)了當(dāng)前事件,若消費(fèi)則會(huì)通過(guò)newTouchTarget = addTouchTarget(child, idBitsToAssign)對(duì)mFirstTouchTarget賦值,具體會(huì)在下面說(shuō)明

接下來(lái)看一下FLAG_DISALLOW_INTERCEPT這個(gè)參數(shù),FLAG_DISALLOW_INTERCEPT可以通過(guò)requestDisallowInterceptTouchEvent方法設(shè)置,一般用于子view,如果子view設(shè)置了FLAG_DISALLOW_INTERCEPT,ViewGroup將無(wú)法攔截除ACTION_DOWN以外的事件。

為什么是除了ACTION_DOWN以外的事件?上面說(shuō)了,ACTION_DOWN會(huì)重置FLAG_DISALLOW_INTERCEPT,導(dǎo)致子view設(shè)置的這個(gè)標(biāo)記位無(wú)效,ViewGroup總是調(diào)用onInterceptedTouchEvent詢問(wèn)是否攔截事件。

222.png

除去ACTION_DOWN事件, 若子view設(shè)置了FLAG_DISALLOW_INTERCEPT,intercept = false, 不攔截;否則調(diào)用onInterceptTouchEvent詢問(wèn)是否攔截事件, 默認(rèn)返回false,事件交由子view處理,接著看代碼,

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.
    if (childWithAccessibilityFocus != null) {
           if (childWithAccessibilityFocus != child) {
                continue;
          }
          childWithAccessibilityFocus = null;
          i = childrenCount - 1;
    }
    //子view是否可見(jiàn)或是否正在播放動(dòng)畫
    if (!canViewReceivePointerEvents(child)
            //點(diǎn)擊區(qū)域是否落在子view內(nèi)部
            || !isTransformedTouchPointInView(x, y, child, null)) {
         ev.setTargetAccessibilityFocus(false);
         continue;
    }
    //如果已經(jīng)有子view處理過(guò)事件序列中的一個(gè)事件,則newTouchTarget不為空,跳出當(dāng)前循環(huán)
    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);
    //調(diào)用child的diapatchTouchEvent方法
    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);
}

遍歷ViewGroup的子view,子view如果不可見(jiàn)并且沒(méi)有播放動(dòng)畫,并且點(diǎn)擊區(qū)域沒(méi)有落在子view區(qū)域內(nèi)部,繼續(xù)遍歷下一個(gè)子view;接下來(lái)這段代碼比較有意思,

private TouchTarget getTouchTarget(@NonNull View child) {
        for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
            if (target.child == child) {
                return target;
            }
        }
        return null;
    }

分發(fā)事件時(shí)會(huì)先判斷newTouchTarget是否為空,那么這個(gè)newTouchTarget何時(shí)為空,何時(shí)不為空呢。還記得本文上面有提到addTouchTarget方法么,事實(shí)上mFirstTouchTarget也是在那個(gè)時(shí)候被賦值的,當(dāng)某個(gè)子view處理了ACTION_DOWN事件,mFirstTouchTarget被設(shè)置為當(dāng)前子view,當(dāng)這一個(gè)事件序列的其他事件分發(fā)時(shí),此時(shí)再去獲取newTouchTarget時(shí),newTouchTarget顯然不為空,直接跳出循環(huán),當(dāng)然首次處理時(shí),mFirstTouchTarget為空,newTouchTarget也為空。

接下來(lái)就要了分發(fā)過(guò)程中最重要的步驟了,放大招
通過(guò)將遍歷到的子view傳入dispatchTransformedTouchEvent方法中(此時(shí)child不為空),我們也看一下這個(gè)方法的核心源碼,

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        ...
        final boolean handled;
        ...
        if (child == null) {
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            ...
            handled = child.dispatchTouchEvent(transformedEvent);
        }
        return handled;
    }

方法中通過(guò)child是否為空分為兩種情況,當(dāng)child不為空時(shí),事件就交由子view去處理,這樣就完成了一輪事件的分發(fā),如果子view的dispatchTouchEvent返回true,終于在這里看到了addTouchTarget方法,是的,mFirstTouchTarget就是在這里賦值的,結(jié)束以后跳出循環(huán)。

 newTouchTarget = addTouchTarget(child, idBitsToAssign);


private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

如果遍歷完以后mFirstTouchTarget仍然為空,說(shuō)明子view的dispatchTouchEvent返回了false或者ViewGroup內(nèi)沒(méi)有子元素,此時(shí)ViewGroup會(huì)自己處理這個(gè)點(diǎn)擊事件

if (mFirstTouchTarget == null) {
    //傳入的child為null,說(shuō)明沒(méi)有子view去處理這個(gè)事件
     handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
}

child == null時(shí),handle = super.dispatchTouchEvent(event),到此,事件就轉(zhuǎn)到了View的dispatchTouchEvent方法中。

那么View是怎么分發(fā)事件的

既然到了View,還是要show一下源碼

public boolean dispatchTouchEvent(MotionEvent event) {
        ...
        boolean result = false;
        ...
        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //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;
    }

View的dispatchTouchEvent方法中就比ViewGroup的精簡(jiǎn)了很多,因?yàn)?strong>View是一個(gè)單獨(dú)的元素,它沒(méi)有子view的概念,省去了遍歷子view分發(fā)事件的步驟。從代碼可以看出View對(duì)事件的處理,首先判斷view有沒(méi)有設(shè)置OnTouchListener,如果設(shè)置了并且onTouch方法返回true,那么onTouchEvent方法就不會(huì)被調(diào)用了,若返回false則調(diào)用OnTouchEvent方法。接著我們看一下onTouchEvent方法

public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();
        //clickable和long_clickable只要有一個(gè)為true,就會(huì)消費(fèi)點(diǎn)擊事件
        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
           // 此處說(shuō)明了只要clickable為true,不可用狀態(tài)也會(huì)消費(fèi)點(diǎn)擊事件
            return clickable;
        }
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    if ((viewFlags & TOOLTIP) == TOOLTIP) {
                        handleTooltipUp();
                    }
                    if (!clickable) {
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        break;
                    }
                    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 (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed.  Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            setPressed(true, x, y);
                        }

                        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)) {
                                    //ACTION_UP事件觸發(fā)performClick()
                                    performClickInternal();
                                }
                            }
                        }

                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }

                        if (prepressed) {
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            // If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }

                        removeTapCallback();
                    }
                    mIgnoreNextUpEvent = false;
                    break;
                ...
            }

            return true;
        }

        return false;
    }

通過(guò)View的OnTouchEvent方法不難看出,當(dāng)一個(gè)View的clickable或longClickable為true時(shí),即使它處于不可用的狀態(tài),也依然會(huì)消費(fèi)點(diǎn)擊事件返回true;在ACTION_UP事件發(fā)生時(shí),會(huì)觸發(fā)performClick()方法,看一下這個(gè)方法的代碼

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;
        }
        ...
        return result;
    }

如果View設(shè)置了OnClickLisener,就會(huì)調(diào)用它的onClick方法,到這里,View的事件分發(fā)就結(jié)束了,好像還差點(diǎn)什么。

結(jié)論

1.一個(gè)事件序列以一個(gè)ACTION_DOWN開(kāi)始,中間可能伴隨著多個(gè)ACTION_MOVE,最后以一個(gè)ACTION_UP結(jié)束
2.一個(gè)事件序列只能被一個(gè)view攔截并消費(fèi)掉,若一個(gè)View攔截了ACTION_DOWN,則必定會(huì)設(shè)置mFirstTouchTarget,其它事件會(huì)通過(guò)通過(guò)getTouchTarget.child獲取到子view并交由它處理
3.ViewGroup默認(rèn)不攔截任何事件
4.View沒(méi)有onInterceptedTouchEvent方法,View是單獨(dú)元素,一旦有點(diǎn)擊事件傳遞給它,就會(huì)調(diào)用它的onTouchEvent方法
5.View 的onTouchEvent方法默認(rèn)會(huì)消耗掉事件(返回true),除非它是不可點(diǎn)擊的
6.View的enable屬性不影響onTouchEvent的返回值,只要clickable和longclickable有一個(gè)為true,它的onTouchEvent就返回true
7.onClick發(fā)生的前提是當(dāng)前view是可點(diǎn)擊的,通過(guò)setOnClickListener或setOnLongClickListener會(huì)默認(rèn)將clickable或longclickable置為true
8.OnTouchListener, onTouchEvent, onClickListener之前的優(yōu)先級(jí) OnTouchListener > onTouchEvent > onClickListener


個(gè)人能力有限,如有錯(cuò)誤,歡迎指正

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容