天天看點

Android事件分發機制--淺顯易懂解析

Android事件分發機制的本質

将點選事件向某個View進行傳遞并且最終得到處理,即當一個點選事件發生後,系統需要将這個事件傳遞給一個具體的View處理,這個事件的傳遞過程就是事件分發過程

事件在那些對象傳遞

Activity、ViewGroup、View分發流程:Activity(Window)-> ViewGroup -> Viewsuper:調用父類方法true:處理事件,事件不在繼續往下傳遞false:不處理事件,事件也不繼續傳遞,交給父控件的onTouchEvent()處理傳遞:

Activity -> ViewGroup -> View 從上往下調用dispatchTouchEvent()

View -> ViewGroup -> Activity 從下往上調用onTouchEvent()

1. Activity的事件分發

當一個點選事件發生時,事件最先到達Activity的dispatchTouchEvent()進行事件分發

public boolean dispatchTouchEvent(MotionEvent ev) {
         //一個事件的開始總是從DOWN開始
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            //預設空方法,每當按鍵、觸摸、trackBall事件分發到目前的Activity就會被調用,
            //如果想在Activity運作的時候能夠感覺使用者正在與裝置互動,重寫此方法
            onUserInteraction();
        }
        //getWindow()=Window抽象類,唯一的實作類PhoneWindow
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
           

PhoneWindow

@Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        //mDecor=DecorView,是視圖頂層View,繼承FrameLayout,所有界面的父類
        return mDecor.superDispatchTouchEvent(event);
    }
           

DecorView

public boolean superDispatchTouchEvent(MotionEvent event) {
        //由于DecorView繼承自FrameLayout,FrameLayout繼承自ViewGroup
        //super.dispatchTouchEvent為ViewGroup 的dispatchTouchEvent
        return super.dispatchTouchEvent(event);
    }
           

即getWindow().superDispatchTouchEvent(ev)就是執行了ViewGroup.dispatchTouchEvent(event)

說明事件就是從Activity傳遞到ViewGroup中

Android事件分發機制--淺顯易懂解析

總結:

1. 事件最先傳遞到Activity的dispatchTouchEvent()進行事件分發

2. 調用Window唯一實作類PhoneWindow的 superDispatchTouchEvent()

3. 調用DecorView的superDispatchTouchEvent()

4. 最終調用DecorView父類FrameLayout的dispatchTouchEvent()即ViewGroup的dispatchTouchEvent()

2.View事件分發

public boolean dispatchTouchEvent(MotionEvent event) {
        ...
        boolean result = false;
               ...
        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            ListenerInfo li = mListenerInfo;
            //分析(1)
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
         ...
        //如果分析(1)中onTouch()傳回false(onTouch()未消費事件),
        return result;
    }
           

分析(1):

第一個條件:li != null,經過跟蹤發現在View中發現了 getListenerInfo() 那麼這個方法又是什麼時候調用的呢?

ListenerInfo getListenerInfo() {
        if (mListenerInfo != null) {
            return mListenerInfo;
        }
        mListenerInfo = new ListenerInfo();
        return mListenerInfo;
    }
           

繼續跟蹤發現,getListenerInfo()方法在多個地方使用到,比如setOnClickListener(),setOnLongClickListener(),setOnTouchListener()等等,這裡也正好找到了li.mOnTouchListener != null指派地方setOnTouchListener()

public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }
           
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
        if (!isLongClickable()) {
            setLongClickable(true);
        }
        getListenerInfo().mOnLongClickListener = l;
    }
           
public void setOnTouchListener(OnTouchListener l) {
        getListenerInfo().mOnTouchListener = l;
    }
           

第三個條件:(mViewFlags & ENABLED_MASK) == ENABLED,即判斷目前控件是否是enable,一般控件都是預設enable

第四個條件:li.mOnTouchListener.onTouch(this, event),其實這個方法就是回調控件注冊的touch事件的onTouch()方法,也就是如果我們在onTouch()裡傳回true,這四個條件也就滿足了,進而讓整個方法直接傳回了true,如果我們在onTouch()方法傳回false,就會執行onTouchEvent()方法。

根據上面的研究,我們可以得出結論:onTouch()方法優先于onClick()執行,如果onTouch()方法傳回true(事件被消費)因而事件不會繼續向下傳遞,onClick()方法也将不在執行

我們發現如果條件不通過,則會執行onTouchEvent()方法,可想而知onClick()方法也必然在onTouchEvent()方法中,接着看onTouchEvent源碼:

public boolean onTouchEvent(MotionEvent event) {
        ...
        final int action = event.getAction();
        //隻要控件是CLICKABLE(點選)、LONG_CLICKABLE(長按)有一個為true,onTouchEvent就一定消費事件
        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
        ...
        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)) {
                                    //分析(1)
                                    performClickInternal();
                                }
                            }
                        }
                ...
            }

            return true;
        }
        return false;
    }

           

分析(1):跟蹤performClickInternal()方法

private boolean performClickInternal() {
        // Must notify autofill manager before performing the click actions to avoid scenarios where
        // the app has a click listener that changes the state of views the autofill service might
        // be interested on.
        notifyAutofillManagerOnClick();
        return performClick();
    }
           
public boolean performClick() {
        // We still need to call this method to handle the cases where performClick() was called
        // externally, instead of through performClickInternal()
        notifyAutofillManagerOnClick();
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            //如果注冊了OnClickListener事件就會回調onClick()方法
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        notifyEnterOrExitForAutoFillIfNeeded(true);
        return result;
    }
           

view的事件分發源碼解析到此結束,上總結:

1. onTouch()優先于onClick()先執行

2. 控件被點選時:

(1) 如果onTouch傳回false,執行onTouchEvent()方法,進而執行onClick()方法

(2) 如果onTouch傳回true,則事件被onTouch()消費,onClick()就不會執行了。

3.ViewGroup事件分發

Android事件分發機制--淺顯易懂解析

測試用例:(1)隻點選Button (2)點選ViewGroupB

測試結果:當點選Button的時候,執行Button 的Onclick()方法,但是ViewGroupB 的onTouch()不會執行,隻有點選了ViewGroupB時才會觸發其onTouchEvent()

測試結論:Button的onClick()将事件消費了,是以事件不會繼續向下傳遞

測試用例:(1)重寫Button的onTouch事件傳回true,點選Button

測試結果:Button的onTouch消費了事件,onClick()收不到事件

測試用例:(1)單獨點選ViewGroupB (2)單獨ViewGroupA (3)重寫B的onTouch事件傳回true

測試結果:(1)當單獨點選ViewGroupB的時候A/B 的onTouch()都會被觸發;(2)單獨單擊ViewGroupA時觸發了A的onTouch()

(3)單獨點選ViewGroupB時A的onTouch()沒有被觸發了,因為已經被 B消費了

源碼分析:

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        ...
        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;
            // Handle an initial down.初始化
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);
                //恢複mFirstTouchTarget==null
                resetTouchState();
            }
            // Check for interception.
            final boolean intercepted;
            //按下并且首次
            //mFirstTouchTarget:判斷目前的ViewGroup是否攔截了事件,如果攔截了mFirstTouchTarget=null,如果沒有攔截
            //并交由子view處理,則mFirstTouchTarget!=null
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {//分析(1)
                  
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {//分析(2)
                    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;
    }
           

分析(1):首先必須是ACTION_DOWN狀态,一次完整的事件序列應該是從DOWN開始UP結束,mFirstTouchTarget的意義是:目前ViewGroup是否攔截了事件,如果攔截了,mFirstTouchTarget=null,如果沒有攔截交由子View來處理,則mFirstTouchTarget!=null。假設目前的ViewGroup攔截了事件(disallowIntercept=false),mFirstTouchTarget!=null為false,如果這時觸發ACTION_DOWN事件,則會執行onInterceptTouchEvent()方法;如果觸發的是ACTION_MOVE、ACTION_UP事件,則不再執行onInterceptTouchEvent()方法,而是直接設定了intercepted=true,此後的一個事件序列均由這個ViewGroup處理。

分析(2):FLAG_DISALLOW_INTERCEPT标志主要是禁止ViewGroup攔截除了DOWN之外的事件,一般通過子View的requestDisallowInterceptTouchEvent來設定。

總結:當ViewGroup要攔截事件的時候,那麼後續的事件都要由他處理,而不再調用onInterceptTouchEvent()方法,其中onInterceptTouchEvent預設false,如果想要ViewGroup攔截則重寫方法傳回true。

繼續往下面分析:

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
       ...
       final int childrenCount = mChildrenCount;
       if (newTouchTarget == null && childrenCount != 0) {
           final float x = ev.getX(actionIndex);
           final float y = ev.getY(actionIndex);
           // Find a child that can receive the event.
           // Scan children from front to back.
           
           final ArrayList<View> preorderedList = buildTouchDispatchChildList();
           final boolean customOrder = preorderedList == null
                   && isChildrenDrawingOrderEnabled();
           final View[] children = mChildren;
           //倒序周遊子元素是否能夠接收點選事件(從最上層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的範圍或者子view是否正在播放動畫,如果不符合要求則continue
               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)) {//分析(1)
                   // 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);
           }
           if (preorderedList != null) preorderedList.clear();
       }}}
    ...
    }
           

分析(1):dispatchTransformedTouchEvent()事件分發實際的處理方法

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) {
                //如果沒有子view
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        } 
        ...
        return handled;
    }
           

如果存在子View,則調用View的dispatchTouchEvent()方法,如果ViewGroup沒有子View,則調用super.dispatchTouchEvent()方法即View的dispatchTouchEvent()方法就回到了View的分發上面了。

4. 點選事件分發的傳遞規則

從上面分析得出,僞代碼

fun dispatchTouchEvent(ev: MotionEvent): Boolean {
    var result = false
    if (onInterceptTouchEvent(ev)) {
        result = super.onTouchEvent(ev)
    } else {
        result = child.dispatchTouchEvent(ev)
    }
    return result
}
           

onInterceptTouchEvent()方法和onTouchEvent()方法都是在dispatchTouchEvent()方法中調用的。

我們知道他們的傳遞規則是由上而下,如果到最底層的View一直還沒有消耗事件則又從下而上傳遞

如下圖:(網絡配圖侵權删)

Android事件分發機制--淺顯易懂解析

舉個栗子:(摘抄自劉望舒的《Android進階之光》)

在金庸的《倚天屠龍記》中,武當派實力強勁,按照身份和實力區分,分别是武當掌門張
三豐、武當七俠、武當弟子。這時突然有一個敵人來犯,這個消息首先會彙報給武當掌門張三豐。張三豐
當然不會親自出馬,是以他就将應戰的任務交給武當七俠之一的宋遠橋(onInterceptTouchEvent()傳回
false);宋遠橋威名遠揚,也不會應戰,是以他就把應戰的任務交給武當弟子宋青書
(onInterceptTouchEvent()傳回 false);武當弟子宋青書沒有手下,他隻能自己應戰。在這裡我們把武當
掌門張三豐比作頂層 ViewGroup,将武當七俠之一的宋遠橋比作中層ViewGroup,武當弟子宋青書比作底層
View。那麼事件的傳遞流程就是:武當掌門張三豐(頂層ViewGroup)→武當七俠宋遠橋(中層
ViewGroup)→武當弟子宋青書(底層View)。是以得出結論,事件由上而下傳遞傳回值的規則如下:為
true,則攔截,不繼續向下傳遞;為false,則不攔截,繼續向下傳遞。
接下來講解點選事件由下而上的傳遞。當點選事件傳給底層的 View 時,如果其onTouchEvent()方法
傳回true,則事件由底層的View消耗并處理;如果傳回false則表示該View不做處理,則傳遞給父View的
onTouchEvent()處理;如果父View的onTouchEvent()仍舊傳回false,則繼續傳遞給該父View的父View
處理,如此反複下去。
再傳回上面武俠的例子。武當弟子宋青書發現來犯的敵人是混元霹靂手成昆,他打不過成昆
(onTouchEvent()傳回  false),于是就跑去找宋遠橋,宋遠橋來了,發現自己也打不過成昆
(onTouchEvent()傳回  false),就去找武當掌門張三豐,張三豐用太極拳很輕松地打敗了成昆
(onTouchEvent()傳回true)。是以得出結論,事件由下而上傳遞傳回值的規則如下:為true,則處理
了,不繼續向上傳遞;為false,則不處理,繼續向上傳遞。
           

5.總結

  • onToutch()和onTouchEvent()的差別

    這兩個方法都是在View的dispatchTouchEvent()中調用的;但onTouch優先于onTouchEvent()執行,如果在onTouch()中傳回true,onTouchEvent() 将不再執行。

    另外,onTouch能夠得到執行隻需兩個前提條件,第一mOnTouchListener的值不能為空,第二目前點選的控件必須是enable,因為如果是非enable,那麼注冊的onTouch事件永遠無法得到執行,對于這一類控件,重寫onTouchEvent方法來監聽touch事件

  • Touch事件的後續事件層級傳遞

    當dispatchTouchEvent()在進行事件分發的時候,隻有前一個事件傳回true,才會收到後一個事件(比如:在執行ACTION_DOWN時傳回了false,後面一系列的ACTION_MOVE、ACTION_UP事件都不會執行了)

    dispatchTouchEvent()和onTouchEvent()消費事件、終結事件傳遞傳回 true,那麼收到ACTION_DOWN,也會收到ACTION_MOVE、ACTION_UP

    如果在某個對象(Activity、ViewGroup、View)的onTouchEvent()消費事件(傳回true),那麼ACTION_MOVE和ACTION_UP事件從上往下到這個View後就不在往下傳遞了,而是直接傳給自己的onTouchEvent()并結束本次事件傳遞的過程。

完結散花。。

繼續閱讀