android事件處理芦昔,最復(fù)雜的就是對Touch事件的處理诱贿,因為Touch事件包括:down, move, up, cancle和多點觸摸等多種情況娃肿,多點觸摸的情況先不討論咕缎,因為Touch有這么多的狀態(tài),所以Touch相對來說是最難處理的料扰,下面就來討論一下android系統(tǒng)是如何處理Touch事件的
先來張參照圖:
1.說到事件處理凭豪,首先我們要明白,為什么要處理事件晒杈,要了解android系統(tǒng)本身對事件的一個處理過程.在實際的開發(fā)中嫂伞,我們?nèi)绻加孟到y(tǒng)的基本控件,那是不需要去處理事件的拯钻,但是如果我們用復(fù)雜的布局嵌套去做一些特殊的需求帖努,例如:ScrollView中嵌套ListView,ScrollView嵌套ViewPager等,則會產(chǎn)生事件沖突粪般,所以拼余,由于事件沖突的存在,我們要去處理這些沖突亩歹,只有了解android的事件處理機制匙监,才能有效的去處理事件沖突.還有就是如果我們要新開發(fā)一個組件,則組件的所有事件都要我們自己去做處理小作,這種情況也需要我們?nèi)ヌ幚硎录裕河捎诖嬖谝陨险f到的兩種情況亭姥,我們要自己處理事件.
2.有了處理事件的動機后,接下來就要了解android系統(tǒng)本身是如何處理復(fù)雜的事件的.a(chǎn)ndroid系統(tǒng)為所有的事件提供了三個相關(guān)的方法顾稀,以下只以Touch事件為例說明.
這三個方法分別是:
dispatchTouchEvent(MotionEvent ev); (Activity, ViewGroup, View都有此方法)
onInterceptTouchEvent(MotionEvent ev); (只有ViewGroup有)
onTouchEvent(MotionEvent); (Activity, ViewGroup, View都有此方法)
要想了解android系統(tǒng)是如何對事件進行一步一步的處理达罗,這三個方法是必須要掌握的.其中: dispatchTouchEvent(MotionEvent ev);方法是用來對事件進行分發(fā)的,即將事件分發(fā)到目標(biāo)控件静秆,onInterceptTouchEvent(MotionEvent ev)是用來過濾事件的粮揉,即進行事件的攔截,也就是是否要向下傳遞事件诡宗,onTouchEvent(MotionEvent ev)才是最終用來處理事件的滔蝉,也就是說我們平常重寫onTouchEvent時,其實塔沃,系統(tǒng)已經(jīng)默認幫我們調(diào)用了前兩個方法.下面就來詳細分析一下三個方法.
首先要提的是蝠引,android系統(tǒng)對本件的處理是一層一層向下傳遞處理(樹形處理).那這棵樹是從那來的呢..就是我們的布局樹阳谍,一個布局,無論是代碼編寫的布局還是xml生成的布局螃概,android系統(tǒng)對它進行解析時都是將其組裝成一棵UI樹矫夯,最外層布局是整個UI樹的根.知道這個以后,再來分析事件的處理.
處理流程:當(dāng)我們的手指觸摸到手機屏幕時吊洼,當(dāng)前處于onStart()狀態(tài)的Activity最先接收到此Touch事件下的ACTON_DOWN训貌,然后開始調(diào)用它自己dispatchTouchEvent()開始進行DOWN事件分發(fā),如果此方法返回true,則Activity不向下分發(fā)事件冒窍,則整個布局都不會收到DOWN事件递沪,TouchEvent直接到Activity的onTouchEvent()方法進行事件處理.如果返回false,則表示DOWN是要被分發(fā)到下層的,此時DOWN事件被直接分發(fā)(因為沒有過濾方法)到UI樹的根布局(即最外層的布局)综液,根布局拿到DOWN事件時款慨,執(zhí)行自己的dispatchTouchEvent方法,返回true,則事件直接交到根布局的onTouchEvent()中進行處理谬莹,false則表示還得向下分發(fā)檩奠,此時事件被傳遞到根布局的onInterceptTouchEvent()方法中,如果此方法返回true,表示要對此事件進行過濾附帽,則此DOWN事件又直接進行到根布局的onTouchEvent()方法直接處理埠戳,false則,要根布局不對事件進行過濾蕉扮,DOWN事件繼續(xù)向下傳遞整胃,直到達到目標(biāo)組件后,目標(biāo)組件調(diào)用自己的dispatchTouchEvent()方法慢显,由于是目標(biāo)組件爪模,直接分發(fā)事件到自己的onTouchEvent方法中,目標(biāo)組件如果處理完這個DOWN事件后返回true,表示該事件被消費完畢荚藻,不再向上層傳遞屋灌,如果返回false,則表示沒有消費完這個DOWN事件应狱,DOWN向上傳遞到自己的父組件中共郭,父組件再進行DOWN事件的處理.一直向上傳遞直到事件被扔到虛擬機.DOWN事件才算處理完成,接著調(diào)用MOVE,MOVE完了UP,整個流程與DOWN是一樣的.
這里要強調(diào)一點的是:如果一個組件沒有接收到DOWN事件疾呻,那么一定接收不到MOVE,UP事件除嘹。
通過以上的流程,我們可以明白:android系統(tǒng)對任何一個事件的處理都是這樣的岸蜗,分發(fā)事件尉咕,過濾事件,處理事件璃岳,下一個事件年缎, 分發(fā)事件悔捶,過濾事件,處理事件……一直這樣循環(huán)去處理所有的事件的单芜。即:事件的分發(fā)蜕该,過濾是從根到葉的,處理則是從葉再到根的
從圖上看洲鸠,我們可以更直觀的感受整個Touch事件的處理流堂淡。
總結(jié):onInterceptTouchEvent負責(zé)對事件進行攔截,攔截成功后交給最先遇到onTouchEvent返回true的那個view進行處理扒腕。
看到這里如果你還是有點困惑绢淀,請想象一下生活中常見的場景:假如你所在的公司,有一個總經(jīng)理袜匿,級別最高更啄,它下面有個部長稚疹,級別次之居灯,最底層就是干活的你,沒有級別∧诠罚現(xiàn)在總經(jīng)理有一個任務(wù)怪嫌,總經(jīng)理將這個業(yè)務(wù)布置給部長,部長又把任務(wù)安排給你柳沙,當(dāng)你完成這個任務(wù)時岩灭,就把任務(wù)反饋給部長,部長覺得這個任務(wù)完成的不錯赂鲤,于是就簽了他的名字反饋給總經(jīng)理噪径,總經(jīng)理看了也覺得不錯,就也簽了名字交給董事會数初,這樣找爱,一個任務(wù)就順利完成了。這其實就是一個典型的事件攔截機制泡孩。
在這里我們先定義三個類:
一個總經(jīng)理—MyViewGroupA车摄,最外層的ViewGroup
一個部長—MyViewGroupB,中間的ViewGroup
一個你—MyView仑鸥,在最底層
根據(jù)以上的場景吮播,我們可以繪制以下流程圖:
從圖中,我們可以看到在ViewGroup中眼俊,比View多了一個方法—onInterceptTouchEvent()方法意狠,這個是干嘛用的呢,是用來進行事件攔截的疮胖,如果被攔截环戈,事件就不會往下傳遞了誊役,不攔截則繼續(xù)。
如果我們稍微改動下谷市,如果總經(jīng)理(MyViewGroupA)發(fā)現(xiàn)這個任務(wù)太簡單蛔垢,覺得自己就可以完成,完全沒必要再找下屬迫悠,因此MyViewGroupA就使用了onInterceptTouchEvent()方法把事件給攔截了鹏漆,此時流程圖:
我們可以看到,事件就傳遞到MyVewGroupA這里就不繼續(xù)傳遞下去了创泄,就直接返回艺玲。
如果我們再改動下,總經(jīng)理(MyViewGroupA)委托給部長(MyViewGroupB)鞠抑,部長覺得自己就可以完成饭聚,完全沒必要再找下屬,因此MyViewGroupB就使用了onInterceptTouchEvent()方法把事件給攔截了搁拙,此時流程圖:
我們可以看到秒梳,MyViewGroupB攔截后,就不繼續(xù)傳遞了箕速,同理如果酪碘,到干貨的我們上(MyView),也直接返回True的話盐茎,事件也是不會繼續(xù)傳遞的兴垦,如圖:
源碼
分析Android View事件傳遞機制之前有必要先看下源碼的一些關(guān)系,如下是幾個繼承關(guān)系圖:
看了官方這個繼承圖是不是明白了上面例子中說的LinearLayout是ViewGroup的子類字柠,ViewGroup是View的子類探越,Button是View的子類關(guān)系呢?其實窑业,在Android中所有的控件無非都是ViewGroup或者View的子類钦幔,說高尚點就是所有控件都是View的子類。
從View的dispatchTouchEvent方法說起
在Android中你只要觸摸控件首先都會觸發(fā)控件的dispatchTouchEvent方法(其實這個方法一般都沒在具體的控件類中数冬,而在他的父類View中)节槐,所以我們先來看下View的dispatchTouchEvent方法,如下:
public boolean dispatchTouchEvent(MotionEvent event) {
// If the event should be handled by accessibility focus first.
if (event.isTargetAccessibilityFocus()) {
// We don't have focus or no virtual descendant has it, do not handle the event.
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
// We have focus and got the event, then use normal event dispatch.
event.setTargetAccessibilityFocus(false);
}
boolean result = false;
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
if (onFilterTouchEventForSecurity(event)) {
//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;
}
}
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
// Clean up after nested scrolls if this is the end of a gesture;
// also cancel it if we tried an ACTION_DOWN but we didn't want the rest
// of the gesture.
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}```
dispatchTouchEvent的代碼有點長拐纱,但可以挑幾個重點講講铜异,if (onFilterTouchEventForSecurity(event))語句判斷當(dāng)前View是否沒被遮住等,然后定義ListenerInfo局部變量秸架,ListenerInfo是View的靜態(tài)內(nèi)部類揍庄,用來定義一堆關(guān)于View的XXXListener等方法;接著if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event))語句就是重點东抹,首先li對象自然不會為null蚂子,li.mOnTouchListener呢沃测?你會發(fā)現(xiàn)ListenerInfo的mOnTouchListener成員是在哪兒賦值的呢?怎么確認他是不是null呢食茎?通過在View類里搜索可以看到:
/**
* Register a callback to be invoked when a touch event is sent to this view.
* @param l the touch listener to attach to this view
*/
public void setOnTouchListener(OnTouchListener l) {
getListenerInfo().mOnTouchListener = l;
}```
li.mOnTouchListener是不是null取決于控件(View)是否設(shè)置setOnTouchListener監(jiān)聽蒂破,在上面的實例中我們是設(shè)置過Button的setOnTouchListener方法的,所以也不為null别渔,接著通過位與運算確定控件(View)是不是ENABLED 的附迷,默認控件都是ENABLED 的,接著判斷onTouch的返回值是不是true哎媚。通過如上判斷之后如果都為true則設(shè)置默認為false的result為true喇伯,那么接下來的if (!result && onTouchEvent(event))就不會執(zhí)行,最終dispatchTouchEvent也會返回true拨与。而如果if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event))語句有一個為false則if (!result && onTouchEvent(event))就會執(zhí)行稻据,如果onTouchEvent(event)返回false則dispatchTouchEvent返回false,否則返回true买喧。
這下再看前面的實例部分明白了吧捻悯?控件觸摸就會調(diào)運dispatchTouchEvent方法,而在dispatchTouchEvent中先執(zhí)行的是onTouch方法岗喉,所以驗證了實例結(jié)論總結(jié)中的onTouch優(yōu)先于onClick執(zhí)行道理秋度。如果控件是ENABLE且在onTouch方法里返回了true則dispatchTouchEvent方法也返回true,不會再繼續(xù)往下執(zhí)行钱床;反之,onTouch返回false則會繼續(xù)向下執(zhí)行onTouchEvent方法埠居,且dispatchTouchEvent的返回值與onTouchEvent返回值相同
dispatchTouchEvent總結(jié) :
在View的觸摸屏傳遞機制中通過分析dispatchTouchEvent方法源碼我們會得出如下基本結(jié)論:
觸摸控件(View)首先執(zhí)行dispatchTouchEvent方法查牌。
在dispatchTouchEvent方法中先執(zhí)行onTouch方法,后執(zhí)行onClick方法(onClick方法在onTouchEvent中執(zhí)行滥壕,下面會分析)纸颜。
如果控件(View)的onTouch返回false或者mOnTouchListener為null(控件沒有設(shè)置setOnTouchListener方法)或者控件不是enable的情況下會調(diào)運onTouchEvent,dispatchTouchEvent返回值與onTouchEvent返回一樣绎橘。
如果控件不是enable的設(shè)置了onTouch方法也不會執(zhí)行胁孙,只能通過重寫控件的onTouchEvent方法處理(上面已經(jīng)處理分析了),dispatchTouchEvent返回值與onTouchEvent返回一樣称鳞。
如果控件(View)是enable且onTouch返回true情況下涮较,dispatchTouchEvent直接返回true,不會調(diào)用onTouchEvent方法冈止。
onTouchEvent方法 :
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
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) {
// 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();
}
break;
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;
if (performButtonActionOnTouchDown(event)) {
break;
}
// Walk up the hierarchy to determine if we're inside a scrolling container.
boolean isInScrollingContainer = isInScrollingContainer();
// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
checkForLongClick(0);
}
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
break;
case MotionEvent.ACTION_MOVE:
drawableHotspotChanged(x, y);
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
setPressed(false);
}
}
break;
}
return true;
}
return false;
}```
首先地6到14行可以看出狂票,如果控件(View)是disenable狀態(tài),同時是可以clickable的則onTouchEvent直接消費事件返回true熙暴,反之如果控件(View)是disenable狀態(tài)闺属,同時是disclickable的則onTouchEvent直接false慌盯。多說一句,關(guān)于控件的enable或者clickable屬性可以通過java或者xml直接設(shè)置掂器,每個view都有這些屬性亚皂。
接著22行可以看見,如果一個控件是enable且disclickable則onTouchEvent直接返回false了国瓮;反之孕讳,如果一個控件是enable且clickable則繼續(xù)進入過于一個event的switch判斷中,然后最終onTouchEvent都返回了true巍膘。switch的ACTION_DOWN與ACTION_MOVE都進行了一些必要的設(shè)置與置位厂财,接著到手抬起來ACTION_UP時你會發(fā)現(xiàn),首先判斷了是否按下過峡懈,同時是不是可以得到焦點璃饱,然后嘗試獲取焦點,然后判斷如果不是longPressed則通過post在UI Thread中執(zhí)行一個PerformClick的Runnable肪康,也就是performClick方法荚恶。具體如下:
public boolean performClick() {
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);
return result;
}```
這個方法也是先定義一個ListenerInfo的變量然后賦值,接著判斷l(xiāng)i.mOnClickListener是不是為null磷支,決定執(zhí)行不執(zhí)行onClick谒撼。你指定現(xiàn)在已經(jīng)很機智了,和onTouch一樣雾狈,搜一下mOnClickListener在哪賦值的唄廓潜,結(jié)果發(fā)現(xiàn):
public void setOnClickListener(OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
控件只要監(jiān)聽了onClick方法則mOnClickListener就不為null,而且有意思的是如果調(diào)運setOnClickListener方法設(shè)置監(jiān)聽且控件是disclickable的情況下默認會幫設(shè)置為clickable善榛。
onTouchEvent小結(jié) :
onTouchEvent方法中會在ACTION_UP分支中觸發(fā)onClick的監(jiān)聽辩蛋。
當(dāng)dispatchTouchEvent在進行事件分發(fā)的時候,只有前一個action返回true移盆,才會觸發(fā)下一個action悼院。
通過以上總結(jié),Android中的事件攔截機制咒循,其實跟我們生活中的上下級委托任務(wù)很像据途,領(lǐng)導(dǎo)可以處理掉,也可以下發(fā)給下屬員工處理叙甸,如果員工處理的好颖医,領(lǐng)導(dǎo)才敢給你下發(fā)任務(wù),如果你處理不好蚁署,則領(lǐng)導(dǎo)也不敢把任務(wù)交給你便脊,這就像在中途把下發(fā)的任務(wù)的中途攔截掉了。通過流程和源碼的分析,相信大家能比較容易了解事件的分發(fā)哪痰、攔截遂赠、處理事件的流程。學(xué)習(xí)過程中晌杰,保持好奇心是很重要的跷睦。