【我的Android進階之旅】解決重寫onTouch事件提示的警告:onTouch should call View#performClick when a click is detected
一、問題描述
當你對一個控制元件(例如ImageView)使用setOnTouchListener() 或者是對你的自定義控制元件重寫onTouchEvent方法時會出現這個警告,警告內容全文如下:
MyImageOnTouchListener#onTouch should call View#performClick when a click is detected less… (Ctrl+F1)
If a View that overrides onTouchEvent or uses an OnTouchListener does not also implement performClick and call it when clicks are detected, the View may not handle accessibility actions properly. Logic handling the click actions should ideally be placed in View#performClick as some accessibility services invoke performClick when a click action should occur.
這段英文翻譯過來,大致意思如下:
如果覆蓋onTouchEvent或使用OnTouchListener的View沒有實現performClick方法,並且在檢測到click事件時呼叫它,則View可能無法正確地處理可訪問性操作。處理單擊操作的邏輯理想情況下應該放在View#performClick中,因為某些可訪問性服務在應該發生單擊操作時呼叫performClick。
這段中文翻譯好繞,意思可能不太明瞭。
這裡簡單說明一下:當你新增了一些點選操作,例如像setOnClickListener這樣的,它會呼叫performClick才可以完成操作,但你重寫了onTouch,就有可能把performClick給遮蔽了,這樣這些點選操作就沒辦法完成了,所以就會有了這個警告。如下所示:
這個imageView,呼叫了setOnTouchListener方法和setOnClickListener方法。
一般情況下我們很少會在重寫了onTouchEvent後再使用setOnClickListener。
二、探索原理:為什麼會出現這個警告?
我們來探索下,為什麼會出現這個警告?
2.1 View.onTouch原始碼
我們開啟onTouch方法的定義原始碼,如下所示:
/**
* Interface definition for a callback to be invoked when a touch event is
* dispatched to this view. The callback will be invoked before the touch
* event is given to the view.
*/
public interface OnTouchListener {
/**
* 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);
}
那這個onTouch方法在什麼地方被呼叫呢?這就得考慮到 Android事件分發機制了,這個 Android事件分發機制太複雜,這裡就不著重去講解了,簡單的講解一下我們這個問題。
2.2 View.dispatchTouchEvent原始碼
先看下View.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)) {
//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方法的原始碼,我們可以看到如下的結論:
onTouchListener的介面的優先順序是要高於onTouchEvent的,假若onTouchListener中的onTouch方法返回true, 表示此次事件已經被消費了,那onTouchEvent是接收不到訊息的。
比如,將ImageView設定一個onTouchListener並且重寫onTouch方法,返回值為true, 此時的ImageView設定的OnClickListener還會處理點選事件嗎?
執行下程式,發現點選圖片,點選事件不響應,如下所示:
由上面的測試,我們發現ImageView設定的OnClickListener不會處理點選事件了。
原因是:ImageView的performClick是利用onTouchEvent實現,假若onTouchEvent沒有被呼叫到,那麼ImageView的Click事件也無法響應。
2.3 View.onTouchEvent原始碼
繼續深究View.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) {
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);
}
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;
}
如上圖所示,在onTouchEvent(MotionEvent event)方法中,處理MotionEvent.ACTION_UP事件的時候,會去呼叫**performClick()**方法。
PerformClick 原始碼
private final class PerformClick implements Runnable {
@Override
public void run() {
performClick();
}
}
performClick() 原始碼
/**
* 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;
}
如上所示,performClick()呼叫你在setOnClickListener時重寫的onClick()方法。
- 結論
在onTouchEvent的ACTION_UP過程中啟用了一個新的執行緒來呼叫performClick(),而performClick()呼叫你在setOnClickListener時重寫的onClick()方法。
2.4 總結
onTouchListener的onTouch方法優先順序比onTouchEvent高,會先觸發。
假如onTouch方法返回false會接著觸發onTouchEvent,反之onTouchEvent方法不會被呼叫。
- 內建諸如click事件的實現等等都基於onTouchEvent,假如onTouch返回true,這些事件將不會被觸發。
- 順序為: onTouch—–>onTouchEvent—>onClick
三、解決警告
為了解決這個警告,我們應該在重寫onTouch的時候,在合適的時機處呼叫一下 View#performClick方法,如下所示:
示例程式碼:
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//some code....
break;
case MotionEvent.ACTION_UP:
v.performClick();
break;
default:
break;
}
return true;
}
下面是我自己專案中的處理邏輯,如下所示:
class MyImageOnTouchListener implements View.OnTouchListener {
@Override
public boolean onTouch(View v, MotionEvent event) {
// onTouch是優先於onClick的,
// 並且執行了兩次,一次是ACTION_DOWN,一次是ACTION_UP(可能還會有多次ACTION_MOVE),
// 因此事件傳遞的順序是先經過OnTouch,再傳遞給onClick
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN://手指按下
Log.d(TAG, "onTouch==手指按下");
mNoLeakHandler.removeCallbacksAndMessages(null);
break;
case MotionEvent.ACTION_MOVE://手指在這個控制元件上移動
break;
case MotionEvent.ACTION_CANCEL://手指在這個控制元件上移動
Log.d(TAG, "onTouch==事件取消");
break;
case MotionEvent.ACTION_UP://手指離開
Log.d(TAG, "onTouch==手指離開");
mNoLeakHandler.removeCallbacksAndMessages(null);
mNoLeakHandler.sendEmptyMessageDelayed(0, 4000);
//如果底下的返回值為true,則需要呼叫performClick()方法,否則OnClick事件無效
//如果底下的返回值為false,則不一定需要呼叫performClick()方法
v.performClick();
break;
}
return true;
}
}
這樣在合適的時機處呼叫一下 View#performClick方法之後,錯誤的警告提示也不存在了。
我們繼續思考下,這一次將ImageView設定一個onTouchListener並且重寫onTouch方法,返回值為true, 此時的ImageView設定的OnClickListener還會處理點選事件嗎?
現在再重新執行程式,試一試,如下所示:
四、總結
當對一個View既設定了setOnTouchListener()方法,又設定了setOnClickListener()方法的時候,記得在OnTouchListener的onTouch()方法裡呼叫一下performClick()方法。
因為如果你重寫了onTouch,並且返回值返回true的話,就有可能把performClick()方法給遮蔽了,這樣這些點選操作就沒辦法完成了。
五、參考連結
- https://blog.csdn.net/qq_32916805/article/details/78567651
- https://blog.csdn.net/fenganit/article/details/53750265
- https://www.cnblogs.com/Claire6649/p/5947139.html
作者:歐陽鵬 歡迎轉載,與人分享是進步的源泉!
轉載請保留原文地址:https://blog.csdn.net/ouyang_peng/article/details/82563779
☞ 本人QQ: 3024665621
☞ QQ交流群: 123133153
☞ github.com/ouyangpeng
☞ oypcz@foxmail.com
如果本文對您有所幫助,歡迎您掃碼下圖所示的支付寶和微信支付二維碼對本文進行打賞。
相關文章
- Android自定義OnTouch事件Android事件
- 我的Android重構之旅:架構篇Android架構
- 我的Android重構之旅:外掛化篇Android
- 如何解決 touchstart 事件與 click 事件的衝突事件
- 前端進階的破冰之旅前端
- 【我的Android進階之旅】如何解決當機、斷電等情況之後,重啟Android Studio可以編譯apk,但是所有原始碼都爆紅錯誤的問題Android編譯APK原始碼
- 福祿克光纖測試的警告提示怎麼解決
- 重寫JS中的apply,call,bind,new方法JSAPP
- iOS逆向之旅(進階篇) — 重簽名APP(二)iOSAPP
- iOS逆向之旅(進階篇) — 重簽名APP(一)iOSAPP
- swiper loop:true引發繫結dom的click事件無效及解決方案OOP事件
- click事件形成的條件 – Eric事件
- onclick與click事件的區別事件
- JavaScript click 事件JavaScript事件
- click事件生成事件
- Android 進階/面試 重難點Android面試
- React 進階(四)事件詳解React事件
- 前端工程師做事的三重境界:我的進階之路前端工程師
- Android中點選事件的四種寫法詳解Android事件
- touch事件和click事件多次觸發的問題事件
- Android進階(七)Android中的ClassLoaderAndroid
- JTalk《0325第四期-Android進階之旅》總結Android
- touch事件與click事件區別事件
- 高階 Android 工程師的進階之路Android工程師
- 被蘋果11.2警告的解決方案蘋果
- 解決pycharm警告InsecureRequestWarning:PyCharm
- ORA-15096: lost disk write detected的解決與修復
- 動態生成的html元素繫結click事件HTML事件
- 你的Linux進階之旅,終點是何處?Linux
- When should reread of cl_crm_bol_entity and $scope.$apply be called manuallyAPP
- 我的python之旅Python
- Android 進階之旅 | JTalk 掘金線下活動第四期Android
- Android:ListView的擴充與進階AndroidView
- Fragment中的那些坑——Android進階FragmentAndroid
- Android進階:十四、熟悉Android打包編譯的流程Android編譯
- 在ubuntu的QtCreator 出現提示Wayland警告UbuntuQT
- click事件在蘋果手機失效的問題事件蘋果
- RocketMQ的invokeSync call timeout異常的解決辦法MQ