1.前言
View的事件分發(fā)機(jī)制是面試的要點(diǎn),也是必須要吃透的基礎(chǔ)知識(shí)。雖然平時(shí)用到的地方不是那么頻繁,但是一旦要用,如果這個(gè)不夠扎實(shí),就會(huì)卡手。就獨(dú)立一篇來(lái)整理,日常開(kāi)發(fā)中有發(fā)現(xiàn)有要注意的事情也在此補(bǔ)充歸納。
??與View做交互操作會(huì)產(chǎn)生事件,事件起點(diǎn)是誰(shuí),系統(tǒng)是如何將一個(gè)事件從起點(diǎn)傳到目標(biāo)View的。帶著疑問(wèn)去探索。
2.點(diǎn)擊事件的傳遞規(guī)則
起始與流通:
從Android應(yīng)用層看,事件起于Activity,Activity也有dispatchTouchEvent,onTouchEvent方法。
??Activity會(huì)傳遞給Window,Window會(huì)傳給頂層View,頂層View分發(fā)給子View,一級(jí)級(jí)向下分發(fā),如果某層不消費(fèi),則返給父層處理。
Activity -》 Window ->Decor View -> ContentView
??形象地來(lái)看,就如上級(jí)分派任務(wù)給下級(jí)。
三個(gè)核心方法:
- public boolean dispatchTouchEvent(MotionEvent ev)
View都有。事件能傳給當(dāng)前View,則一定會(huì)調(diào)用.在這個(gè)方法會(huì)根據(jù)不同條件去調(diào)用onInterceptTouchEvent和onTouchEvent.返回值表示當(dāng)前view是否消費(fèi)了事件。
true消費(fèi)了;false沒(méi)消費(fèi),事件交給父View處理.所有的父View都不處理,則傳回Activity,在Activity中消亡。 - public boolean onInterceptTouchEvent(MotionEvent ev)
僅ViewGroup有。返回值表示是否攔截事件。默認(rèn)false不攔截,攔截則交給onTouch處理.不攔截交給子View處理(調(diào)用子View的dispatchTouchEvent())。 - public boolean onTouchEvent(MotionEvent ev)
View都有。返回值表示對(duì)事件的處理。true表示處理了,false表示不處理。
改變這三個(gè)方法的返回值可以改變事件分發(fā)傳遞的過(guò)程,需要注意這樣做默認(rèn)的父類(lèi)處理邏輯就不會(huì)執(zhí)行了。
onTouchListener與方法優(yōu)先級(jí):
View可以設(shè)置onTouchListener,這個(gè)監(jiān)聽(tīng)能干擾View的事件分發(fā)過(guò)程。它會(huì)先于onTounchEvent執(zhí)行,也就是它的優(yōu)先級(jí)高。
??優(yōu)先級(jí)onTouchListener > onTouchEvent > onClickListener(具體View交互回調(diào))
??onTouchListener中方法onTouch默認(rèn)返回false,onTouchEvent會(huì)調(diào)用;true,不會(huì)調(diào)用OnTouchEvent,認(rèn)為是當(dāng)前View消費(fèi)事件,即dispatchTouchEvent 返回true。
??無(wú)論onTouch怎樣,dispatchTouchEvent最終都會(huì)調(diào)用,可以認(rèn)為dispatchTouchEvent的優(yōu)先級(jí)要高于onTouchListener。
View的dispatchTouchEvent()源碼直觀地體現(xiàn)了這一點(diǎn).
事件與事件序列:
一個(gè)事件序列指手指從接觸屏幕一刻起到離開(kāi)屏幕這個(gè)過(guò)程產(chǎn)生的一系列事件。事件序列完整才會(huì)有相應(yīng)的View交互回調(diào)方法執(zhí)行。有ACTION_DOWN起。。。ACTION_UP止。
??正常情況下,一個(gè)事件序列交由一個(gè)View處理。一些特殊的手段可以讓兩個(gè)View都響應(yīng),比如在一個(gè)View的onTouchEvent中強(qiáng)行傳遞給其他View。
ViewGroup攔截ACTION_DOWN,ACTION_Down會(huì)傳給自身的onTouchEvent.事件序列中之后事件不會(huì)再向下分發(fā),傳給自身的onTouchEvent.
ViewGroup攔截除ACTION_DOWN之外的事件,會(huì)產(chǎn)生一個(gè)ACTION_CANCEL的事件分發(fā)給子View.事件序列中之后的事件傳給自身的onTouchEvent.此時(shí)mFirstTouchTarget為null,dispatchTouchEvent()不會(huì)調(diào)用onIntercepte()判斷是否攔截,直接將intercepted賦值true.
View如果不消耗ACTION_DOWN事件(在onTounch或dispatchTouchEvent返回false),ACTION_DOWN事件和事件序列中之后的事件都交給父View處理.
View如果不消耗除ACTION_DOWN事件之外的事件,當(dāng)前事件交給父View處理,之后還是會(huì)繼續(xù)接收到事件序列中之后的事件.
子View用requestDisallowInterceptTouchEvent設(shè)置標(biāo)記量mGroupFlags是否允許父View攔截事件,默認(rèn)值是允許.每當(dāng)有ACTION_DOWN事件來(lái)時(shí)會(huì)重置此標(biāo)記量允許.所以面對(duì)ACTION_DOWN事件時(shí),ViewGroup總會(huì)調(diào)用自己的InterceptTouchEvent方法來(lái)詢(xún)問(wèn)自己是否要攔截事件。當(dāng)此標(biāo)記量設(shè)置成不允許的時(shí)候,除ACTION_DOWN事件,dispatchTouchEvent()不會(huì)調(diào)用onIntercepte()判斷是否攔截,直接將intercepted賦值false.
longClickable與clickable對(duì)View事件消耗的影響
View的onTouchEvent默認(rèn)都會(huì)消耗事件,除非是不可點(diǎn)擊的,即longClickable與clickable都為false.
??View的enable不影響事件分發(fā).點(diǎn)擊事件不會(huì)響應(yīng).
源碼分析可以參考:
http://blog.csdn.net/weixin_37077539/article/details/54895485
可點(diǎn)擊這個(gè)屬性對(duì)View的事件消費(fèi)有影響,在處理View的事件分發(fā)邏輯,記得檢查View是否可點(diǎn)擊.
dispatchTouchEvent()源碼分析
原理在源碼體現(xiàn).
傳送門(mén):http://www.itdecent.cn/p/93a060053cbc
3.滑動(dòng)沖突實(shí)訓(xùn)
場(chǎng)景一:外部滑動(dòng)方向與內(nèi)部不一致。(外橫內(nèi)豎,viewpage已做外部攔截)
A:外部攔截法(常用)
重寫(xiě)父容器的onInterceptTouchEvent().當(dāng)事件為ACTION_MOVE的時(shí)候。根據(jù)條件判斷外部是否要攔截。ACTION_DOWN只在外層滑動(dòng)將結(jié)束,優(yōu)化滑動(dòng)體驗(yàn)的時(shí)候才加上.
boolean intercepted = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
intercepted = false;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
intercepted = true;
}
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
intercepted = Math.abs(deltaX) > Math.abs(deltaY);
break;
case MotionEvent.ACTION_UP:
//給子View響應(yīng)點(diǎn)擊事件
intercepted = false;
break;
}
mLastX = x;
mLastY = y;
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
B:內(nèi)部攔截法
內(nèi)部攔截要重寫(xiě)父View的onInterceptTouchEvent與子View的dispatchTouchEvent().利用的是子View可以用getParent.requestDisallowInterceptTouchEvent()方法改變父View的攔截標(biāo)記位mGroupFlags,達(dá)到事件按照業(yè)務(wù)規(guī)則給自己或者父View處理的目的.
使用這個(gè)方法需要注意
1.父View不能將ACTION_DOWN攔截掉,ACTION_DOWN一旦被攔截,子View得不到事件序列后續(xù)的事件.
2.較外部攔截的效果需要處理內(nèi)部滑動(dòng)后不再將事件給父View.
//內(nèi)部攔截父View需要的代碼
mLastX = (int) ev.getRawX();
mLastY = (int) ev.getRawY();
if (MotionEvent.ACTION_DOWN == ev.getAction()) {
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
return true;
}
return false;
} else {
return true;
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
float x = ev.getRawX();
float y = ev.getRawY();
if (MotionEvent.ACTION_DOWN == ev.getAction()) {
getParent().requestDisallowInterceptTouchEvent(true);
slop = false;
}
if (MotionEvent.ACTION_MOVE == ev.getAction()) {
if (Math.abs(mLastY - y) < Math.abs(mLastX - x)) {
if (!slop) getParent().requestDisallowInterceptTouchEvent(false);
} else {
int touchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
if (Math.abs(y - mLastY) > touchSlop) {
//已經(jīng)開(kāi)始了豎向滑動(dòng)
slop = true;
}
}
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(ev);
}
場(chǎng)景二:外部滑動(dòng)方向與內(nèi)部一致。
同場(chǎng)景一處理方式,區(qū)別在判斷條件由業(yè)務(wù)定,看業(yè)務(wù)什么時(shí)候需要交給外層,什么時(shí)候需要交給內(nèi)層。
場(chǎng)景三:場(chǎng)景一場(chǎng)景二交替同時(shí)出現(xiàn)
剝繭法層層解決.
可以加一些回彈或過(guò)渡效果使滑動(dòng)沖突處理的過(guò)程顯得更加圓滑。
后記
分發(fā)機(jī)制比較靈活,比較細(xì)膩,花我老長(zhǎng)時(shí)間去分析理解鞏固,源碼風(fēng)格不怎么利于閱讀...233.有些情況套用公式是不行的,要多動(dòng)手結(jié)合真實(shí)項(xiàng)目代碼思考練習(xí).
如果碰到不懂的地方,翻源碼思考是一個(gè)比較好的選擇.
Action_Cancle事件怎么產(chǎn)生與如何影響分發(fā)的,待做.
接下來(lái)會(huì)想對(duì)項(xiàng)目中RecyclerView左滑的實(shí)例進(jìn)行思考探究.在這之前也需要有一定的View的繪制機(jī)制基礎(chǔ).