詳解Android事件的分發(fā)、攔截和執(zhí)行
在平常的開發(fā)中,我們經(jīng)常會(huì)遇到點(diǎn)擊,滑動(dòng)之類的事件。有時(shí)候不同的view之間也存在各種滑動(dòng)沖突。比如布局的內(nèi)外兩層都能滑動(dòng)的話,那么就會(huì)出現(xiàn)沖突了。這個(gè)時(shí)候我們就需要了解Android的事件分發(fā)機(jī)制。
Android的觸摸事件分發(fā)過程由三個(gè)很重要的方法來共同完成:dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent。我先將這三個(gè)方法大體的介紹一下。
•public boolean dispatchTouchEvent(MotionEvent ev)
用來進(jìn)行事件的分發(fā)。如果事件能夠傳遞給當(dāng)前View,那么此方法一定會(huì)被調(diào)用,返回結(jié)果受當(dāng)前View的onTouchEvent和下級(jí)View的dispatchTouchEvent方法的影響,表示是否消耗當(dāng)前事件。ACTION_DOWN的dispatchTouchEvent()返回true,后續(xù)事件(ACTION_MOVE、ACTION_UP)會(huì)再傳遞,如果返回false,dispatchTouchEvent()就接收不到ACTION_UP、ACTION_MOVE。簡單的說,就是當(dāng)dispatchTouchEvent在進(jìn)行事件分發(fā)的時(shí)候,只有前一個(gè)action返回true,才會(huì)觸發(fā)后一個(gè)action。
•public boolean onInterceptTouchEvent(MotionEvent event)
這個(gè)方法是在dispatchTouchEvent方法中調(diào)用的,用來攔截某個(gè)事件的。如果當(dāng)前View攔截了某個(gè)事件,那么在同一個(gè)事件序列中,此方法不會(huì)被再次調(diào)用,返回的結(jié)果表示是否攔截當(dāng)前事件。它是ViewGroup提供的方法,默認(rèn)返回false。
•public boolean onTouchEvent(MotionEvent event)
在dispatchTouchEvent方法中調(diào)用,用來處理點(diǎn)擊事件,返回結(jié)果表示是否消耗掉當(dāng)前事件(true表示消耗,false表示不消耗),如果不消耗,則在同一個(gè)事件序列中,當(dāng)前View無法再次接收到事件。View和ViewGroup都有該方法,View默認(rèn)返回true,表示消費(fèi)了這個(gè)事件。
View里,有兩個(gè)回調(diào)函數(shù) :
public boolean dispatchTouchEvent(MotionEvent ev);
public boolean onTouchEvent(MotionEvent ev);
ViewGroup里,有三個(gè)回調(diào)函數(shù) :
public boolean dispatchTouchEvent(MotionEvent ev);
public boolean onInterceptTouchEvent(MotionEvent ev);
public boolean onTouchEvent(MotionEvent ev);
上述三個(gè)方法中有什么區(qū)別和關(guān)系呢?下面用一段偽代碼表示:
public boolean dispatchTouchEvent(MotionEvent ev) { boolean consume = false; if(onInterceptTouchEvent(ev)){ consume = onTouchEvent(ev); } else { consume = child.dispatchTouchEvent(ev); } return consume; }
通過上面的偽代碼大家可能對(duì)點(diǎn)擊事件的傳遞規(guī)則有了更清楚的認(rèn)識(shí),即:對(duì)于一個(gè)根ViewGroup來說,點(diǎn)擊事件產(chǎn)生后,首先會(huì)傳遞給它,這時(shí)它的dispatchTouchEvent就會(huì)被調(diào)用,如果這個(gè)ViewGroup的onInterceptTouchEvent方法返回true表示它要攔截此事件,接著這個(gè)事件就會(huì)交給這個(gè)ViewGroup處理,即它的onTouchEvent方法就會(huì)被調(diào)用;如果這個(gè)ViewGroup的onInterceptTouchEvent方法返回false,就表示它不攔截此事件,這是當(dāng)前事件就會(huì)繼續(xù)傳遞給它的子元素,接著子元素的dispatchTouchEvent方法就會(huì)被調(diào)用,如此反復(fù)直到事件被最終處理。
下面的幾張圖參考自[eoe]:
•圖一:ACTION_DOWN都沒被消費(fèi)
•圖二(一):ACTION_DOWN被View消費(fèi)了
•圖二(二):后續(xù)ACTION_MOVE和UP在不被攔截的情況下都會(huì)去找VIEW
•圖三:后續(xù)的被攔截了
•圖四:ACTION_DOWN一開始就被攔截
View事件分發(fā)源碼分析:
•dispatchTouchEvent方法:
public boolean dispatchTouchEvent(MotionEvent event) { if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event)) { return true; } return onTouchEvent(event); }
如果mOnTouchListener != null,(mViewFlags&ENABLED_MASK)==ENABLED和mOnTouchListener.onTouch(this, event)這三個(gè)條件都為真,就返回true,否則就去執(zhí)行onTouchEvent(event)方法并返回。
總結(jié)下來onTouch能夠得到執(zhí)行需要兩個(gè)前提條件(都滿足):
1.設(shè)置了OnTouchListener
2.控件是enable狀態(tài)
而onTouchEvent能夠得到執(zhí)行滿足以下三個(gè)條件任意一個(gè)即可:
1.沒有設(shè)置OnTouchListener
2.控件不是enable狀態(tài)
3.onTouch返回false
再來看一下dispatchTouchEvent的返回值,它其實(shí)受onTouch和onTouchEvent函數(shù)的返回值控制,也就是說touch事件被成功消費(fèi)返回true,它也就返回true,說明分發(fā)成功,此后的事件序列也會(huì)在此被分發(fā),而如果返回false,則認(rèn)為分發(fā)失敗,此后的事件序列就不再分發(fā)下去了。
•onTouchEvent方法:
if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { ... return true; }
View的onTouchEvent默認(rèn)都會(huì)消耗掉事件(該方法返回true),除非它是不可點(diǎn)擊的(clickable和longClickable同時(shí)為false)。并且View的longClickable默認(rèn)為false,clickable屬性要分情況,比如Button默認(rèn)為true,TextView、ImageView默認(rèn)為false。
public boolean performClick() { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); if (mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); mOnClickListener.onClick(this); return true; } return false; }
這不就是我們熟悉的OnClickListener嗎,它原來是在onTouchEvent中被調(diào)用的。只要mOnClickListener不是null,就會(huì)去調(diào)用它的onClick方法。
總結(jié)下來onClick能夠得到執(zhí)行需要兩個(gè)前提條件(都滿足):
1.可以執(zhí)行到onTouchEvent
2.設(shè)置了OnClickListener
整個(gè)View的事件轉(zhuǎn)發(fā)流程是:
dispatchEvent->setOnTouchListener->onTouchEvent->setOnClickListener
最后還有一個(gè)問題,setOnLongClickListener和setOnClickListener是否只能執(zhí)行一個(gè)?
答:不是的,只要setOnLongClickListener中的onClick返回false,則兩個(gè)都會(huì)執(zhí)行;返回true則會(huì)屏蔽setOnClickListener。
ViewGroup事件分發(fā)源碼分析:
•dispatchTouchEvent方法:
... if (disallowIntercept || !onInterceptTouchEvent(ev)) { ev.setAction(MotionEvent.ACTION_DOWN); final int scrolledXInt = (int) scrolledXFloat; final int scrolledYInt = (int) scrolledYFloat; final View[] children = mChildren; final int count = mChildrenCount; for (int i = count - 1; i >= 0; i--) { final View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { child.getHitRect(frame); if (frame.contains(scrolledXInt, scrolledYInt)) { final float xc = scrolledXFloat - child.mLeft; final float yc = scrolledYFloat - child.mTop; ev.setLocation(xc, yc); child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; if (child.dispatchTouchEvent(ev)) { // Event handled, we have a target now. mMotionTarget = child; return true; } } } } }
兩種可能會(huì)進(jìn)入if代碼段(即事件被分發(fā)給子View):
1、當(dāng)前不允許攔截,即disallowIntercept = true.
2、當(dāng)前沒有攔截,即onInterceptTouchEvent(ev)返回false.
注:disallowIntercept是指是否禁用掉事件攔截的功能,默認(rèn)是false,可以通過ViewGroup.requestDisallowInterceptTouchEvent(boolean)進(jìn)行設(shè)置;而onInterceptTouchEvent(ev)可以進(jìn)行復(fù)寫。
進(jìn)入if代碼段后,通過一個(gè)for循環(huán),遍歷當(dāng)前ViewGroup下的所有子View,判斷當(dāng)前遍歷的View是不是正在點(diǎn)擊的View,如果是的話就會(huì)調(diào)用該View的dispatchTouchEvent,就進(jìn)入了View的事件分發(fā)流程了,上面有講。當(dāng)child.dispatchTouchEvent(ev)返回true,則為mMotionTarget=child;然后return true,說明ViewGroup的dispatchTouchEvent返回值受childView的dispatchTouchEvent返回值影響,子view事件分發(fā)成功,ViewGroup的事件分發(fā)才成功,此后的事件序列也會(huì)在此分發(fā)(從上面知:子view的clickable或longClickable為true都能分發(fā)成功),而如果ViewGroup事件分發(fā)失敗或者沒有找到子View(點(diǎn)擊空白位置),則會(huì)走到它的onTouchEvent,以后的事件序列也不會(huì)分發(fā)下去,直接走onTouchEvent。
整個(gè)ViewGroup的事件轉(zhuǎn)發(fā)流程是:
dispatchEvent->onInterceptTouchEvent->child.dispatchEvent->(setOnTouchListener->onTouchEvent)
上面的總結(jié)都是基于:如果沒有攔截;那么如何攔截呢?
•onInterceptTouchEvent
public boolean onInterceptTouchEvent(MotionEvent ev) { return false; }
代碼很簡單,只有一句,即返回false,ViewGroup默認(rèn)是不攔截的。如果你需要攔截,只要return true就行了,這樣該事件就不會(huì)往子View傳遞了,并且如果你在DOWN return true ,則DOWN,MOVE,UP子View都不會(huì)捕獲到事件;如果你在MOVE return true , 則子View在MOVE和UP都不會(huì)捕獲到事件。
如何不被攔截:
如果ViewGroup的onInterceptTouchEvent(ev) 當(dāng)ACTION_MOVE時(shí)return true ,即攔截了子View的MOVE以及UP事件;此時(shí)子View希望依然能夠響應(yīng)MOVE和UP時(shí)該咋辦呢?
答:onInterceptTouchEvent是定義在ViewGroup中的,子View無法修改。Android給我們提供了一個(gè)方法:requestDisallowInterceptTouchEvent(boolean) 用于設(shè)置是否允許攔截,我們在子View的dispatchTouchEvent中直接這么寫:
@Override public boolean dispatchTouchEvent(MotionEvent event) { getParent().requestDisallowInterceptTouchEvent(true); int action = event.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: Log.e(TAG, "dispatchTouchEvent ACTION_DOWN"); break; case MotionEvent.ACTION_MOVE: Log.e(TAG, "dispatchTouchEvent ACTION_MOVE"); break; case MotionEvent.ACTION_UP: Log.e(TAG, "dispatchTouchEvent ACTION_UP"); break; default: break; } return super.dispatchTouchEvent(event); }
getParent().requestDisallowInterceptTouchEvent(true); 這樣即使ViewGroup在MOVE的時(shí)候return true,子View依然可以捕獲到MOVE以及UP事件。
注:如果ViewGroup在onInterceptTouchEvent(ev) ACTION_DOWN里面直接return true了,那么子View是沒有辦法的捕獲事件的!
總結(jié)
關(guān)于代碼流程上面已經(jīng)總結(jié)過了~
1、如果ViewGroup找到了能夠處理該事件的View,則直接交給子View處理,自己的onTouchEvent不會(huì)被觸發(fā);
2、可以通過復(fù)寫onInterceptTouchEvent(ev)方法,攔截子View的事件(即return true),把事件交給自己處理,則會(huì)執(zhí)行自己對(duì)應(yīng)的onTouchEvent方法
3、子View可以通過調(diào)用getParent().requestDisallowInterceptTouchEvent(true); 阻止ViewGroup對(duì)其MOVE或者UP事件進(jìn)行攔截;
好了,那么實(shí)際應(yīng)用中能解決哪些問題呢?
比如你在ScrollView中嵌套了一個(gè)EditText,當(dāng)EditText中文字內(nèi)容太多超出范圍時(shí),你想上下滑動(dòng)使EditText中文字滾動(dòng)出來,卻發(fā)現(xiàn)滾動(dòng)的是ScrollView。這時(shí)我們設(shè)置EditText的onTouch事件,在onTouch中設(shè)置不讓ScrollView攔截我的事件,最后在UP時(shí)把狀態(tài)改回去。
@Override public boolean onTouch(View view, MotionEvent motionEvent) { if ((view.getId() == R.id.tousuContentEditText && canVerticalScroll(tousuContentEditText))) { view.getParent().requestDisallowInterceptTouchEvent(true); if (motionEvent.getAction() == MotionEvent.ACTION_UP) { view.getParent().requestDisallowInterceptTouchEvent(false); } } return false; } private boolean canVerticalScroll(EditText editText) { int scrollY = editText.getScrollY(); int scrollRange = editText.getLayout().getHeight(); int scrollExtent = editText.getHeight() - editText.getCompoundPaddingTop() - editText.getCompoundPaddingBottom(); int scrollDifference = scrollRange - scrollExtent; if (scrollDifference == 0) { return false; } return (scrollY > 0) || (scrollY < scrollDifference - 1); }
以上就是本文的全部內(nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Android動(dòng)態(tài)加載Activity原理詳解
這篇文章主要介紹了Android動(dòng)態(tài)加載Activity原理詳解的相關(guān)資料,需要的朋友可以參考下2016-04-04Android實(shí)現(xiàn)網(wǎng)易云音樂的旋轉(zhuǎn)專輯View
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)網(wǎng)易云音樂的旋轉(zhuǎn)專輯View,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-11-11Android 應(yīng)用更換皮膚實(shí)現(xiàn)方法
本文主要介紹Android 應(yīng)用更換皮膚,Android應(yīng)用如果想更換皮膚這里幫大家整理了相關(guān)資料,有需要的小伙伴可以參考下2016-08-08Android RecyclerView緩存復(fù)用原理解析
RecyclerView是Android一個(gè)更強(qiáng)大的控件,其不僅可以實(shí)現(xiàn)和ListView同樣的效果,還有優(yōu)化了ListView中的各種不足。其可以實(shí)現(xiàn)數(shù)據(jù)縱向滾動(dòng),也可以實(shí)現(xiàn)橫向滾動(dòng)(ListView做不到橫向滾動(dòng))。接下來講解RecyclerView的用法2022-11-11Android常用控件ImageSwitcher使用方法詳解
這篇文章主要為大家詳細(xì)介紹了Android常用控件ImageSwitcher的使用方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08android多媒體音樂(MediaPlayer)播放器制作代碼
這篇文章主要為大家詳細(xì)介紹了android多媒體音樂(MediaPlayer)播放器的制作相關(guān)代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-02-02Android實(shí)現(xiàn)圖像灰度化、線性灰度變化和二值化處理方法
這篇文章主要介紹了Android實(shí)現(xiàn)圖像灰度化、線性灰度變化和二值化處理方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-10-10