事件分發(fā)機(jī)制面試也會(huì)經(jīng)常被提及,如果你能get到要領(lǐng),并跟面試官深入的靈魂交流一下鹦马,那么一定會(huì)讓面試官對(duì)你印象深刻,拋出愛(ài)的橄欖枝想想都有點(diǎn)小激動(dòng)呢忆肾。那么就讓我們從淺入深荸频,由表及里的去看事件分發(fā)機(jī)制,全方位客冈,立體式旭从,去弄懂這個(gè)神秘的事件分發(fā)機(jī)制吧。
事件分化機(jī)制的對(duì)象: onTouchEvent
MotionEvent事件初探:
我們對(duì)屏幕的點(diǎn)擊郊酒,滑動(dòng)遇绞,抬起等一系的動(dòng)作都是由一個(gè)一個(gè)MotionEvent對(duì)象組成的。根據(jù)不同動(dòng)作燎窘,主要有以下三種事件類(lèi)型:
1.ACTION_DOWN:手指剛接觸屏幕摹闽,按下去的那一瞬間產(chǎn)生該事件
2.ACTION_MOVE:手指在屏幕上移動(dòng)時(shí)候產(chǎn)生該事件
3.ACTION_UP:手指從屏幕上松開(kāi)的瞬間產(chǎn)生該事件
從ACTION_DOWN開(kāi)始到ACTION_UP結(jié)束我們稱(chēng)為一個(gè)事件序列
正常情況下,無(wú)論你手指在屏幕上有多么騷的操作褐健,最終呈現(xiàn)在MotionEvent上來(lái)講無(wú)外乎下面兩種:
1.點(diǎn)擊后抬起付鹿,也就是單擊操作:ACTION_DOWN -> ACTION_UP
2.點(diǎn)擊后再風(fēng)騷的滑動(dòng)一段距離澜汤,再抬起:ACTION_DOWN -> ACTION_MOVE -> ... -> ACTION_MOVE -> ACTION_UP
舉個(gè)例子:
public class MotionEventActivity extends BaseActivity {
private Button mButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_motion_event);
mButton = (Button) findViewById(R.id.button);
mButton.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
e("MotionEvent: ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
e("MotionEvent: ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
e("MotionEvent: ACTION_UP");
break;
}
return false;
}
});
}
public void click(View v) {
e("點(diǎn)擊了按鈕");
}
}
當(dāng)我們單擊按鈕:
當(dāng)我們?cè)诎粹o上滑動(dòng)時(shí):
可以發(fā)現(xiàn)了我們常用的按鈕的onclick事件都是在ACTION_UP以后才被調(diào)用的。這和View的事件分發(fā)機(jī)制是不是有某種不可告人的關(guān)系呢舵匾?俊抵! 當(dāng)我們給button設(shè)置了OnTouchListener并重寫(xiě)了onTouch方法,方法返回值默認(rèn)為false坐梯。如果這里我們返回true徽诲,那么你會(huì)發(fā)現(xiàn)onclick方法不執(zhí)行了!這些隨著我們的深入探討吵血,結(jié)論就會(huì)浮出水面谎替!針對(duì)MotionEvent,我們先說(shuō)這么多蹋辅。
MotionEvent事件分發(fā)
當(dāng)一個(gè)MotionEvent產(chǎn)生了以后钱贯,就是你的手指在屏幕上做一系列動(dòng)作的時(shí)候,系統(tǒng)需要把這一系列的MotionEvent分發(fā)給一個(gè)具體的View侦另。我們重點(diǎn)需要了解這個(gè)分發(fā)的過(guò)程秩命,那么系統(tǒng)是如何去判斷這個(gè)事件要給哪個(gè)View,也就是說(shuō)是如何進(jìn)行分發(fā)的呢褒傅?
事件分發(fā)需要View的三個(gè)重要方法來(lái)共同完成:
1.public boolean dispatchTouchEvent(MotionEvent event)
通過(guò)方法名我們不難猜測(cè)弃锐,它就是事件分發(fā)的重要方法。那么很明顯樊卓,如果一個(gè)MotionEvent傳遞給了View拿愧,那么dispatchTouchEvent方法一定會(huì)被調(diào)用杠河!
返回值:表示是否消費(fèi)了當(dāng)前事件碌尔。可能是View本身的onTouchEvent方法消費(fèi)券敌,也可能是子View的dispatchTouchEvent方法中消費(fèi)唾戚。返回true表示事件被消費(fèi),本次的事件終止待诅。返回false表示View以及子View均沒(méi)有消費(fèi)事件叹坦,將調(diào)用父View的onTouchEvent方法
2.public boolean onInterceptTouchEvent(MotionEvent ev)
事件攔截,當(dāng)一個(gè)ViewGroup在接到MotionEvent事件序列時(shí)候卑雁,首先會(huì)調(diào)用此方法判斷是否需要攔截募书。特別注意,這是ViewGroup特有的方法测蹲,View并沒(méi)有攔截方法
返回值:是否攔截事件傳遞莹捡,返回true表示攔截了事件,那么事件將不再向下分發(fā)而是調(diào)用View本身的onTouchEvent方法扣甲。返回false表示不做攔截篮赢,事件將向下分發(fā)到子View的dispatchTouchEvent方法。
3.public boolean onTouchEvent(MotionEvent ev)
真正對(duì)MotionEvent進(jìn)行處理或者說(shuō)消費(fèi)的方法。在dispatchTouchEvent進(jìn)行調(diào)用启泣。
返回值:返回true表示事件被消費(fèi)涣脚,本次的事件終止。返回false表示事件沒(méi)有被消費(fèi)寥茫,將調(diào)用父View的onTouchEvent方法
上面的三個(gè)方法可以用以下的偽代碼來(lái)表示其之間的關(guān)系遣蚀。
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;//事件是否被消費(fèi)
if (onInterceptTouchEvent(ev)){//調(diào)用onInterceptTouchEvent判斷是否攔截事件
consume = onTouchEvent(ev);//如果攔截則調(diào)用自身的onTouchEvent方法
}else{
consume = child.dispatchTouchEvent(ev);//不攔截調(diào)用子View的dispatchTouchEvent方法
}
return consume;//返回值表示事件是否被消費(fèi),true事件終止纱耻,false調(diào)用父View的onTouchEvent方法
}
接下來(lái)我們來(lái)看一下View 和ViewGroup 在事件分發(fā)的時(shí)候有什么不一樣的地方
ViewGroup是View的子類(lèi)妙同,也就是說(shuō)ViewGroup本身就是一個(gè)View,但是它可以包含子View(當(dāng)然子View也可能是一個(gè)ViewGroup)膝迎,所以不難理解粥帚,上面所展示的偽代碼表示的是ViewGroup 處理事件分發(fā)的流程。而View本身是不存在分發(fā)限次,所以也沒(méi)有攔截方法(onInterceptTouchEvent)芒涡,它只能在onTouchEvent方法中進(jìn)行處理消費(fèi)或者不消費(fèi)。
上面結(jié)論先簡(jiǎn)單的理解一下卖漫,通過(guò)下面的流程圖费尽,會(huì)更加清晰的幫助我們梳理事件分發(fā)機(jī)制.
可以看出事件的傳遞過(guò)程都是從父View到子View。
但是這里有三點(diǎn)需要特別強(qiáng)調(diào)一下:
1.子View可以通過(guò)requestDisallowInterceptTouchEvent方法干預(yù)父View的事件分發(fā)過(guò)程(ACTION_DOWN事件除外)羊始,而這就是我們處理滑動(dòng)沖突常用的關(guān)鍵方法旱幼。關(guān)于處理滑動(dòng)沖突,我們下一篇文章會(huì)專(zhuān)門(mén)去分析突委,這里就不做過(guò)多解釋柏卤。
2.對(duì)于View(注意!ViewGroup也是View)而言匀油,如果設(shè)置了onTouchListener缘缚,那么OnTouchListener方法中的onTouch方法會(huì)被回調(diào)。onTouch方法返回true敌蚜,則onTouchEvent方法不會(huì)被調(diào)用(onClick事件是在onTouchEvent中調(diào)用)所以三者優(yōu)先級(jí)是onTouch->onTouchEvent->onClick
View 的onTouchEvent 方法默認(rèn)都會(huì)消費(fèi)掉事件(返回true)桥滨,除非它是不可點(diǎn)擊的(clickable和longClickable同時(shí)為false),View的longClickable默認(rèn)為false弛车,clickable需要區(qū)分情況齐媒,如Button的clickable默認(rèn)為true,而TextView的clickable默認(rèn)為false纷跛。
View事件分發(fā)源碼分析
點(diǎn)擊事件產(chǎn)生最先傳遞到當(dāng)前的Activity喻括,由Acivity的dispatchTouchEvent方法來(lái)對(duì)事件進(jìn)行分發(fā)。那么很明顯我們先看Activity的dispatchTouchEvent方法
Class Activity:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {//事件分發(fā)并返回結(jié)果
return true;//事件被消費(fèi)
}
return onTouchEvent(ev);//沒(méi)有View可以處理忽舟,調(diào)用Activity onTouchEvent方法
}
通過(guò)上面的代碼我們可以發(fā)現(xiàn)双妨,事件會(huì)給Activity附屬的Window進(jìn)行分發(fā)淮阐。如果返回true,那么事件被消費(fèi)刁品。如果返回false表示事件發(fā)下去卻沒(méi)有View可以進(jìn)行處理泣特,則最后return Activity自己的onTouchEvent方法。
跟進(jìn)getWindow().superDispatchTouchEvent(ev)方法發(fā)現(xiàn)是Window類(lèi)當(dāng)中的一個(gè)抽象方法
Window類(lèi)說(shuō)明
/**
* Abstract base class for a top-level window look and behavior policy. An
* instance of this class should be used as the top-level view added to the
* window manager. It provides standard UI policies such as a background, title
* area, default key processing, etc.
*
* <p>The only existing implementation of this abstract class is
* android.view.PhoneWindow, which you should instantiate when needing a
* Window.
*/
Class Window:
//抽象方法挑随,需要看PhoneWindow的實(shí)現(xiàn)
public abstract boolean superDispatchTouchEvent(MotionEvent event);
Window的源碼有說(shuō)明The only existing implementation of this abstract class is
android.view.PhoneWindow状您,Window的唯一實(shí)現(xiàn)類(lèi)是PhoneWindow。那么去看PhoneWindow對(duì)應(yīng)的代碼
class PhoneWindow
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
PhoneWindow又調(diào)用了DecorView的superDispatchTouchEvent方法兜挨。而這個(gè)DecorView就是Window的頂級(jí)View膏孟,我們通過(guò)setContentView設(shè)置的View是它的子View(Activity的setContentView,最終是調(diào)用PhoneWindow的setContentView拌汇,有興趣同學(xué)可以去閱讀柒桑,這塊不是我們討論重點(diǎn))
到這里事件已經(jīng)被傳遞到我們的頂級(jí)View中,一般是ViewGroup噪舀。
那么接下來(lái)重點(diǎn)將放到ViewGroup的dispatchTouchEvent方法中魁淳。我們之前說(shuō)過(guò),事件到達(dá)View會(huì)調(diào)用dispatchTouchEvent方法与倡,如果View是ViewGroup那么會(huì)先判斷是否攔截該事件界逛。
class ViewGroup:
public boolean dispatchTouchEvent(MotionEvent ev) {
...
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
//清除FLAG_DISALLOW_INTERCEPT設(shè)置并且mFirstTouchTarget 設(shè)置為null
resetTouchState();
}
// Check for interception.
final boolean intercepted;//是否攔截事件
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//FLAG_DISALLOW_INTERCEPT是子View通過(guò)
//requestDisallowInterceptTouchEvent方法進(jìn)行設(shè)置的
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//調(diào)用onInterceptTouchEvent方法判斷是否需要攔截
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
...
}
我們前面說(shuō)過(guò)子View可以通過(guò)requestDisallowInterceptTouchEvent方法干預(yù)父View的事件分發(fā)過(guò)程(ACTION_DOWN事件除外)
為什么ACTION_DOWN除外?通過(guò)上述代碼我們不難發(fā)現(xiàn)纺座。如果事件是ACTION_DOWN息拜,那么ViewGroup會(huì)重置FLAG_DISALLOW_INTERCEPT標(biāo)志位并且將mFirstTouchTarget 設(shè)置為null。對(duì)于mFirstTouchTarget 我們可以先這么理解净响,如果事件由子View去處理時(shí)mFirstTouchTarget 會(huì)被賦值并指向子View少欺。
所以當(dāng)事件為ACTION_DOWN或者 mFirstTouchTarget !=null(即事件由子View處理)時(shí)會(huì)進(jìn)行攔截判斷别惦。具體規(guī)則是如果子View設(shè)置了FLAG_DISALLOW_INTERCEPT標(biāo)志位狈茉,那么intercepted =false夫椭。否則調(diào)用onInterceptTouchEvent方法掸掸。
如果事件不為ACTION_DOWN 且事件為ViewGroup本身處理(即mFirstTouchTarget ==null)那么intercepted =false,很顯然事件已經(jīng)交給自己處理根本沒(méi)必要再調(diào)用onInterceptTouchEvent去判斷是否攔截蹭秋。
結(jié)論:
當(dāng)ViewGroup決定攔截事件后扰付,后續(xù)事件將默認(rèn)交給它處理并且不會(huì)再調(diào)用onInterceptTouchEvent方法來(lái)判斷是否攔截。子View可以通過(guò)設(shè)置 FLAG_DISALLOW_INTERCEPT 標(biāo)志位來(lái)不讓ViewGroup攔截除ACTION_DOWN以外的事件仁讨。
所以我們知道了onInterceptTouchEvent并非每次都會(huì)被調(diào)用羽莺。如果要處理所有的點(diǎn)擊事件那么需要選擇 dispatchTouchEvent 方法
而 FLAG_DISALLOW_INTERCEPT 標(biāo)志位可以幫助我們?nèi)ビ行У奶幚砘瑒?dòng)沖突,當(dāng)ViewGroup不攔截事件洞豁,那么事件將下發(fā)給子View進(jìn)行處理盐固。
當(dāng)ViewGroup不攔截事件荒给,那么事件將下發(fā)給子View進(jìn)行處理。
class ViewGroup:
public boolean dispatchTouchEvent(MotionEvent ev) {
final View[] children = mChildren;
//對(duì)子View進(jìn)行遍歷
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
//判斷1刁卜,View可見(jiàn)并且沒(méi)有播放動(dòng)畫(huà)志电。2,點(diǎn)擊事件的坐標(biāo)落在View的范圍內(nèi)
//如果上述兩個(gè)條件有一項(xiàng)不滿足則continue繼續(xù)循環(huán)下一個(gè)View
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
//如果有子View處理即newTouchTarget 不為null則跳出循環(huán)蛔趴。
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
//dispatchTransformedTouchEvent第三個(gè)參數(shù)child這里不為null
//實(shí)際調(diào)用的是child的dispatchTouchEvent方法
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
//當(dāng)child處理了點(diǎn)擊事件挑辆,那么會(huì)設(shè)置mFirstTouchTarget 在addTouchTarget被賦值
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
//子View處理了事件,然后就跳出了for循環(huán)
break;
}
}
}
上面代碼是將事件分發(fā)給子View的關(guān)鍵代碼孝情,需要關(guān)注的地方都加了注釋鱼蝉。分發(fā)過(guò)程首先需要遍歷ViewGroup的所有子View,可以接收點(diǎn)擊事件的View需要滿足下面條件箫荡。
1.如果View可見(jiàn)并且沒(méi)有播放動(dòng)畫(huà)canViewReceivePointerEvents方法判斷
/**
* Returns true if a child view can receive pointer events.
* @hide
*/
private static boolean canViewReceivePointerEvents(@NonNull View child) {
return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null;
}
2.點(diǎn)擊事件的坐標(biāo)落在View的范圍內(nèi)isTransformedTouchPointInView方法判斷
/**
* Returns true if a child view contains the specified point when transformed
* into its coordinate space.
* Child must not be null.
* @hide
*/
protected boolean isTransformedTouchPointInView(float x, float y, View child,
PointF outLocalPoint) {
final float[] point = getTempPoint();
point[0] = x;
point[1] = y;
transformPointToViewLocal(point, child);
//調(diào)用View的pointInView方法進(jìn)行判斷坐標(biāo)點(diǎn)是否在View內(nèi)
final boolean isInView = child.pointInView(point[0], point[1]);
if (isInView && outLocalPoint != null) {
outLocalPoint.set(point[0], point[1]);
}
return isInView;
}
如果滿足上面兩個(gè)條件魁亦,接著我們看后面的代碼newTouchTarget = getTouchTarget(child);
/**
* Gets the touch target for specified child view.
* Returns null if not found.
*/
private TouchTarget getTouchTarget(@NonNull View child) {
for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
if (target.child == child) {
return target;
}
}
return null;
}
可以看到當(dāng)mFirstTouchTarget不為null的時(shí)候并且target.child就為我們當(dāng)前遍歷的child的時(shí)候,那么返回的newTouchTarget 就不為null羔挡,則跳出循環(huán)吉挣。我們前面說(shuō)過(guò),當(dāng)子View處理了點(diǎn)擊事件那么mFirstTouchTarget就不為nulll婉弹。事實(shí)上此時(shí)我們還沒(méi)有將事件分發(fā)給子View睬魂,所以正常情況下我們的newTouchTarget 此時(shí)為null
接下來(lái)關(guān)鍵來(lái)了
dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)方法。為方便我們將代碼再一次貼到后面來(lái)
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
//當(dāng)child處理了點(diǎn)擊事件镀赌,那么會(huì)設(shè)置mFirstTouchTarget 在addTouchTarget被賦值
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
//子View處理了事件氯哮,然后就跳出了for循環(huán)
break;
}
可以看到它被最后一個(gè)if包圍,如果它返回為true商佛,那么就break跳出循環(huán)喉钢,如果返回為false則繼續(xù)遍歷下一個(gè)子View。
我們跟進(jìn)dispatchTransformedTouchEvent方法可以看到這樣的關(guān)鍵邏輯
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
這里child是我們遍歷傳入的子View此時(shí)不為null良姆,則調(diào)用了child.dispatchTouchEvent(event);
我們子View的dispatchTouchEvent方法返回true肠虽,表示子View處理了事件,那么我們一直提到的玛追,mFirstTouchTarget 會(huì)被賦值税课,是在哪里完成的呢?
再回頭看dispatchTransformedTouchEvent則為true進(jìn)入最后一個(gè)if語(yǔ)句痊剖,有這么一句
newTouchTarget = addTouchTarget(child, idBitsToAssign);
/**
* Adds a touch target for specified child to the beginning of the list.
* Assumes the target child is not already present.
*/
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
沒(méi)錯(cuò)韩玩,mFirstTouchTarget 就是在addTouchTarget中被賦值!到此子View遍歷結(jié)束
如果在遍歷完子View以后ViewGroup仍然沒(méi)有找到事件處理者即ViewGroup并沒(méi)有子View或者子View處理了事件陆馁,但是子View的dispatchTouchEvent返回了false(一般是子View的onTouchEvent方法返回false)那么ViewGroup會(huì)去處理這個(gè)事件找颓。
從代碼上看就是我們遍歷的dispatchTransformedTouchEvent方法返回了false。那么mFirstTouchTarget 必然為null叮贩;
在ViewGroup的dispatchTouchEvent遍歷完子View后有下面的處理击狮。
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
上面的dispatchTransformedTouchEvent方法第三個(gè)child參數(shù)傳null
我們剛看了這個(gè)方法佛析。當(dāng)child為null時(shí),handled = super.dispatchTouchEvent(event);所以此時(shí)將調(diào)用View的dispatchTouchEvent方法彪蓬,點(diǎn)擊事件給了View说莫。到此事件分發(fā)過(guò)程全部結(jié)束!
結(jié)論:
1. ViewGroup會(huì)遍歷所有子View去尋找能夠處理點(diǎn)擊事件的子View(可見(jiàn)寞焙,沒(méi)有播放動(dòng)畫(huà)储狭,點(diǎn)擊事件坐標(biāo)落在子View內(nèi)部)最終調(diào)用子View的dispatchTouchEvent方法處理事件
2. 當(dāng)子View處理了事件則mFirstTouchTarget 被賦值,并終止子View的遍歷捣郊。
3辽狈、如果ViewGroup并沒(méi)有子View或者子View處理了事件,但是子View的dispatchTouchEvent返回了false(一般是子View的onTouchEvent方法返回false)那么ViewGroup會(huì)去處理這個(gè)事件(本質(zhì)調(diào)用View的dispatchTouchEvent去處理)
通過(guò)ViewGroup對(duì)事件的分發(fā)呛牲,我們知道事件最終是調(diào)用View的dispatchTouchEvent來(lái)處理
View最終是怎么去處理事件的
class View:
public boolean dispatchTouchEvent(MotionEvent ev) {
// 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)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//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;
}
上面是View的dispatchTouchEvent方法的全部代碼刮萌。相比ViewGroup我們需要好幾段去拆開(kāi)看的長(zhǎng)篇大論而言,它就簡(jiǎn)潔多了娘扩。很明顯View是單獨(dú)的一個(gè)元素着茸,它沒(méi)有子View,所以也沒(méi)有分發(fā)的代碼琐旁。我們需要關(guān)注的也只是上面當(dāng)中的一部分代碼涮阔。
//如果窗口沒(méi)有被遮蓋
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
//當(dāng)前監(jiān)聽(tīng)事件
ListenerInfo li = mListenerInfo;
//需要特別注意這個(gè)判斷當(dāng)中的li.mOnTouchListener.onTouch(this, event)條件
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//result為false調(diào)用自己的onTouchEvent方法處理
if (!result && onTouchEvent(event)) {
result = true;
}
}
通過(guò)上面代碼我們可以看到View會(huì)先判斷是否設(shè)置了OnTouchListener,如果設(shè)置了OnTouchListener并且onTouch方法返回了true灰殴,那么onTouchEvent不會(huì)被調(diào)用敬特。
當(dāng)沒(méi)有設(shè)置OnTouchListener或者設(shè)置了OnTouchListener但是onTouch方法返回false則會(huì)調(diào)用View自己的onTouchEvent方法。
接下來(lái)看onTouchEvent方法:
class View:
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
//1.如果View是設(shè)置成不可用的(DISABLED)仍然會(huì)消費(fèi)點(diǎn)擊事件
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == 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)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
...
//2.CLICKABLE 和LONG_CLICKABLE只要有一個(gè)為true就消費(fèi)這個(gè)事件
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
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 && !mIgnoreNextUpEvent) {
// 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)) {
//3.在ACTION_UP方法發(fā)生時(shí)會(huì)觸發(fā)performClick()方法
performClick();
}
}
}
...
break;
}
...
return true;
}
return false;
}
上述代碼有三個(gè)關(guān)鍵點(diǎn)分別在注釋處標(biāo)出牺陶∥袄可以看出即便View是disabled狀態(tài),依然不會(huì)影響事件的消費(fèi)掰伸,只是它看起來(lái)不可用皱炉。只要CLICKABLE和LONG_CLICKABLE有一個(gè)為true,就一定會(huì)消費(fèi)這個(gè)事件狮鸭,就是onTouchEvent返回true合搅。這點(diǎn)也印證了我們前面說(shuō)的View 的onTouchEvent 方法默認(rèn)都會(huì)消費(fèi)掉事件(返回true),除非它是不可點(diǎn)擊的(clickable和longClickable同時(shí)為false)怕篷,View的longClickable默認(rèn)為false历筝,clickable需要區(qū)分情況,如Button的clickable默認(rèn)為true廊谓,而TextView的clickable默認(rèn)為false。
(沒(méi)錯(cuò)這是復(fù)制前面的B橄鳌U舯浴4好帧)
ACTION_UP方法中有performClick();接下來(lái)看一下它:
class View:
/**
* Call this view's OnClickListener, if it is defined. Performs all normal
* actions associated with clicking: reporting accessibility event, playing
* a sound, etc.
*
* @return True there was an assigned OnClickListener that was called, false
* otherwise is returned.
*/
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;
}
很明顯叠荠,如果View設(shè)置了OnClickListener匿沛,那么會(huì)回調(diào)onClick方法。到這里相信大家對(duì)一開(kāi)始的例子已經(jīng)沒(méi)有什么疑惑了吧榛鼎。
最后再強(qiáng)調(diào)一點(diǎn)逃呼,我們剛說(shuō)過(guò)View的longClickable默認(rèn)為false,clickable需要區(qū)分情況者娱,如Button的clickable默認(rèn)為true抡笼,而TextView的clickable默認(rèn)為false。
這是默認(rèn)情況黄鳍,我們可以單獨(dú)給View設(shè)置clickable屬性推姻,但有時(shí)候會(huì)發(fā)現(xiàn)View的setClickable方法失效了。假如我們想讓View默認(rèn)不可點(diǎn)擊框沟,將View的clickable設(shè)置成false藏古,在合適的時(shí)候需要可點(diǎn)擊所以我們又給View設(shè)置了OnClickListener,那么你會(huì)發(fā)現(xiàn)View默認(rèn)依然可以點(diǎn)擊忍燥,也就是說(shuō)setClickable失效了拧晕。
class View:
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
if (!isLongClickable()) {
setLongClickable(true);
}
getListenerInfo().mOnLongClickListener = l;
}
View的setOnClickListener會(huì)默認(rèn)將View的clickable設(shè)置成true。
View的setOnLongClickListener同樣會(huì)將View的longClickable設(shè)置成true梅垄。
至此防症,MotionEvent事件分發(fā)機(jī)制與源碼的分析已經(jīng)搞定,大家是否有g(shù)et到技能+1的感覺(jué)哎甲?
接下來(lái)一篇文章將講述如何解決View滑動(dòng)沖突蔫敲。