Android源碼分析之View事件分發機制

什麼叫事件分發機制

事件分發是:當發生了一個事件時,在屏幕上找到一個合適的控件來處理這個事件的過程。因爲一個界面上控件如此之多,發生一個事件後總要尋找一個合適來處理事件吧。這個過程就叫做事件分發的機制。

事件分發流程

Activity是Android應用程序的門面和載體,它代表一個完整的用戶界面。Activity提供了一個窗口來繪製各種視圖,即PhoneWindow類。該類繼承自頂層窗口類Window,並且包含一個DecorView類對象。DecorView繼承自FrameLayout(幀佈局),所以本質上是一個ViewGroup,而且是當前活動所放置的全部View的根視圖(RootView)。當我們創建一個活動時,在活動的onCreate()方法中調用 setContentView(layoutID) 方法就是爲該活動的ContentView部分指定佈局內容從而完成GUI的渲染。

當用戶點擊屏幕產生一個事件,事件通過底層硬件捕獲,然後交給ViewRootImpl處理,ViewRootImpl通過Window將事件交給Activity。事件要傳遞給Activity那麼它就必須持有Activity的引用,Window在Activity的attach方法中通過mWindow.setCallback(this)調用持有了Activity的引用,Activity實現了Window.Callback的接口方法。所以最終事件是通過Window.Callback.dispatchTouchEvent把時間交給Acitivity的。

/**
 * 從窗口返回到調用方的API。這允許客戶端攔截密鑰調度、面板和菜單等。
 */
public interface Callback {
    public boolean dispatchKeyEvent(KeyEvent event);
    public boolean dispatchKeyShortcutEvent(KeyEvent event);
    public boolean dispatchTouchEvent(MotionEvent event);
    ...........
}

事件發生時,ViewRootImpl通過Window將事件交給Activity,然後再一層層的向下層傳遞,直到找到合適的處理控件。大致如下:

硬件 -> ViewRootImpl -> Window -> Activity -> PhoneWindow -> DecorView -> VIewGroup -> View

但是如果事件傳遞到最後的View還是沒有找到合適的View消費事件,那麼事件就會向相反的方向傳遞,最終傳遞給Activity,如果最後 Activity 也沒有處理,本次事件纔會被拋棄:

Activity <- PhoneWindow <- DecorView <- ViewGroup <- View

事件的類型

事件主要分爲觸摸事件和點擊事件。

觸摸事件

觸摸事件對應的是MotionEvent類,主要有以下三種類型

  • ACTION_DOWN:表示用戶手指按下的動作,標誌着觸摸事件的開始。
  • ACTION_UP:表示用戶手指離開屏幕的動作,標誌着觸摸事件的結束。
  • ACTION_CANCEL:如果某一個子View處理了Down事件,那麼隨之而來的Move和Up事件也會交給它處理。但是交給它處理之前,父View還是可以攔截事件的,如果攔截了事件,那麼子View就會收到一個Cancel事件,並且不會收到後續的Move和Up事件。
  • ACTION_MOVE:表示用戶手指移動的動作。當用戶手指按下屏幕後,在鬆開之前,只要移動的距離超過了一定的閾值即判定爲ACTION_MOVE動作。實際上,即使是手指非常 輕微的移動也會被系統監測到從而判定爲ACTION_MOVE動作。

用戶觸摸屏幕操作由ACTION_DOWN事件開始,結束於ACTION_UP事件,可以有0次或多次ACTION_MOVE事件。

點擊事件

用戶手指按下→停留若干時間(可長可短)→用戶手指鬆開,這一完整的過程視爲一次點擊事件。可以看出,觸摸事件先於點擊事件執行。

事件的分發

在我們平時的使用或寫自定義View時,都會直接或間接的使用View的事件分發,View的事件分發主要與View源碼中的3個方法有關:

  • dispatchTouchEvent()
  • onTouch()
  • onTouchEvent()

當事件發生時,ViewGroup會在dispatchTouchEvent方法中先看自己能否處理事件,如果不能再去遍歷子View查找合適的處理控件。如果到最後result還是false,表示所有的子View都不能處理,纔會調用自身的onTouchEvent來處理。View的事件分發我們需要看一個方法dispatchTouchEvent。我們通過View的事件分發的實現源碼來分析分發流程。

/**
* 將觸摸屏運動事件向下傳遞到目標視圖,如果它是目標視圖,則傳遞此視圖。
* @param 事件要調度的運動事件。
* @return 如果事件由視圖處理,則爲True;否則爲false。
*/
public boolean dispatchTouchEvent(MotionEvent event) {
    //省略代碼
    boolean result = false;
    if (onFilterTouchEventForSecurity(event)) {
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
        //ListenerInfo是View的內部類,這個類定義了各種的監聽以及事件,
        //包括焦點變化監聽,滾動變化監聽,點擊事件監聽等
        ListenerInfo li = mListenerInfo;
        //如果li.mOnTouchListener.onTouch(this, event)返回true,
        //並且view的狀態是enable狀態下,該方法的result就直接返回true
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }
        //如果result=false,纔會走下面的onTouchEvent
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }
    //省略代碼
    return result;
}

註釋寫得很清楚了,主要概括幾個要點:

  • 如果我們傳入的OnTouchListener中的onTouch返回true的話,並且在enable=true情況下,就不會執行到onTouchEvent方法
  • view.setEnable(false)的時候,不會執行onTouchListener的onTouch方法,但是會執行View自身的onTouchEvent方法
  • view.setEnable(false)的時候,OnClickLisener.onClick方法不會執行(下面提到)

dispatchTouchEvent中沒有發現view的onClick方法的調用,其實onClick在onTouchEvent中,在判斷事件類型的swithc中,在ACTION_UP擡起的事件中,我們看到這樣的代碼:

public boolean onTouchEvent(MotionEvent event) {
	...
	switch (action) {
		case MotionEvent.ACTION_UP:
		...
		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)) {
                     performClickInternal();
                 }
             }
         }
}

在performClickInternal中調用了 performClick()方法,就是在這個方法中執行了view的onClick方法

/**
 * Entry point for {@link #performClick()} - other methods on View should call it instead of
 * {@code performClick()} directly to make sure the autofill manager is notified when
 * necessary (as subclasses could extend {@code performClick()} without calling the parent's
 * method).
 */
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();
}

在performClick中,可以看到如果設置了監聽的話就會調用view的onClick方法。

 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);
        li.mOnClickListener.onClick(this);
        result = true;
    } else {
        result = false;
    }
    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
    notifyEnterOrExitForAutoFillIfNeeded(true);
    return result;
}

所以我們可以得出一個初步的結論。也就是說在view的onTouchEventListener中的onTouch()如果返回了true,表示此次事件被處理,也就是按下的ACTION_DOWN事件被消耗,但是ACTION_UP事件無法傳遞執行,所以onClick不會執行。
如果onTouchEventListener中的onTouch返回false。則會執行View自身的onTouchEvent方法,表示onTouch不會消耗事件,所以onClick點擊事件會響應。
有一點需要特別注意:setClickable(false)需要在setOnClickListener之後調用才起作用,因爲在setOnClickListener中將view設置setClickable(true)了

/**
  * Register a callback to be invoked when this view is clicked. If this view is not
  * clickable, it becomes clickable.
  * @param l The callback that will run
  * @see #setClickable(boolean)
  */
 public void setOnClickListener(@Nullable OnClickListener l) {
     if (!isClickable()) {
         setClickable(true);
     }
     getListenerInfo().mOnClickListener = l;
 }

ViewGroup事件傳遞

前面分析了View的事件分發,但在實際開發過程中真正要使用View事件分發時,基本都是因爲ViewGroup的嵌套導致的內外滑動問題,所以對ViewGroup的事件分發更需要深入瞭解,和View的事件分發一樣,ViewGroup事件分發一樣與幾個重要方法有關:

  • dispatchTouchEvent() -> 用來分派事件
  • onInterceptTouchEvent() -> 用來攔截事件
  • onTouchEvent() -> 用來處理事件

使用一段僞代碼來表述上面三個方法在ViewGroup事件分發中的作用,代碼如下:

public boolean dispatchTouchEvent(MotionEvent event){
	boolean consume = false;
	if(onInterceptTouchEvent(event)){
		consume = onTouchEvent(event);
	}else{
		consume = child.dispatchTouchEvent(event);
	}
	return consume;
}

從上面代碼中看出,事件傳遞到ViewGroup時首先傳遞到dispatchTouchEvent(MotionEvent event)中,然後執行以下邏輯,首先在ViewGroup.dispatchTouchEvent() 中調用onInterceptTouchEvent() 方法:

  • 返回true,表示攔截事件 -> onTouchEvent() -> 返回true 表示消耗
  • 返回false,表示不攔截事件 -> child.dispatchTouchEvent(event) 事件向下傳遞,如此反覆傳遞分發

在onInterceptTouchEvent() 返回false時,表明當前ViewGroup不消耗事件,此事件會向下傳遞給子View,此子View可能是View也可能是ViewGroup,如果是View則按照上面的事件分發消耗事件。
事件的傳遞首先是從手指觸摸屏幕開始,所以我們先查看ViewGroup的dispatchTouchEvent()中的ACTION_DOWN方法,剔除剩餘複雜的邏輯,方法有一段主要的代碼:

public boolean dispatchTouchEvent(MotionEvent event){
	...
	// 標誌自身是否攔截此事件
	final boolean intercepted;
	if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
	    // 當子View調用requestDisallowInterceptTouchEvent函數時該變量爲true。
	    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;  // 返回true表示子View設置了父容器不攔截事件
	    if (!disallowIntercept) {
	    	// 如果子View沒有禁止父View攔截事件,父View通過該函數判斷是否需要攔截此事件。
	        intercepted = onInterceptTouchEvent(ev);
	        ev.setAction(action); 
	    } else {
	        intercepted = false;
	    }
	} else {
		// 一定攔截事條件:事件類型不爲ACTION_DOWN並且mFirstTouchTarget爲null。
		// 這說明ACTION_DOWN事件已經被自身消耗,那麼該事件序列中的剩餘事件也應該被自身消耗。
	    intercepted = true;
	}
	...
}

上述代碼雖然簡單但ViewGroup的事件分發多半與此處的邏輯有關,裏面的每個細節都會影響到最終的事件消耗,總結上面代碼執行如下:

  • 首先當一個事件進來的時候,會先判斷當前事件是否是down或着mFirstTouchTarget是否爲null(當我們第一次進到ViewGroup.dispatchTouchEvent的時候mFirstTouchTarget會爲空的),如果當前事件爲down或者mFirstTouchTarget爲null的時候就會調用ViewGroup.onInterceptTouchEvent方法,接着會將onInterceptTouchEvent的返回進行記錄。
  • mFirstTouchTarget:指向處理觸摸事件的子View;當ViewGroup子View成功攔截後,mFirstTouchTarget指向子View,此時滿足mFirstTouchTarget != null,則在整個事件過程中會不斷詢問ViewGroup的攔截狀況;
  • 如果ViewGroup確定攔截事件,mFirstTouchTarget爲null,所以整個觸摸事件不會詢問ViewGroup的onInterceptedTouchEvent(),且之後的事件直接交給ViewGroup執行;
  • 可能有小夥伴會問,disallowIntercept這個值是什麼東西,不知道大家有沒有用過getParent().requestDisallowInterceptTouchEvent(true)方法來達到禁止父ViewGroup攔截事件,當我們給這個方法設置什麼,disallowIntercept就會是什麼值,所以我們在事件攔截的時候,可以在其子View裏面調用該方法進行事件攔截。

當intercepted爲false也就是不攔截的時候,就會遍歷子元素,並將事件向下分發交給子元素進行處理:

final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
       if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
             //給mFirstTouchTarget賦值
             newTouchTarget = addTouchTarget(child, idBitsToAssign);
      }
}

可以看到當在遍歷子孩子的時候會調用dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)方法,,而在dispatchTransformedTouchEvent方法中我們會發現下面代碼,通過下面代碼會發現,當child不爲空的時候他就會直接調用子元素的dispatchTouchEvent方法,這樣事件就交由子元素處理了,從而完成一輪分發。

if (child == null) {
     handled = super.dispatchTouchEvent(event);
 } else {
     handled = child.dispatchTouchEvent(event);
 }

當子View.dispatchTouchEvent返回爲true時,就會調用addTouchTarget(child, idBitsToAssign)方法,該方法就是在給mFirstTouchTarget賦值。
當子View.dispatchTouchEvent返回爲false時,就不會調用addTouchTarget(child, idBitsToAssign)方法,故mFirstTouchTarget爲null。
那麼mFirstTouchTarget爲null時會出現什麼情況呢,繼續向下看,會看到下面的代碼,注意這裏的view傳的是null,也就是說會調用super.dispatchTouchEvent(event)代碼,super.dispatchTouchEvent(event)是什麼呢?他就是我們自己的dispatchTouchEcent方法。也就是事件將我們自己去處理。

if (mFirstTouchTarget == null) {
     handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
 }

根據上面的View和ViewGroup的事件分發學習,這裏給出幾個View事件傳遞的結論(以下結論針對系統自動分發),並根據學習內容進行逐條分析

  • 事件序列指的是從手指接觸屏幕那一刻起,到手指離開屏幕那一刻爲止產生的所有事件。
  • ViewGroup默認不攔截任何事件。
  • 事件分發過程中ViewGroup會考慮多點觸控的問題,例如在一個佈局中有兩個子控件,如果兩個手指同時對它們進行操作,控件是可以正常響應的。
  • 正常情況下一個事件序列只能被一個View攔截或消耗,除非使用特殊方法控制事件傳遞;
  • 對於View一旦決定攔截事件即onTouchEvent()返回true,那後續的整個事件序列都會交給它消耗;
  • 如果View不消耗ACTION_DOWN事件,則後續的事件序列都不會再給他處理
  • 如果View在ACTION_DOWN時返回false,那系統的mFirstTouchTarget爲null,在後續的MOVE、UP事件中onInterceptTouchEvent()不會再被調用,直接攔截事件

View事件攔截案例分析

事件攔截最經典的使用示例和場景就是滑動衝突,按照View的衝突場景分,滑動衝突可以分爲3類:

  • 外部滑動和內部滑動方向不一致
  • 外部滑動和內部滑動方向一致
  • 以上兩種情況嵌套

一般處理滑動衝突有兩種攔截方法:外攔截和內攔截

外部攔截

外攔截顧名思義是在View的外部攔截事件,對View來說外部就是其父類容器,即在父容器中攔截事件,通過上面的代碼我們知道,ViewGroup的事件攔截取決與onInterceptTouchEvent()的返回值,所以我們在ViewGroup中重寫onInterceptTouchEvent()方法,在父類需要的時候返回true攔截事件,具體需要的場景要按照自己的業務邏輯判斷:

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
	switch (event.getAction()) {
	    case MotionEvent.ACTION_DOWN:
	    case MotionEvent.ACTION_UP:
	        break;
	    case MotionEvent.ACTION_MOVE:
		    if(isParentNeed()){
		    	//父容器的邏輯
		    	return true;
		    }
	        break;
		default:
		        break;
	}
	super.onInterceptTouchEvent(event)
    return false;
}

從上面代碼中看出:在onInterceptTouchEvent()的ACTION_DOWN中必須返回false,即不攔截ACTION_DOWN事件,因爲如果ACTION_DOWN一但攔截,事件後面的事件都會默認給ViewGroup處理,也不會再調用onInterceptTouchEvent()詢問攔截,那子View將沒有獲取事件的機會;在ACTION_DOWN中,根據自己需要的時候返回true,那此時事件就會被父ViewGroup消耗。

內部攔截

內部攔截法父View攔截除ACTION_DOWN以外的其它事件。子View在ACTION_DOWN中調用getParent().requestDisallowInterceptTouchEvent(true)方法接管事件並在ACTION_MOVE中根據業務邏輯決定事件是否教給父View處理。如需交給父View處理則調用requestDisallowInterceptTouchEvent(false)方法。內部攔截法不符合事件分發流程,是通過子VIew反向控制父View攔截,規則:

  • 父元素要默認攔截除了ACTION_DOWN以外的其他事件
  • 子元素調用parent.requestDisallowInterceptTouchEvent(false/true)來控制父元素是否攔截事件
  • 父元素不能攔截ACTION_DOWN因爲它不受FLAG_DISALLOW_INTERCEPT標誌位控制,一旦父容器攔截ACTION_DOWN那麼所有的事件都不會傳遞給子View
/**
 * 內部攔截法
 * 父View需攔截除DOWN以外的其他事件
 */
public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        super.onInterceptTouchEvent(ev);
        return false;
    } else {
        return true;
    }
}

/**
 * 內部攔截法
 * 子View.dispatchTouchEvent特殊處理
 */
public boolean dispatchTouchEvent(MotionEvent event) {
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
            parent.requestDisallowInterceptTouchEvent(true);
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            int deltaX = x - mLastX;
            int deltaY = y - mLastY;
            if (isParentNeed()) {
                parent.requestDisallowInterceptTouchEvent(false);
            }
            break;
        }
        case MotionEvent.ACTION_UP: {
            break;
        }
        default:
            break;
    }
    mLastX = x;
    mLastY = y;
    return super.dispatchTouchEvent(event);
}

上述代碼是內部攔截的典型代碼,當面對不同的滑動策略時只需要修改裏面的條件即可,其他不需要做改動而且也不能有改動。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章