本文主要參照郭霖大神的博客坊夫,配合食用效果更佳哦
最近在寫學長布置的作業(yè)的時候,為了模仿QQ的滑動菜單功能荷腊,走了點彎路自己寫了個slidingmenu出來,結(jié)果一運行急凰,爆炸了女仰。外層的HorizontalScrollView對觸摸事件異常敏感,為了解決這個問題抡锈,花了幾天的時間研究了下Android的事件分發(fā)過程疾忍,下面是我的一點心得,歡迎大家指正~
注:本文主要是從Android源碼角度解釋滑動沖突的產(chǎn)生原因床三,想要直接得到滑動沖突的解決辦法的一罩,請出門左拐,百度谷歌
View與ViewGroup
首先撇簿,滑動事件必須有個載體聂渊,你得有個給你滑你才可以滑對不差购?那么Android里,這個東西就是View和ViewGroup
因此在分析滑動沖突之前汉嗽,我們先對View與ViewGroup涉及到的有關(guān)滑動方面的方法做以下列舉
View涉及到的方法:
- public boolean dispatchTouchEvent(MotionEvent ev)
用于對觸摸事件的分發(fā)欲逃,一個觸摸事件到底交給哪一個View來處理,由這個方法分配饼暑,源碼是
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
- public boolean onTouch(View v, MotionEvent event)
如果想讓該View優(yōu)先執(zhí)行某一個觸摸事件稳析,可以用view.setOnTouchListenrer()
重寫onTouch方法就行了。
從dispatchTouchEvent的源碼里我們可以看到
If(view實現(xiàn)了touchListener0 && 該控件是可以點的 && onTouch的返回值是true)
該事件就返回true分發(fā)完畢弓叛,不會再執(zhí)行onTouchEvent中的代碼彰居,而如果onTouch返回值為false,則將執(zhí)行onTouchEvent中的代碼
那么我們可以將onTouch返回true理解為已經(jīng)消費了該事件邪码,返回false理解為還沒有消費該事件裕菠,該次觸摸事件可以被繼續(xù)響應
因此我們可以把這個方法看做谷歌暴露給用戶的一個接口,可以借此屏蔽該控件自帶的觸摸事件的邏輯判斷闭专。
-
public boolean onTouchEvent(MotionEvent event)
當觸摸事件被分配給某個View處理時奴潘,如果這個View沒有實現(xiàn)onTouch,則將該事件交給這個事件的onTouchEvent處理影钉,這個方法可以理解為該View對觸摸事件的默認處理方法
這里涉及到一個touch事件的層級傳遞的知識點画髓。我們知道,一個touch事件至少由一次Down事件平委,任意個Move事件奈虾,還有一次Up事件,順序組成廉赔,只有當前面的事件返回true時肉微,后面的事件才能正常運行,否則終止執(zhí)行蜡塌。所以我們一般只在Up中返回false碉纳,其它的情況下返回true
注意:千萬不要把onTouchEvent返回值的作用和onTouch返回值的作用混淆了。如果你去百度搜onTouchEvent返回值馏艾,那些大佬們都說什么true就是消費了false就是沒消費劳曹,那都是對onTouch的解釋。兩者并沒有任何關(guān)系....
關(guān)于這一點我是看郭霖大神的博客才徹底弄懂琅摩,之前困擾了我好久好久铁孵,還是郭霖大法好啊~
ViewGroup涉及到的方法:
view的三個方法ViewGroup都有涉及,onTouchEvent和onTouch兩個方法與View的差不多房资,ondispatchTouchEvent與View的并不相同蜕劝,代碼量多而且涉及到了Android的事件分發(fā)機制,分析它的源碼上郭霖大神已經(jīng)講的很詳細了。不過它還有一個獨特的方法也是我們解決滑動沖突的一個重要方法
- public boolean onInterceptTouchEvent(MotionEvent ev)
你去看源碼的話會發(fā)現(xiàn)它有著大段大段的注釋和只返回了一個false的奇葩特性熙宇,我們可以把它理解為谷歌留給我們用來幫助ondispatchTouchEvent處理分發(fā)事件的一個輔助方法鳖擒,它具有攔截觸摸事件的功能,使其它的View不能響應該事件烫止,默認返回false
由于在這個方法里我們可以拿到觸摸事件的event蒋荚,它的寫法跟onTouchEvent很相似。
因此我們可以通過判斷手勢的邏輯操作來決定
是++return true++攔截這個觸摸事件交給自己的onTouchEvent處理好呢
還是++return false++不攔截交給子view處理
Android事件分發(fā)機制
- 相關(guān)源碼將在文章最后貼出來*
再從Android的事件分發(fā)機制說起吧馆蠕,當用戶對手機進行了一次觸摸事件時期升,Android會執(zhí)行各種方法,最終將該事件交給Activity互躬,Activity又交給它的布局ViewGroup處理播赁。想了解這其中奧秘的,請點這里
ViewGroup拿到這個事件之后吼渡,它會交給ondispatchTouchEvent去分發(fā)該事件容为,通過讀源碼我們可以看到在判斷一切觸摸事件的 ”起手招式” Down手勢時會進行如下判斷
if (disallowIntercept || !onInterceptTouchEvent(ev))
disallowIntercept這個參數(shù)可以用 requestDisallowInterceptTouchEvent 進行修改,默認為false寺酪,因此坎背,我們重點關(guān)注后一個參數(shù)。誒寄雀?這不就是上節(jié)介紹的ViewGroup的獨特方法嗎得滤?
通過讀源碼我們可以看到,當 onInterceptTouchEvent(ev) 返回true時,ViewGroup的ondispatchTouchEvent 將不再進行事件傳遞,而是攔截該事件自行處理它
返回false時纺腊,將判斷用戶所觸摸的是哪一個子View,并調(diào)用該View的ondispatchTouchEvent秧廉,讓子View再分發(fā)一次,如果子View是繼承自VIew的控件并且沒有實現(xiàn)onTouch 方法,聯(lián)系上文可以知道將由子View的 onTouchEvent 去處理該事件。
下面是源碼調(diào)用子view的 dispatchTouchEvent(ev) 方法的部分
if (child.dispatchTouchEvent(ev)) {
mMotionTarget = child;
return true;
}
根據(jù)子view的分發(fā)結(jié)果來判斷該次事件分發(fā)是否結(jié)束
那么皂股,整個Android事件分發(fā)流程大致就是,Activity拿到TouchEvent命黔,交給最外層布局的dispatchTouchEvent去判斷該事件是由誰來處理。期間會調(diào)用onInterceptTouchEvent判斷自己該不該攔截這個觸摸事件
如果是該布局自己處理就斤,則調(diào)用onTouchEvent處理該事件
如果是子view處理悍募,則交給子view的dispatchTouchEvent繼續(xù)判斷
現(xiàn)在再來考慮如何解決滑動沖突是不是就有頭緒了?由于我當初是HorizontalScrollView嵌套RecyclerView洋机,一個橫著滑一個豎著滑坠宴。在沒有重寫 onInterceptTouchEvent(ev) 這個方法時,由于默認的返回值為false绷旗,Android先給HorizontalScollView處理滑動事件喜鼓,再給RecyclerView處理滑動事件副砍,相當于一個滑動事件被處理了兩次,只要在豎著滑動的時候稍微往橫軸偏移一點點就會觸發(fā)橫向滑動庄岖,那么我們在 onInterceptTouchEvent(ev) 里處理ACTION_MOVE的情況時添加一個判斷
case MotionEvent.ACTION_MOVE:
final int deltaX = lastX - currentX;
final int deltaY = lastY - currentY;
if (Math.abs(deltaX) > Math.abs(deltaY)){
return true;
}else {
//既然已經(jīng)交給子view處理這次事件了豁翎,那之后就不要再攔截了吧
requestDisallowInterceptTouchEvent(true);
return false;
}
世界就變得和諧了~
源碼
- ViewGroup中的dispatchTouchEvent方法
public boolean dispatchTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
final float xf = ev.getX();
final float yf = ev.getY();
final float scrolledXFloat = xf + mScrollX;
final float scrolledYFloat = yf + mScrollY;
final Rect frame = mTempRect;
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (action == MotionEvent.ACTION_DOWN) {
if (mMotionTarget != null) {
mMotionTarget = null;
}
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)) {
mMotionTarget = child;
return true;
}
}
}
}
}
}
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
(action == MotionEvent.ACTION_CANCEL);
if (isUpOrCancel) {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
final View target = mMotionTarget;
if (target == null) {
ev.setLocation(xf, yf);
if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
}
return super.dispatchTouchEvent(ev);
}
if (!disallowIntercept && onInterceptTouchEvent(ev)) {
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
ev.setAction(MotionEvent.ACTION_CANCEL);
ev.setLocation(xc, yc);
if (!target.dispatchTouchEvent(ev)) {
}
mMotionTarget = null;
return true;
}
if (isUpOrCancel) {
mMotionTarget = null;
}
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
ev.setLocation(xc, yc);
if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
mMotionTarget = null;
}
return target.dispatchTouchEvent(ev);
}
這是我第一次寫博客,肯定有很多不足的地方隅忿,歡迎大家替我指點指點心剥,小生先行謝過。