近期正在測試一個通過左右觸劃MainView,平滑切換兩側菜單的項目,如果僅僅是觸劃切換也許我就不必顧慮什麼dispatch、onIntercept了,估計一個onTouch就差不多搞定了,但是惱人的是MainView自身嵌套了比較複雜的可以上拉、下拉重新整理的自定義PullToFreshView,而PullToFresh内部又嵌套了一個可以上下Scroll的ScrollView和GridView,MainView策劃切換到菜單的手勢必須與ScrollView中上下滾動Item的手勢分開才行,這樣必須搞一個MotionEvent攔截覆寫在MainView(PullToFreshView)中,onTouch研究的并不深入,但用起來還是比較上手的,而Intercept看着眼熟,沒深究過其攔截機制,恰恰再此項目中手勢的攔截又寫的很複雜,看來有必要把這三個方法理一理了。也許很多新手還想想不到onIntercept能有何作為,不過看看模拟的示意圖,可以試着考慮:在右側List打開狀态,該如何分離一系列MotionEvent是拉回主界面的手勢呢,還是上下翻動List的手勢呢?
查閱關于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相當于快遞員。隻有有價值的快遞才會被裝進收件的卡車,碰到磚頭、空箱之類的廢物就直接丢棄。等開車載着值得分發的快遞回到公司,該輪到分揀員工作了。分揀員可能會把這一車快遞分成兩類,一類是本地的,另一類是外地的,本地的快遞自然要找本地的快遞員遞送到客戶手中,而外地的快遞還要重新裝車拉往外地子公司再次進行以上集中、篩選、遞送流程。下圖是我自己做的關于攔截原理的示意圖:
盲點二:它們的協作邏輯是怎樣的?
要描述三者之間的邏輯,還是用僞代碼來示範更明了了吧:
//首先進入分發環節
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的介紹。
提醒:本人習慣上把宿主(基本視圖)相對于寄生者(内嵌視圖)叫做外(下),不适應的請轉換一下思考角度。
本視圖包含三層(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,當然自己寫一個也不費啥事,文中不實之處,還望指正,共同完善。