天天看點

dispatch、onIntercept、onTouch三者對MotionEvent的分發、攔截機制分析

近期正在測試一個通過左右觸劃MainView,平滑切換兩側菜單的項目,如果僅僅是觸劃切換也許我就不必顧慮什麼dispatch、onIntercept了,估計一個onTouch就差不多搞定了,但是惱人的是MainView自身嵌套了比較複雜的可以上拉、下拉重新整理的自定義PullToFreshView,而PullToFresh内部又嵌套了一個可以上下Scroll的ScrollView和GridView,MainView策劃切換到菜單的手勢必須與ScrollView中上下滾動Item的手勢分開才行,這樣必須搞一個MotionEvent攔截覆寫在MainView(PullToFreshView)中,onTouch研究的并不深入,但用起來還是比較上手的,而Intercept看着眼熟,沒深究過其攔截機制,恰恰再此項目中手勢的攔截又寫的很複雜,看來有必要把這三個方法理一理了。也許很多新手還想想不到onIntercept能有何作為,不過看看模拟的示意圖,可以試着考慮:在右側List打開狀态,該如何分離一系列MotionEvent是拉回主界面的手勢呢,還是上下翻動List的手勢呢?

dispatch、onIntercept、onTouch三者對MotionEvent的分發、攔截機制分析

查閱關于oninterceptTouchEvent的使用方法,有很多資料,其用法就是判斷目前ViewGroup是否有必要将MotionEvent繼續往内(頂)層子view(Group)進行傳遞,而查詢dispatchTouchEvent的使用方法則收獲很少,經過大量翻閱,才發現自己做了很多無用功,不過好在對其略知一二了,便把這個方法的調用機制分享給那些同樣在做無用功的同學們吧。

盲點一:它們的傳回值決定了什麼樣的行為?

方法一:onTouchEvent(父(Group)View預設傳回false;最外層可被點選的子View預設傳回true)

onTouchEvent是最常見、最容易上手的方法,它是三兄弟中最基層的MotionEvent消費方法,處于流水線的最下遊,負責着大部分MotionEvent的分析處理任務,是響應觸摸指令的最直接機關。其意義在于消費一個MotionEvent對象,一旦吸收、轉化了該對象,該對象就從對象流中被抹殺掉了,不會再往子View(Group)傳遞。

方法二:onInterceptTouchEvent(預設傳回false)

onInterceptTouchEvent是初學者很少見的方法,該方法用于篩選有利于該層面的View(Group)的MotionEvent對象,根據篩選的傳回值判斷一個MotionEvent對象是消費在該層還是繼續傳遞給内層嵌套的子View(Group),通常父View攔截、消費觸摸是沒有意義的,是以預設不攔截這些對象流,都發給最頂層或者最内層的View。當然位于最内層的View即便攔截也是沒有意義的,對象傳到這裡就必須得出個結果,是以在最頂層的子View攔截也得進入onTouchEvent處理,不攔截還是要進入這個方法處理的,是以沒必要覆寫什麼攔截方法了。

方法三:dispatchTouchEvent(預設傳回true)

dispatchTouchEvent對初學者來說更加少見,而涉及其詳細處理邏輯的文章非常少見,小生作本文的初衷便是窺得該方法的冰山一角,以為它是條大魚,想再挖掘點好玩的出來,可費了老大勁後才發現,其内部無非是對前兩個方法的疊代調用,隻不過邏輯有點耐人尋味而已。聞其名而知其意,dispatch--分發,該方法是前兩者的集合,而且其傳回狀态直接決定MotionEvent對象是進入該層View(Group)的攔截過濾、分析處理子產品,還是直接丢棄。該方法如此霸道,自有其霸道的資本,因為任何MotionEvent對象預設都是先走他的父類方法的,既然産生觸摸事件,那這些對象自然是預設進行分發操作的,隻因為很少如此絕決的阻斷某一系列Event對象,初級程式員很少有必要對該方法進行覆寫罷了,即便覆寫該方法,其内部疊代邏輯我們是改變不了的,隻能在直接丢棄某些MotionEvent對象時有所作為,不過通常類似丢垃圾的操作都在onTouch裡面解決,當然要想保持onTouch的整潔,可以考慮在dispatch中幹掉目标Event,免去後顧之憂。

小結:我想到了一個很具體的機構可以用來形容三個方法的關系--快遞公司。

dispatch相當于收集快遞的卡車;onIntercept相當于分揀員;onTouch相當于快遞員。隻有有價值的快遞才會被裝進收件的卡車,碰到磚頭、空箱之類的廢物就直接丢棄。等開車載着值得分發的快遞回到公司,該輪到分揀員工作了。分揀員可能會把這一車快遞分成兩類,一類是本地的,另一類是外地的,本地的快遞自然要找本地的快遞員遞送到客戶手中,而外地的快遞還要重新裝車拉往外地子公司再次進行以上集中、篩選、遞送流程。下圖是我自己做的關于攔截原理的示意圖:

dispatch、onIntercept、onTouch三者對MotionEvent的分發、攔截機制分析

盲點二:它們的協作邏輯是怎樣的?

要描述三者之間的邏輯,還是用僞代碼來示範更明了了吧:

//首先進入分發環節
dispatchTouchEvent()
{ //進入dispatchTouchEvent方法
	//判斷目前對象的傳回值,true - 進行分發(攔截、處理);false - 不進行分發,直接丢棄
	if(dispatch== true)
	{//進入onInterceptTouchEvent方法
		//其次判斷目前onInterceptTouchEvent的傳回值(是否被攔截)
		if(Intercept== false)
		{
			//沒被攔截,得到此ViewGroup的全部子類:
			for (int i = count - 1; i >= 0; i--){
			//擷取子View(Group)對象,對其進行分發檢查
     			final View child = children[i];
			child.dispatchTouchEvent(this){···};
			}
    		}  else  {//進入onTouchEvent方法
			//如果ViewGroup被打斷(onInterceptTouchEvent傳回true),或者目前為最内(頂)層的純View
			onTouchEvent();
		}
	}  else  {
	//将傳回标志false的MotionEvent對象統統丢棄
	}
 }
           

小結:

MotionEvent對象首先流經dispatch,直接決定該對象分發、處理的必要性;dispatch傳回true,才進入本層面的intercept攔截檢查;攔截檢查傳回true的對象直接進入本層面的onTouch進行處理;攔截傳回false的對象,将繼續從底到上,從外到内傳遞給子類疊代這個分發、攔截、處理過程。

嚴正聲明

上述觀點大部分源于對開源知識的總結,小部分為個人通過Demo調試、分析獲得,是以文章内容僅供參考,如有異議,小生洗耳恭聽,在技術認知上求同存異、共同提高。下面是個人Demo的介紹。

dispatch、onIntercept、onTouch三者對MotionEvent的分發、攔截機制分析

提醒:本人習慣上把宿主(基本視圖)相對于寄生者(内嵌視圖)叫做外(下),不适應的請轉換一下思考角度。

本視圖包含三層(View):

深褐色區域-自定義LinearLayout--MainView

墨綠色區域-自定義LinearLayout--InnerView

灰白色區域-自定義Button--BtnView

粉紅色文字-僅作提示之用

操作方法:

觸摸上半屏可以攔截該層以上(inner、btn)的Action_Move;

觸摸下半屏可以攔截該層以上(btn)的的Action_Move。

MainView.java如下(InnerView、BtnView的主體與此相同不再列出,隻是對标簽加以區分,便于分析日志):

package com.yaong.myview;

public class MainView extends LinearLayout {

	private final String TAG = "111";
	private int iStart = 0 ;
	public MainView(Context context) {
		super(context);
		// TODO Auto-generated constructor stub
	}

	public MainView(Context context, AttributeSet attrs) {
		super(context, attrs);
		// TODO Auto-generated constructor stub
	}

	@SuppressLint("NewApi")
	public MainView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		// TODO Auto-generated constructor stub
	}

	@SuppressLint("NewApi")
	@Override
	public boolean onInterceptTouchEvent(MotionEvent event) {
		// TODO Auto-generated method stub
		switch (event.getAction()) {
		case MotionEvent.ACTION_DOWN:
			Log.v(TAG, "III    DDD");
			break;
		case MotionEvent.ACTION_MOVE:
			Log.v(TAG, "III    MMM");
			//觸摸螢幕上半邊,攔截該View的所有ActionMove對象
			if (event.getY()<Constant.iCenterY) {
				return true;
			}
			break ;
		case MotionEvent.ACTION_CANCEL:
			Log.v(TAG, "III    CCC");
			break;
		case MotionEvent.ACTION_UP:
			Log.v(TAG, "III    UUU");
			break;
		default:
			break;
		}
		return super.onInterceptTouchEvent(event) ;
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		// TODO Auto-generated method stub
		switch (event.getAction()) {
		case MotionEvent.ACTION_DOWN:
			Log.v(TAG, "TTT    DDD");
			break;
		case MotionEvent.ACTION_MOVE:
			Log.v(TAG, "TTT    MMM");
			break;
		case MotionEvent.ACTION_CANCEL:
			Log.v(TAG, "TTT    CCC");
			break;
		case MotionEvent.ACTION_UP:
			Log.v(TAG, "TTT    UUU");
			break;
		default:
			break;
		}
		return super.onTouchEvent(event);
	}

	@Override
	public boolean dispatchTouchEvent(MotionEvent event) {
		switch (event.getAction()) {
		case MotionEvent.ACTION_DOWN:
			Log.v(TAG, "DDD    DDD");
			break ;
		case MotionEvent.ACTION_MOVE:
			Log.v(TAG, "DDD    MMM");
			break ;
		case MotionEvent.ACTION_CANCEL:
			Log.v(TAG, "DDD    CCC");
		case MotionEvent.ACTION_UP:
			Log.v(TAG, "DDD    UUU");
			break;
		default:
			break;
		}
		return super.dispatchTouchEvent(event);
	}
}

           

布局檔案:activity_main.xml如下:

<com.yaong.myview.MainView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#44F00F00"
    android:orientation="vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity" >

    <com.yaong.myview.ViewInner1
        android:id="@+id/inner_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#4400FF00"
        android:orientation="vertical"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin" >


        <com.yaong.myview.View_MyButton
            android:id="@+id/btn_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:text="ABCABC"
            android:textColor="@color/clr3"
             >
        </com.yaong.myview.View_MyButton>

    </com.yaong.myview.ViewInner1>

</com.yaong.myview.MainView>
           

列印日志說明:

标簽Tag		111		代表MainView
		222 		代表InnerView
		333或444 	代表BtnView或TxtView
Text前半段 	DDD 		代表dispatch方法内
		III 		代表intercept方法内
		TTT 		代表touch方法内
Text後半段 	DDD 		代表Action_Down
		MMM 		代表Action_Move
		CCC 		代表Action_Cancel
		UUU 		代表Action_Up
           

羅列部分日志供分析參考、分析:

情況一:輕點目标MainView一下,TAG=111,

01-23 12:25:32.159: V/111(15128): DDD    DDD
01-23 12:25:32.159: V/111(15128): III    DDD
01-23 12:25:32.159: V/111(15128): TTT    DDD
01-23 12:25:32.279: V/111(15128): DDD    UUU
01-23 12:25:32.279: V/111(15128): TTT    UUU
01-23 12:25:32.279: E/MainAct(15128): main click

           

點選最底層紅褐色區域,

Action_Down流程 dispatch(111)-->intercept(111)-->touch(111)

Action_Up流程 dispatch(111)-->touch(111)

分析:觸摸事件的終點便是MainView,雖然在布局中内部嵌套了子View,但觸摸與上層子View無關,隻能由被點選View消費該事件,而Up事件作為Down的後續事件不必再進行攔截檢測。

情況二:輕點目标InnerView一下,TAG=222,

01-23 13:34:52.559: V/111(17377): DDD    DDD
01-23 13:34:52.559: V/111(17377): III    DDD
01-23 13:34:52.559: I/222(17377): DDD    DDD
01-23 13:34:52.559: I/222(17377): III    DDD
01-23 13:34:52.559: I/222(17377): TTT    DDD
01-23 13:34:52.649: V/111(17377): DDD    UUU
01-23 13:34:52.649: V/111(17377): III    UUU
01-23 13:34:52.649: I/222(17377): DDD    UUU
01-23 13:34:52.649: I/222(17377): TTT    UUU
01-23 13:34:52.649: E/MainAct(17377): inner click
           

點選墨綠色環形區域,

Action_Down流程 dispatch(111)-->intercept(111)-->dispatch(222)-->intercept(222)-->touch(222)

Action_Up流程 dispatch(111)-->intercept(111)-->dispatch(222)-->touch(222)

分析:觸摸事件的目标View是InnerView,觸摸事件必然從父ViewGroup(111)傳到子View(222),111中的攔截檢測預設傳回false,觸摸事件繼續向子View内傳遞,由于目标是InnerView,觸摸事件将被222消費掉。

情況三:輕點目标BtnView一下,TAG=333,

01-23 13:46:14.679: V/111(17377): DDD    DDD
01-23 13:46:14.679: V/111(17377): III    DDD
01-23 13:46:14.679: I/222(17377): DDD    DDD
01-23 13:46:14.679: I/222(17377): III    DDD
01-23 13:46:14.679: E/333(17377): DDD    DDD
01-23 13:46:14.679: E/333(17377): TTT    DDD
01-23 13:46:14.769: V/111(17377): DDD    UUU
01-23 13:46:14.769: V/111(17377): III    UUU
01-23 13:46:14.769: I/222(17377): DDD    UUU
01-23 13:46:14.769: I/222(17377): III    UUU
01-23 13:46:14.769: E/333(17377): DDD    UUU
01-23 13:46:14.769: E/333(17377): TTT    UUU
01-23 13:46:14.769: E/MainAct(17377): btn click
           

點選中間白色區域,

Action_Down流程 dispatch(111)-->intercept(111)-->dispatch(222)-->intercept(222)-->dispatch(333)-->touch(333)

Action_Up流程 dispatch(111)-->intercept(111)-->dispatch(222)-->intercept(222)-->dispatch(333)-->touch(333)

分析:觸摸事件的目标View是BtnView,觸摸事件必然途徑111、222,再傳到333中,111、222中攔截狀态皆是false,直到對象傳至333中被消費掉。

情況四:觸劃上半屏BtnView(MainVIew将攔截該層的所有ActionMove對象)

01-23 14:49:31.189: V/111(18763): DDD    DDD
01-23 14:49:31.189: V/111(18763): III    DDD
01-23 14:49:31.189: I/222(18763): DDD    DDD
01-23 14:49:31.189: I/222(18763): III    DDD
01-23 14:49:31.189: E/333(18763): DDD    DDD
01-23 14:49:31.189: E/333(18763): TTT    DDD
01-23 14:49:31.259: V/111(18763): DDD    MMM
01-23 14:49:31.259: V/111(18763): III    MMM
01-23 14:49:31.259: I/222(18763): DDD    CCC
01-23 14:49:31.259: I/222(18763): DDD    UUU
01-23 14:49:31.259: I/222(18763): III    CCC
01-23 14:49:31.259: E/333(18763): DDD    CCC
01-23 14:49:31.259: E/333(18763): TTT    CCC
01-23 14:49:31.289: V/111(18763): DDD    MMM
01-23 14:49:31.289: V/111(18763): TTT    MMM
//此處省略無限多111的Move狀态日志······
01-23 14:49:31.399: V/111(18763): DDD    UUU
01-23 14:49:31.399: V/111(18763): TTT    UUU
           

由于在MainView的onInterceptTouchEvent種對Action_Move進行了攔截,那麼本應該傳到Btn的Touch中的Action_Move對象将被攔截在111中,除了底層111的onTouch能接收Action_Move,其嵌套的InnerView、BtnView都将接收不到Move事件,也就是上面日志中随着移動手指,111将産生無限多Move事件,而另外兩者則一直沉默。不過,困擾我的是日志中粉色背景色的周圍的日志,ActionMove被攔截在111中後,222中産生一個ActionCancel事件,然後演變成UP事件,但222有Down記錄,也再此産生了Up記錄,但并沒有産生一個對222的click事件。

情況五:觸劃下半屏BtnView(InnerVIew将攔截該層的所有ActionMove對象)

01-23 15:26:12.739: V/111(18763): DDD    DDD
01-23 15:26:12.739: V/111(18763): III    DDD
01-23 15:26:12.739: I/222(18763): DDD    DDD
01-23 15:26:12.739: I/222(18763): III    DDD
01-23 15:26:12.739: E/333(18763): DDD    DDD
01-23 15:26:12.739: E/333(18763): TTT    DDD
01-23 15:26:12.819: V/111(18763): DDD    MMM
01-23 15:26:12.819: V/111(18763): III    MMM
01-23 15:26:12.819: I/222(18763): DDD    MMM
01-23 15:26:12.819: I/222(18763): III    MMM
01-23 15:26:12.819: E/333(18763): DDD    CCC
01-23 15:26:12.819: E/333(18763): TTT    CCC
01-23 15:26:12.839: V/111(18763): DDD    MMM
01-23 15:26:12.839: V/111(18763): III    MMM
01-23 15:26:12.839: I/222(18763): DDD    MMM
01-23 15:26:12.839: I/222(18763): TTT    MMM
//此處省略無限多111、222的Move狀态日志······
01-23 15:26:12.849: V/111(18763): DDD MMM
01-23 15:26:12.849: V/111(18763): III MMM
01-23 15:26:12.849: I/222(18763): DDD MMM
01-23 15:26:12.849: I/222(18763): TTT MMM
01-23 15:26:12.899: V/111(18763): DDD UUU
01-23 15:26:12.899: V/111(18763): III UUU
01-23 15:26:12.899: I/222(18763): DDD UUU
01-23 15:26:12.899: I/222(18763): TTT UUU
           

在該測試中,觸劃按鈕,InnerView将對ActionMove對象進行攔截,隻不過比情況四攔截的晚一個階段,有日志資訊可知Move事件在222被攔截住後,再333中産生了一個Cancel事件,該Cancel在BtnView的onTouchEvent中消費完後就陷入了沉默,而111、222依然在很有規律的列印Move資訊。

情況六:更改222攔截位置到ActionDown,觸劃BtnView(InnerVIew将攔截該層的所有ActionDown對象)

01-23 15:43:36.129: V/111(23276): DDD    DDD
01-23 15:43:36.129: V/111(23276): III    DDD
01-23 15:43:36.129: I/222(23276): DDD    DDD
01-23 15:43:36.129: I/222(23276): III    DDD
01-23 15:43:36.129: I/222(23276): TTT    DDD
01-23 15:43:36.329: V/111(23276): DDD    MMM
01-23 15:43:36.329: V/111(23276): III    MMM
01-23 15:43:36.329: I/222(23276): DDD    MMM
01-23 15:43:36.329: I/222(23276): TTT    MMM
//此處省略無限多111、222的Move狀态日志······
01-23 15:43:36.689: V/111(23276): DDD UUU
01-23 15:43:36.689: V/111(23276): III UUU
01-23 15:43:36.689: I/222(23276): DDD UUU
01-23 15:43:36.699: I/222(23276): TTT UUU
01-23 15:43:36.699: E/MainAct(23276): inner click
           

這種情況與情況五相似,而其差别在于,一旦Down事件被攔截,BtnView将不可能受到任何MotionEvent對象,也未在333中發生 收不到Move事件,莫名産生一個Cancel事件的情況,而且222産生了一個完整的Click事件。 

情況七:更改222的dispatch方法,在ActionMove事件後傳回标志false(不分發Move事件)

01-23 15:55:01.849: V/111(23981): DDD    DDD
01-23 15:55:01.849: V/111(23981): III    DDD
01-23 15:55:01.849: I/222(23981): DDD    DDD
01-23 15:55:01.849: I/222(23981): III    DDD
01-23 15:55:01.849: E/333(23981): DDD    DDD
01-23 15:55:01.849: E/333(23981): TTT    DDD
01-23 15:55:01.899: V/111(23981): DDD    MMM
01-23 15:55:01.899: V/111(23981): III    MMM
01-23 15:55:01.899: I/222(23981): DDD    MMM
01-23 15:55:01.929: V/111(23981): DDD    MMM
01-23 15:55:01.929: V/111(23981): III    MMM
01-23 15:55:01.929: I/222(23981): DDD    MMM
01-23 15:55:01.949: V/111(23981): DDD    MMM
01-23 15:55:01.949: V/111(23981): III    MMM
01-23 15:55:01.949: I/222(23981): DDD    MMM
01-23 15:55:01.959: V/111(23981): DDD    MMM
01-23 15:55:01.959: V/111(23981): III    MMM
01-23 15:55:01.959: I/222(23981): DDD    MMM
01-23 15:55:01.979: V/111(23981): DDD    MMM
01-23 15:55:01.979: V/111(23981): III    MMM
01-23 15:55:01.979: I/222(23981): DDD    MMM
01-23 15:55:01.999: V/111(23981): DDD    MMM
01-23 15:55:01.999: V/111(23981): III    MMM
01-23 15:55:01.999: I/222(23981): DDD    MMM
01-23 15:55:02.069: V/111(23981): DDD    UUU
01-23 15:55:02.069: V/111(23981): III    UUU
01-23 15:55:02.069: I/222(23981): DDD    UUU
01-23 15:55:02.069: I/222(23981): III    UUU
01-23 15:55:02.069: E/333(23981): DDD    UUU
01-23 15:55:02.069: E/333(23981): TTT    UUU
01-23 15:55:02.069: E/MainAct(23981): btn click
           

如果在dispatch中改動Move事件的傳回标志,則每個Move對象傳遞到dispatch時都卡住了,不能進入本層以及内層的intercept、onTouch方法,是以歸結其原因為dispatch傳回false的所有對象都被丢棄了,不可能再往内層傳遞。是以dispatch是MotionEvent處理的重要方法,但一般不輕易在dispatch裡面做手腳。

總結 經過對Demo的各種改最終就得到了上面那點了解,可能測試過程比較混亂,導緻結果與預期的有所偏差,是以貼出此文以求改進,如果某位也願意考究onTouch、onIntercept、dispatch裡面的學問,可以考慮去下載下傳我的測試Demo,當然自己寫一個也不費啥事,文中不實之處,還望指正,共同完善。