https://blog.csdn.net/guolin_blog/article/details/9097463
https://blog.csdn.net/guolin_blog/article/details/9153747
事件分發(fā)流程圖
View事件分發(fā)
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Log.d("TAG", "onClick execute");
}
});
button.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d("TAG", "onTouch execute, action " + event.getAction());
return false;
}
});
onTouch是優(yōu)先于onClick執(zhí)行的杰捂,并且onTouch執(zhí)行了兩次,一次是ACTION_DOWN棋蚌,一次是ACTION_UP(你還可能會(huì)有多次ACTION_MOVE的執(zhí)行嫁佳,如果你手抖了一下)。因此事件傳遞的順序是先經(jīng)過(guò)onTouch附鸽,再傳遞到onClick脱拼。
onTouch方法是有返回值的,這里我們返回的是false坷备,如果我們嘗試把onTouch方法里的返回值改成true熄浓,再運(yùn)行一次,onClick方法不再執(zhí)行了省撑!你可以先理解成onTouch方法返回true就認(rèn)為這個(gè)事件被onTouch消費(fèi)掉了赌蔑,因而不會(huì)再繼續(xù)向下傳遞。
原理
首先你需要知道一點(diǎn)竟秫,只要你觸摸到了任何一個(gè)控件娃惯,就一定會(huì)調(diào)用該控件的dispatchTouchEvent方法。那當(dāng)我們?nèi)c(diǎn)擊按鈕的時(shí)候肥败,就會(huì)去調(diào)用Button類(lèi)里的dispatchTouchEvent方法趾浅,可是你會(huì)發(fā)現(xiàn)Button類(lèi)里并沒(méi)有這個(gè)方法,那么就到它的父類(lèi)TextView里去找一找馒稍,你會(huì)發(fā)現(xiàn)TextView里也沒(méi)有這個(gè)方法皿哨,那沒(méi)辦法了,只好繼續(xù)在TextView的父類(lèi)View里找一找纽谒,這個(gè)時(shí)候你終于在View里找到了這個(gè)方法证膨,示意圖如下:
View中dispatchTouchEvent方法的源碼:
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
現(xiàn)在我們可以結(jié)合前面的例子來(lái)分析一下了,首先在dispatchTouchEvent中最先執(zhí)行的就是onTouch方法鼓黔,因此onTouch肯定是要優(yōu)先于onClick執(zhí)行的央勒,也是印證了剛剛的打印結(jié)果。而如果在onTouch方法里返回了true澳化,就會(huì)讓dispatchTouchEvent方法直接返回true崔步,不會(huì)再繼續(xù)往下執(zhí)行。而打印結(jié)果也證實(shí)了如果onTouch返回true缎谷,onClick就不會(huì)再執(zhí)行了刷晋。
onTouchEvent的源碼
public boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
// 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 & PREPRESSED) != 0;
if ((mPrivateFlags & 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 (!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) {
mPrivateFlags |= PRESSED;
refreshDrawableState();
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
break;
case MotionEvent.ACTION_DOWN:
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPrivateFlags |= PREPRESSED;
mHasPerformedLongPress = false;
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
break;
case MotionEvent.ACTION_CANCEL:
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
removeTapCallback();
break;
case MotionEvent.ACTION_MOVE:
final int x = (int) event.getX();
final int y = (int) event.getY();
// Be lenient about moving outside of buttons
int slop = mTouchSlop;
if ((x < 0 - slop) || (x >= getWidth() + slop) ||
(y < 0 - slop) || (y >= getHeight() + slop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
// Need to switch from pressed to not pressed
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
}
}
break;
}
return true;
}
return false;
}
首先在第14行我們可以看出,如果該控件是可以點(diǎn)擊的就會(huì)進(jìn)入到第16行的switch判斷中去,而如果當(dāng)前的事件是抬起手指眼虱,則會(huì)進(jìn)入到MotionEvent.ACTION_UP這個(gè)case當(dāng)中喻奥。在經(jīng)過(guò)種種判斷之后,會(huì)執(zhí)行到第38行的performClick()方法捏悬,那我們進(jìn)入到這個(gè)方法里瞧一瞧:
public boolean performClick() {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
if (mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
mOnClickListener.onClick(this);
return true;
}
return false;
}
public void setOnClickListener(OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
mOnClickListener = l;
}
重要的知識(shí)點(diǎn) touch事件的層級(jí)傳遞
我們都知道如果給一個(gè)控件注冊(cè)了touch事件撞蚕,每次點(diǎn)擊它的時(shí)候都會(huì)觸發(fā)一系列的ACTION_DOWN,ACTION_MOVE过牙,ACTION_UP等事件甥厦。這里需要注意,如果你在執(zhí)行ACTION_DOWN的時(shí)候返回了false寇钉,后面一系列其它的action就不會(huì)再得到執(zhí)行了刀疙。簡(jiǎn)單的說(shuō),就是當(dāng)dispatchTouchEvent在進(jìn)行事件分發(fā)的時(shí)候扫倡,只有前一個(gè)action返回true谦秧,才會(huì)觸發(fā)后一個(gè)action。
前面的例子中撵溃,明明在onTouch事件里面返回了false疚鲤,ACTION_DOWN和ACTION_UP不是都得到執(zhí)行了嗎?其實(shí)你只是被假象所迷惑了缘挑,讓我們仔細(xì)分析一下集歇,在前面的例子當(dāng)中,我們到底返回的是什么语淘。
參考著我們前面分析的源碼诲宇,首先在onTouch事件里返回了false,就一定會(huì)進(jìn)入到onTouchEvent方法中惶翻,然后我們來(lái)看一下onTouchEvent方法的細(xì)節(jié)姑蓝。由于我們點(diǎn)擊了按鈕,就會(huì)進(jìn)入到第14行這個(gè)if判斷的內(nèi)部维贺,然后你會(huì)發(fā)現(xiàn)它掂,不管當(dāng)前的action是什么巴帮,最終都一定會(huì)走到第89行溯泣,返回一個(gè)true。
是不是有一種被欺騙的感覺(jué)榕茧?明明在onTouch事件里返回了false垃沦,系統(tǒng)還是在onTouchEvent方法中幫你返回了true。就因?yàn)檫@個(gè)原因用押,才使得前面的例子中ACTION_UP可以得到執(zhí)行肢簿。
那我們可以換一個(gè)控件,將按鈕替換成ImageView,然后給它也注冊(cè)一個(gè)touch事件池充,并返回false桩引。如下所示:
imageView.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d("TAG", "onTouch execute, action " + event.getAction());
return false;
}
});
點(diǎn)擊ImageView,你會(huì)發(fā)現(xiàn)結(jié)果如下:
在ACTION_DOWN執(zhí)行完后收夸,后面的一系列action都不會(huì)得到執(zhí)行了坑匠。這又是為什么呢?因?yàn)镮mageView和按鈕不同卧惜,它是默認(rèn)不可點(diǎn)擊的厘灼,因此在onTouchEvent的第14行判斷時(shí)無(wú)法進(jìn)入到if的內(nèi)部,直接跳到第91行返回了false咽瓷,也就導(dǎo)致后面其它的action都無(wú)法執(zhí)行了设凹。
onTouch和onTouchEvent有什么區(qū)別,又該如何使用茅姜?
從源碼中可以看出闪朱,這兩個(gè)方法都是在View的dispatchTouchEvent中調(diào)用的,onTouch優(yōu)先于onTouchEvent執(zhí)行匈睁。如果在onTouch方法中通過(guò)返回true將事件消費(fèi)掉监透,onTouchEvent將不會(huì)再執(zhí)行。
另外需要注意的是航唆,onTouch能夠得到執(zhí)行需要兩個(gè)前提條件胀蛮,第一mOnTouchListener的值不能為空,第二當(dāng)前點(diǎn)擊的控件必須是enable的糯钙。因此如果你有一個(gè)控件是非enable的粪狼,那么給它注冊(cè)onTouch事件將永遠(yuǎn)得不到執(zhí)行。對(duì)于這一類(lèi)控件任岸,如果我們想要監(jiān)聽(tīng)它的touch事件再榄,就必須通過(guò)在該控件中重寫(xiě)onTouchEvent方法來(lái)實(shí)現(xiàn)。
ViewGroup事件分發(fā)
ViewGroup就是一組View的集合享潜,它包含很多的子View和子VewGroup困鸥,是Android中所有布局的父類(lèi)或間接父類(lèi),像LinearLayout剑按、RelativeLayout等都是繼承自ViewGroup的疾就。但ViewGroup實(shí)際上也是一個(gè)View,只不過(guò)比起View艺蝴,它多了可以包含子View和定義布局參數(shù)的功能猬腰。ViewGroup繼承結(jié)構(gòu)示意圖如下所示:
例子:自定義MyLayout繼承LinearLayout,里面有Button1猜敢,Button2姑荷,Button均設(shè)置點(diǎn)擊時(shí)間(clickListener)盒延,MyLayout設(shè)置(onTouchListener)。結(jié)果:Button點(diǎn)擊時(shí)Button處理事件鼠冕,點(diǎn)擊其他地方MyLayout處理事件
ViewGroup中有一個(gè)onInterceptTouchEvent方法添寺,源碼:
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
Android中touch事件的傳遞,絕對(duì)是先傳遞到ViewGroup懈费,再傳遞到View的畦贸。當(dāng)你點(diǎn)擊了某個(gè)控件,首先會(huì)去調(diào)用該控件所在布局的dispatchTouchEvent方法楞捂,然后在布局的dispatchTouchEvent方法中找到被點(diǎn)擊的相應(yīng)控件薄坏,再去調(diào)用該控件的dispatchTouchEvent方法。如果我們點(diǎn)擊了MyLayout中的按鈕寨闹,會(huì)先去調(diào)用MyLayout的dispatchTouchEvent方法胶坠,可是你會(huì)發(fā)現(xiàn)MyLayout中并沒(méi)有這個(gè)方法。那就再到它的父類(lèi)LinearLayout中找一找繁堡,發(fā)現(xiàn)也沒(méi)有這個(gè)方法沈善。那只好繼續(xù)再找LinearLayout的父類(lèi)ViewGroup,你終于在ViewGroup中看到了這個(gè)方法椭蹄,按鈕的dispatchTouchEvent方法就是在這里調(diào)用的闻牡。修改后的示意圖如下所示:
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);
}
挑重點(diǎn)看,首先在第13行可以看到一個(gè)條件判斷绳矩,如果disallowIntercept和!onInterceptTouchEvent(ev)兩者有一個(gè)為true罩润,就會(huì)進(jìn)入到這個(gè)條件判斷中。disallowIntercept是指是否禁用掉事件攔截的功能翼馆,默認(rèn)是false割以,也可以通過(guò)調(diào)用requestDisallowInterceptTouchEvent方法對(duì)這個(gè)值進(jìn)行修改。那么當(dāng)?shù)谝粋€(gè)值為false的時(shí)候就會(huì)完全依賴(lài)第二個(gè)值來(lái)決定是否可以進(jìn)入到條件判斷的內(nèi)部应媚,第二個(gè)值是什么呢严沥?竟然就是對(duì)onInterceptTouchEvent方法的返回值取反!也就是說(shuō)如果我們?cè)趏nInterceptTouchEvent方法中返回false中姜,就會(huì)讓第二個(gè)值為true消玄,從而進(jìn)入到條件判斷的內(nèi)部,如果我們?cè)趏nInterceptTouchEvent方法中返回true丢胚,就會(huì)讓第二個(gè)值為false翩瓜,從而跳出了這個(gè)條件判斷。
這個(gè)時(shí)候你就可以思考一下了嗜桌,由于我們剛剛在MyLayout中重寫(xiě)了onInterceptTouchEvent方法奥溺,讓這個(gè)方法返回true辞色,導(dǎo)致所有按鈕的點(diǎn)擊事件都被屏蔽了骨宠,那我們就完全有理由相信浮定,按鈕點(diǎn)擊事件的處理就是在第13行條件判斷的內(nèi)部進(jìn)行的!
那我們重點(diǎn)來(lái)看下條件判斷的內(nèi)部是怎么實(shí)現(xiàn)的层亿。在第19行通過(guò)一個(gè)for循環(huán)桦卒,遍歷了當(dāng)前ViewGroup下的所有子View,然后在第24行判斷當(dāng)前遍歷的View是不是正在點(diǎn)擊的View匿又,如果是的話(huà)就會(huì)進(jìn)入到該條件判斷的內(nèi)部方灾,然后在第29行調(diào)用了該View的dispatchTouchEvent,之后的流程就和 Android事件分發(fā)機(jī)制完全解析碌更,帶你從源碼的角度徹底理解(上) 中講解的是一樣的了裕偿。我們也因此證實(shí)了,按鈕點(diǎn)擊事件的處理確實(shí)就是在這里進(jìn)行的痛单。
然后需要注意一下嘿棘,調(diào)用子View的dispatchTouchEvent后是有返回值的。我們已經(jīng)知道旭绒,如果一個(gè)控件是可點(diǎn)擊的鸟妙,那么點(diǎn)擊該控件時(shí),dispatchTouchEvent的返回值必定是true挥吵。因此會(huì)導(dǎo)致第29行的條件判斷成立重父,于是在第31行給ViewGroup的dispatchTouchEvent方法直接返回了true。這樣就導(dǎo)致后面的代碼無(wú)法執(zhí)行到了忽匈,也是印證了我們前面的Demo打印的結(jié)果房午,如果按鈕的點(diǎn)擊事件得到執(zhí)行,就會(huì)把MyLayout的touch事件攔截掉丹允。
那如果我們點(diǎn)擊的不是按鈕歪沃,而是空白區(qū)域呢?這種情況就一定不會(huì)在第31行返回true了嫌松,而是會(huì)繼續(xù)執(zhí)行后面的代碼沪曙。那我們繼續(xù)往后看拣技,在第44行渐逃,如果target等于null,就會(huì)進(jìn)入到該條件判斷內(nèi)部钱慢,這里一般情況下target都會(huì)是null贾陷,因此會(huì)在第50行調(diào)用super.dispatchTouchEvent(ev)缘眶。這句代碼會(huì)調(diào)用到哪里呢?當(dāng)然是View中的dispatchTouchEvent方法了髓废,因?yàn)閂iewGroup的父類(lèi)就是View巷懈。之后的處理邏輯又和前面所說(shuō)的是一樣的了,也因此MyLayout中注冊(cè)的onTouch方法會(huì)得到執(zhí)行慌洪。之后的代碼在一般情況下是走不到的了顶燕,我們也就不再繼續(xù)往下分析凑保。
ViewGroup事件分發(fā)過(guò)程的流程圖
梳理
- Android事件分發(fā)是先傳遞到ViewGroup,再由ViewGroup傳遞到View的涌攻。
- 在ViewGroup中可以通過(guò)onInterceptTouchEvent方法對(duì)事件傳遞進(jìn)行攔截欧引,onInterceptTouchEvent方法返回true代表不允許事件繼續(xù)向子View傳遞,返回false代表不對(duì)事件進(jìn)行攔截恳谎,默認(rèn)返回false芝此。
- 子View中如果將傳遞的事件消費(fèi)掉,ViewGroup中將無(wú)法接收到任何事件因痛。
面試問(wèn)題:關(guān)于ACTION_MOVE 和 ACTION_UP
紅色的箭頭代表ACTION_DOWN 事件的流向
藍(lán)色的箭頭代表ACTION_MOVE 和 ACTION_UP 事件的流向
總結(jié):ACTION_DOWN事件在哪個(gè)控件消費(fèi)了(return true)婚苹, 那么ACTION_MOVE和ACTION_UP就會(huì)從上往下(通過(guò)dispatchTouchEvent)做事件分發(fā)往下傳,就只會(huì)傳到這個(gè)控件鸵膏,不會(huì)繼續(xù)往下傳租副,如果ACTION_DOWN事件是在dispatchTouchEvent消費(fèi),那么事件到此為止停止傳遞较性,如果ACTION_DOWN事件是在onTouchEvent消費(fèi)的用僧,那么會(huì)把ACTION_MOVE或ACTION_UP事件傳給該控件的onTouchEvent處理并結(jié)束傳遞。