Android從原始碼角度剖析View事件分發機制

愛學園發表於2018-11-08

本文由 愛學園平臺 進行聯合編輯整理輸出 轉載請註明出處

原作者:愛學園——莫比烏斯環

       在開始描述問題之前先說點題外話,寫這篇文章的初衷一方面為了構建Android知識體系,另一方面是真心覺得這個是Android面試必問的知識點。網上這方面的部落格和書籍講解這方面的知識也不少,講的也很到位。正所謂只有自己理解了才是自己的,所以在閱讀了他們的文章後,加上自己的理解特此記錄一篇~,以便加深理解和記憶!如理解有誤的地方請留言說明,我們一起探討,謝謝!

聯絡方式:郵箱(ixiyan.li@gmail.com)

1.必備知識點

       事件的分發說白了,就是使用者與應用的互動過程(手指與螢幕接觸)中,發生的一系列事件傳遞與處理過程。

1.1 事件分發涉及的物件--MotionEvent

典型事件型別:

ACTION_DOWN——手指剛觸碰螢幕那一刻(按下)
ACTION_MOVE——手指在螢幕上移動(移動)
ACTION_UP——手指抬起那一刻(抬起)
複製程式碼

一個事件序列:就是從手指按下 View 開始直到手指離開 View 產生的一系列事件。

ACTION_DOWN-> ACTION_UP
ACTION_DOWN->...ACTION_MOVE...->ACTION_UP
複製程式碼

1.2 事件分發涉及的方法

1. dispatchTouchEvent(MotionEvent ev)

用來進行事件分發。返回結果受當前 View 的 onTouchEvent 和子 View 的 dispatchTouchEvent 方法的影響,表示是否消耗當前事件。

2. onInterceptTouchEvent(MotionEvent ev)

在上述dispatchTouchEvent方法內部呼叫,用來進行當前事件是否攔截校驗。這裡有一點要注意的地方就是如果當前View攔截了某個事件(一般指ACTION_DOWN),那麼在同一個事件序列(上面講過這個概念)當中,此方法不會被再次呼叫——即不會做二次攔截校驗。 注:Activity和View內部沒有此方法

3. onTouchEvent(MotionEvent ev)

在上述dispatchTouchEvent方法內部呼叫,返回結果表示是否消耗當前事件。這裡同上也有一點要注意,如果當前方法返回false(不消耗),那麼同一個事件序列中,當前View無法再次接收到事件。

上述方法的關係可用下面的一段虛擬碼表示:

public boolean dispatchTouchEvent(MotionEvetn e){ 
	if(onInterceptTouchEvent(ev)){//是否攔截
		return onTouchEvent(e);//攔截事件處理:是否消耗
	}
	return child.dispatchTouchEvent(e);//不攔截:子類View分發
}
複製程式碼

通過上面的虛擬碼可以大致瞭解到事件的傳遞規則:對於一個根ViewGroup來說,點選事件產生後,首先會傳遞給它,這時它的dispatchTouchEvent就會被呼叫,如果這個ViewGrouponInterceptTouchEvent方法返回true,就說明攔截當前事件,接著事件就會交給這個ViewGrouponTouchEvent方法處理。反之onInterceptTouchEvent方法返回false,就不攔截當前事件,這時當前事件就會傳遞給它的子View,接著ViewdispatchTouchEvent方法就會呼叫,如此反覆直到事件最終被處理。

1.3 事件傳遞過程遵循如下過程

Activity -> Windown(PhoneWindow) -> DecorView(FrameLayout) -> contentView(setContentView) ->..ViewGroup..->View
複製程式碼

2. 事件分發原始碼解析

根據上面瞭解到的事件傳遞的過程分析,下面我們就一步一步撕開它神祕的面紗,從內部瞭解它的呼叫關係。

2.1 Activity對點選事件的分發過程

點選事件用MontionEvent表示,當一個點選操作發生時,最先傳遞給當前Activity,由ActivitydispatchTouchEvent方法進行事件分發,具體的工作由Window來完成。Window會將事件傳遞給DecorViewDecorView一般就是當前介面的底層容器(即setContentView所設定的 View 的父容器),通過Activity.getWindow().getDecorView()可以獲得。因此我們先從ActivitydispatchTouchEvent開始分析。

原始碼-1:Activity#dispatchTouchEvent

/**
* Called to process touch screen events.  You can override this to
* intercept all touch screen events before they are dispatched to the
* window.  Be sure to call this implementation for touch screen events
* that should be handled normally.
*
* @param ev The touch screen event.
*
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
    
public boolean onTouchEvent(MotionEvent event) {
    if (mWindow.shouldCloseOnTouch(this, event)) {
        finish();
        return true;
    }

    return false;
}    
複製程式碼

現在分析上述程式碼,通過原始碼瞭解到事件交給Activity所附屬的Window進行分發,如果getWindow().superDispatchTouchEvent(ev)返回true,事件到此結束,返回false,說明下級所有View的onTouchEvent都返回了false,則Activity的onTouchEvent將會被呼叫(如上)

通過上面瞭解到getWindow().superDispatchTouchEvent(ev)這個才是分發的關鍵,看原始碼:

原始碼-2:Window#superDispatchTouchEvent


/**
 * Abstract base class for a top-level window look and behavior policy.  An
 * instance of this class should be used as the top-level view added to the
 * window manager. It provides standard UI policies such as a background, title
 * area, default key processing, etc.
 *
 * <p>The only existing implementation of this abstract class is
 * android.view.PhoneWindow, which you should instantiate when needing a
 * Window.
 */
public abstract class Window {
	/**
	 * Used by custom windows, such as Dialog, to pass the touch screen event
	 * further down the view hierarchy. Application developers should
	 * not need to implement or call this.
	 *
	 */
	public abstract boolean superDispatchTouchEvent(MotionEvent event);
	...
}
複製程式碼

看上面貼的原始碼發現貼了好多註釋說明,因為這裡Window是個抽象類,那麼它的實現類是什麼呢,是PhoneWindow,為什麼呢?到這裡您可以詳細閱讀下上面Window類的說明,發現此處已經指明瞭Window的唯一實現就是android.view.PhoneWindow,好傢伙,隱藏的夠深的,那麼請移駕,謝謝~

原始碼-3:PhoneWindow#superDispatchTouchEvent相關程式碼

// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}
複製程式碼

到這裡邏輯就清晰了吧!雖然程式碼只有一行,但已經足以說明問題了,此處具體邏輯移交給DecorView(這就是我們前面說的視窗的頂級View-->ViewGroup),即Activity#setContentView設定的View就是DecorView的子View。目前事件傳遞到了DecorView這裡,由於DecorVieW即成自FrameLayout且是父View,那麼得出結論--最終事件會傳遞給View,到這一步並不是我們的重點,事件如何通過頂級View進行傳遞消費才是我們的重頭戲,請繼續,謝謝~

2.2 頂級View對點選事件的分發過程

關於點選事件如何在View中進行分發,上面已經做了描述,這裡就直接上ViewGroup原始碼,原始碼如下:

dispatchTouchEvent方法內容較多分如下幾個片段說明:

原始碼-4:ViewGroup#dispatchTouchEvent——攔截邏輯處理

// 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);
    resetTouchState();
}
// 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;
}
複製程式碼
  1. 是否攔截條件:事件型別為ACTION_DOWN || mFirstTouchTarget != null;
  2. mFirstTouchTarget:每次開始(ACTION_DOWN)都會被初始化為null,當事件由ViewGroup的子元素成功處理時,它指向子元素;
  3. 當事件由ViewGroup攔截時,條件mFirstTouchTarget != null不成立,即當ACTION_MOVEACTION_UP事件到來時,由於第一條攔截條件不滿足,則onInterceptTouchEvent不再呼叫:應證了一旦當前View攔截事件,那麼同一事件序列的其它事件都不再進行攔截校驗,直接交給它處理。
  4. FLAG_DISALLOW_INTERCEPT標記位:這個標記位一旦設定後(requestDisallowInterceptTouchEvent),ViewGroup將無法攔截除了ACTION_DOWN以外的其它點選事件(ACTION_DOWN事件會重置此標記位,將導致子View中設定的這個標記位無效)。
  5. 面對ACTION_DOWN事件時,ViewGroup總是會呼叫自己的onInterceptTouchEvent方法來詢問自己是否要攔截事件,這一點從上面的原始碼中可以看出來。

原始碼-5:ViewGroup#dispatchTouchEvent——初始化

// 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();//重置FLAG_DISALLOW_INTERCEPT標記位
}
複製程式碼

從上面的程式碼可以看出,ViewGroup會在ACTION_DOWN事件到來時會做重置狀態的操作,因此子View呼叫requestDisallowInterceptTouchEvent並不能影響ViewGroupACTION_DOWN事件的處理。

總結:

  1. ViewGroup決定攔截事件(ACTION_DOWN)後,那麼後續的點選事件將會預設交給它處理且不再呼叫它的onInterceptTouchEvent方法。
  2. FLAG_DISALLOW_INTERCEPT這個標誌的作用是讓ViewGroup不再攔截事件,當然前提是ViewGroup不攔截ACTION_DOWN事件。
  3. FLAG_DISALLOW_INTERCEPT為解決滑動衝突解決提供了新的思路。

原始碼-6:ViewGroup#dispatchTouchEvent——不攔截,遍歷子View

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

        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)) {//*子元素呼叫dispatchTouchEvent方法*
            // 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();
            //儲存當前子View:mFirstTouchTarget
            newTouchTarget = addTouchTarget(child, idBitsToAssign);
            alreadyDispatchedToNewTouchTarget = true;
            break;
        }

        // The accessibility focus did not handle the event, so clear
        // the flag and do a normal dispatch to all children.
        ev.setTargetAccessibilityFocus(false);
    }
    if (preorderedList != null) preorderedList.clear();
    //...
}
複製程式碼

原始碼-7:ViewGroup#dispatchTouchEvent——子View下發主要邏輯呼叫

/**
 * 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;

    // Canceling motions is a special case.  We do not 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是否能夠接收點選事件有以下兩點衡量:

  • 子元素是否在播放動畫
  • 點選事件的座標是否落在子元素的區域內

上面這部分程式碼說明的是ViewGroup不攔截情況下,事件向子View下發的過程.即主要呼叫方法為dispatchTransformedTouchEvent,它的內部實際上呼叫的就是子元素的dispatchTouchEvent方法(可通過上面的原始碼-7看得出來).通過具體分析可看出,如果child.dispatchTouchEvent(event)返回true,那麼mFirstTouchTarget(addTouchTarget方法內部操作)就會被賦值同時跳出for迴圈,這裡是否對mFirstTouchTarget賦值,將會影響ViewGroup的攔截策略,如下所示:

原始碼-8:ViewGroup#dispatchTouchEvent——賦值mFirstTouchTarget

/**
 * Adds a touch target for specified child to the beginning of the list.
 * Assumes the target child is not already present.
 */
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
    final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;
}
複製程式碼

mFirstTouchTarget如果為null,將會預設攔截接下來同一序列的所有事件。(不做二次攔截校驗)

遍歷所有子元素,都沒有處理包含兩種情況:

  1. ViewGroup沒有子元素;
  2. 子元素處理了點選事件,但是所有的子元素都沒有消耗事件。

此時ViewGroup將會呼叫super.dispatchTouchEvent(evet),這一點可以從上述原始碼-8可以看出,很顯然這裡ViewGroup繼承自View,所以這裡就轉到ViewdispatchTouchEvent方法,即點選事件交由View處理,那麼請繼續看下面的分析。

2.3 View對點選事件的處理過程

View(不包含ViewGroup)對點選事件的處理稍微簡單,它沒有onInterceptTouchEvent方法且無法向下傳遞事件,只能自己處理,請看它的dispatchTouchEvent方法,如下:

原始碼-9:View#dispatchTouchEvent——View點選事件處理

/**
* Pass the touch screen motion event down to the target view, or this
* view if it is the target.
*
* @param event The motion event to be dispatched.
* @return True if the event was handled by the view, false otherwise.
*/
public boolean dispatchTouchEvent(MotionEvent event) {
// If the event should be handled by accessibility focus first.
//...
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;
}
複製程式碼

從上面的程式碼可以看出:OnTouchListener的onTouchonTouchEvent(event)優先順序高,如果設定了OnTouchListenermOnTouchListener.onTouch返回true那麼onTouchEvent(event)將不會呼叫,反之將會呼叫onTouchEvent(event),見下文:

原始碼-10:View#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();
	
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 does not respond to them.
	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 do not 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)) {
	                        performClick();
	                    }
	                }
	            }
	
	            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;
}
複製程式碼

從上面的程式碼看出:影響事件的消耗因素有兩個:CLICKABLELONG_CLICKABLE只要有一個為true,那麼它就會消耗這個事件,即onTouchEvent方法返回true,實際呼叫方法為performClick();,在其內部呼叫OnClickListener#onClick方法。

到此點選事件的分發機制的原始碼分析就完了,但是Android 的學習才剛開始,還有很長的路要走,下面附上從別處盜來的圖,覺得不錯可以看下

2.4 View事件分發流程圖示例圖

事件分發流程圖

參考相關文章與相關書籍

Android 事件分發

Android事件分發機制解析

書籍:任玉剛的《Android開發藝術探索》

       致親愛的讀者朋友們,從今日起我們《愛學園平臺》將會持續推出前後端技術方面的相關的各類知識點以及疑難問題分析,如果您有需要,但我們還沒有輸出的疑難問題,您也可以給我們留言!我們將會根據您的需求輸出,謝謝!

相關文章