解決已有問題是成功的捷徑之一
1.一些常見問題
onTouch和onTouchEvent有什么區(qū)別?
為什么有時(shí)候onTouch中不返回true,也可以執(zhí)行到MOVE、UP事件周蹭?
onTouch和onTouchEvent和onClick哪個(gè)先執(zhí)行?
一些可滾動(dòng)的控件嵌套時(shí)的怎么不能滾動(dòng)了疲恢?
看源碼是解決問題的捷徑凶朗,么有之一
2.深入
平常使用中,常見的一個(gè)刪除按鈕显拳,想加上onClick點(diǎn)擊事件棚愤,使用方式如下:
deleteButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
LoggerUtil.e("onClick");
}
});
當(dāng)按鈕被點(diǎn)擊時(shí),會(huì)執(zhí)行onClick中的代碼杂数,打印"onClick"宛畦。
view的onTouch方法使用方式類似:
deleteButton.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return false;
}
});
當(dāng)手指在這個(gè)view上面按下、移動(dòng)揍移、抬起時(shí)都會(huì)執(zhí)行onTouch中的代碼次和,這里我沒有打印log,方便大家觀察這個(gè)方法和onClick方法的不同那伐,有兩個(gè)不同點(diǎn):1踏施、入?yún)⒈萶nClick多了一個(gè)MotionEvent(動(dòng)作事件),2喧锦、返回參數(shù)是boolean類型读规,現(xiàn)在在onTouch中加上log并且把多的一個(gè)參數(shù)打印出來。
deleteButton.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
LoggerUtil.e("onTouch action = "+event.getAction());
return false;
}
});
現(xiàn)在兩個(gè)事件都注冊(cè)了燃少,我們來點(diǎn)擊一下這個(gè)view束亏,打印結(jié)果如下:
onTouch action = 0
onTouch action = 2(省略多條)
onTouch action = 1
onClick
可以看到,先執(zhí)行了onTouch再執(zhí)行了onClick阵具,并且onTouch執(zhí)行了很多次碍遍。
暫時(shí)得出結(jié)論:事件傳遞順序是經(jīng)過onTouch,再傳遞到onClick。
我們來看一下剛才說的第一個(gè)不同點(diǎn):MotionEvent源碼中看到0是ACTION_DOWN阳液,2是ACTION_MOVE怕敬,1是ACTION_UP,說明我在點(diǎn)的過程中手指移動(dòng)了一下帘皿。
第二個(gè)不同點(diǎn):可以看到默認(rèn)返回參數(shù)是false东跪,現(xiàn)在改為true再運(yùn)行一次,結(jié)果如下
onTouch action = 0
onTouch action = 1
這一次我們發(fā)現(xiàn)onTouch action = 2沒有打印,onClick也沒有打印虽填,onTouch action = 2沒有打印時(shí)因?yàn)槲疫@次沒有手抖丁恭,onClick為什么也沒有打印呢?
看一下onTouch源碼中對(duì)返回參數(shù)的注釋
/**
* Called when a touch event is dispatched to a view. This allows listeners to
* get a chance to respond before the target view.
*
* @param v The view the touch event has been dispatched to.
* @param event The MotionEvent object containing full information about
* the event.
* @return True if the listener has consumed the event, false otherwise.
*/
boolean onTouch(View v, MotionEvent event);
如果這個(gè)listener消耗了這個(gè)event(事件)斋日,就返回true牲览,否則返回false。
我們可以完善剛才得出的結(jié)論:事件傳遞順序是經(jīng)過onTouch,再傳遞到onClick恶守,如果事件被onTouch消耗了第献,那么就不會(huì)繼續(xù)傳遞給onClick。
繼續(xù)深入源碼兔港,看看為什么事件被消耗就不會(huì)傳遞給onClick庸毫。
所有事物都有源頭。
我們先記住一點(diǎn):事件傳遞的源頭是dispatchTouchEvent方法押框,只要你觸摸了控件岔绸,就一定會(huì)調(diào)用這個(gè)方法。
當(dāng)我們點(diǎn)擊這個(gè)刪除按鈕時(shí)橡伞,會(huì)調(diào)用Button類里的dispatchTouchEvent盒揉,打開Button源碼厅缺,沒有dispatchTouchEvent胞谭,去父類TextView中找,還是么有鹤竭,繼續(xù)去父類View中找挂脑,OK藕漱,找到了,再找不到就不可能了崭闲,因?yàn)閂iew并沒有形式上的父類了肋联。來看一下dispatchTouchEvent源碼
/**
* Pass the touch screen motion event down to the target view, or this
* view if it is the target.
*
* @param event The motion event to be dispatched.
* @return True if the event was handled by the view, false otherwise.
*/
public boolean dispatchTouchEvent(MotionEvent event) {
// 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;
}
首先看一下注釋,這個(gè)方法是用來傳遞一個(gè)動(dòng)作事件給目標(biāo)view刁俭,如果目標(biāo)view就是當(dāng)前view時(shí)橄仍,則傳遞給它寄幾。
返回結(jié)果:如果事件被目標(biāo)view處理返回true牍戚,否則返回false
然后讓我們找到返回true的代碼:
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;
}
}
onFilterTouchEventForSecurity方法先過濾掉view被遮蔽的情況侮繁,然后
(mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)
判斷view狀態(tài)是否是enable(默認(rèn)都是enable)并且event是否是滾動(dòng)條在拖動(dòng),如果是返回true如孝。
如果不是的話宪哩,繼續(xù)執(zhí)行
li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event))
首先判斷mOnTouchListener是否為空,這個(gè)一定不為空第晰,因?yàn)閟etOnTouchListener方法中做了賦值操作锁孟。
然后判斷view狀態(tài)是否是enable彬祖。
(注意,這是onTouch能執(zhí)行的前置條件品抽,onTouchEvent不會(huì)受這個(gè)條件制約涧至,所以如果想對(duì)一個(gè)非enable的控件做事件處理,除了把狀態(tài)變?yōu)閑nable桑包,還可以重寫onTouchEvent方法。)
最后判斷onTouch的返回值纺非,剛才我們看了onTouch源碼的注釋:如果listener消耗了這個(gè)事件就返回true哑了,否則返回false。
我們注意到烧颖,這里執(zhí)行了onTouch方法弱左。
當(dāng)上面返回true的時(shí)候就不會(huì)執(zhí)行下面的方法了,因?yàn)橄旅娣椒ǖ臈l件是要上面結(jié)果為false:
if (!result && onTouchEvent(event)) {
result = true;
}
當(dāng)上面返回false炕淮,并且onTouchEvent返回true拆火,則dispatchTouchEvent結(jié)果為true。
我們注意到涂圆,這里執(zhí)行了onTouchEvent们镜。
這里可以得到一個(gè)結(jié)論:當(dāng)onTouch返回true時(shí),就不會(huì)執(zhí)行onTouchEvent润歉,否則就會(huì)執(zhí)行onTouchEvent模狭。
剛才我們一直在分析onTouch和onClick,這里又冒出來一個(gè)onTouchEvent是干什么的呢踩衩?onClick是否在onTouchEvent方法中呢嚼鹉?
繼續(xù)深入onTouchEvent源碼。
還是先看下方法注釋:
/**
* Implement this method to handle touch screen motion events.
* <p>
* If this method is used to detect click actions, it is recommended that
* the actions be performed by implementing and calling
* {@link #performClick()}. This will ensure consistent system behavior,
* including:
* <ul>
* <li>obeying click sound preferences
* <li>dispatching OnClickListener calls
* <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
* accessibility features are enabled
* </ul>
*
* @param event The motion event.
* @return True if the event was handled, false otherwise.
*/
public boolean onTouchEvent(MotionEvent event)
這個(gè)方法用來處理觸摸屏幕的動(dòng)作事件驱富。
我們也知道onTouch方法是:當(dāng)一個(gè)觸摸事件被發(fā)送給一個(gè)view時(shí)會(huì)調(diào)用onTouch方法锚赤,這樣可以讓listener在目標(biāo)view之前響應(yīng)。
對(duì)比一下可以看出onTouchEvent是對(duì)各種action做了處理褐鸥,而onTouch沒有线脚。
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
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);
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
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)) {
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;
if (performButtonActionOnTouchDown(event)) {
break;
}
// Walk up the hierarchy to determine if we're inside a scrolling container.
boolean isInScrollingContainer = isInScrollingContainer();
// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
checkForLongClick(0, x, y);
}
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_MOVE:
drawableHotspotChanged(x, y);
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
setPressed(false);
}
}
break;
}
return true;
}
return false;
}
大致可以看出這個(gè)方法內(nèi)部主要在處理MotionEvent的各種狀態(tài),什么條件下開始處理呢晶疼,當(dāng)滿足條件:
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE)
也就是說酒贬,當(dāng)view是可以點(diǎn)擊的就可以進(jìn)入到處理動(dòng)作事件的代碼塊中,并且onTouchEvent方法返回true翠霍。
在case MotionEvent.ACTION_UP中的performClick()中可以找到onClick方法:
/**
* 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;
}
可以看到锭吨,只要mOnClickListener不是null,就會(huì)去調(diào)用它的onClick方法寒匙,mOnClickListener的賦值就在setOnClickListener當(dāng)中零如。
結(jié)合上面所有結(jié)論躏将,得出:事件傳遞順序是先傳遞給onTouch,如果事件沒有被onTouch消耗考蕾,會(huì)傳遞給onTouchEvent祸憋,當(dāng)View是可點(diǎn)擊的狀態(tài),抬起手指的事件中會(huì)執(zhí)行onClick肖卧。
onTouch蚯窥,onTouchEvent,onClick之間的流程順序我們清楚了塞帐,那么回頭看最上面的例子拦赠,為什么onClick打印一次,onTouch被打印了多次呢葵姥?
這里有一個(gè)知識(shí)點(diǎn)就是:
touch事件的層級(jí)傳遞荷鼠。我們都知道如果給一個(gè)控件注冊(cè)了touch事件,每次點(diǎn)擊它的時(shí)候都會(huì)觸發(fā)一系列的ACTION_DOWN榔幸,ACTION_MOVE允乐,ACTION_UP等事件。當(dāng)執(zhí)行ACTION_DOWN的時(shí)候返回false削咆,后續(xù)的action都不會(huì)再執(zhí)行牍疏。
可以理解為接力跑步,當(dāng)前一個(gè)運(yùn)動(dòng)員把接力棒交給下一個(gè)運(yùn)動(dòng)員手里時(shí)拨齐,下一個(gè)運(yùn)動(dòng)員才可以開始跑步麸澜;當(dāng)前一個(gè)action返回true時(shí),下一個(gè)action才被觸發(fā)奏黑。
這里又有問題了炊邦!一開始的例子中,onTouch返回的是false熟史,為什么還可以打印很多次馁害?不是應(yīng)該只打印一次嗎?
當(dāng)然不是蹂匹,經(jīng)過剛才的結(jié)論我們已經(jīng)可以解釋這個(gè)現(xiàn)象:
首先執(zhí)行onTouch碘菜,這時(shí)onTouch返回的是false,表示事件沒有被onTouch消耗限寞,繼續(xù)傳遞給onTouchEvent忍啸,又因?yàn)閎utton是默認(rèn)可點(diǎn)擊的,所以繼續(xù)執(zhí)行了后續(xù)的action履植。
可以嘗試下把Button換成ImageView计雌,只注冊(cè)touch監(jiān)聽,并返回false玫霎,只會(huì)打印onTouch action = 0凿滤,因?yàn)镮mageView默認(rèn)不可點(diǎn)擊妈橄。(如果返回true,各種action就都可以打印出來了)
問題又來了翁脆!如果你的ImageView注冊(cè)touch監(jiān)聽眷蚓,并返回false,同時(shí)注冊(cè)了click監(jiān)聽反番,你會(huì)發(fā)現(xiàn)所有的action都打印了沙热,并且onClick也打印了!說好的ImageView默認(rèn)不可點(diǎn)擊只會(huì)打印action = 0呢罢缸?校读??
答案在setOnClickListener方法中祖能,沒錯(cuò)就是設(shè)置click監(jiān)聽的那個(gè)方法,看源碼:
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
這里不僅給mOnClickListener賦值蛾洛,還做了一個(gè)操作:當(dāng)view不可點(diǎn)擊時(shí)养铸,設(shè)置為可點(diǎn)擊。
破案了轧膘。
3.淺出
來回顧下一開始的問題
onTouch和onTouchEvent有什么區(qū)別钞螟?
onTouch和onTouchEvent都是在dispatchTouchEvent中執(zhí)行,onTouch優(yōu)先于onTouchEvent執(zhí)行谎碍。如果onTouch中通過返回true把事件消費(fèi)掉鳞滨,則onTouchEvent不會(huì)執(zhí)行。(可以把剛才同時(shí)注冊(cè)onTouch和onClick的ImageView的onTouch返回改為true蟆淀,onClick就不會(huì)打印了拯啦,因?yàn)閛nClick是在onTouchEvent中調(diào)用的)
為什么有時(shí)候onTouch中不返回true,也可以執(zhí)行到MOVE熔任、UP事件褒链?
因?yàn)閛nTouch返回false的時(shí)候dispatchTouchEvent會(huì)調(diào)用onTouchEvent方法,如果view是可點(diǎn)擊的狀態(tài)疑苔,就會(huì)執(zhí)行各種action甫匹。