【我的Android进阶之旅】解决重写onTouch事件提示的警告:onTouch should call View#performClick when a click is detected

版权声明:本文为【欧阳鹏】原创文章,欢迎转载,转载请注明出处! 【http://blog.csdn.net/ouyang_peng】 https://blog.csdn.net/qq446282412/article/details/82563779

一、问题描述

当你对一个控件(例如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;
        }
    }
![这里写图片描述](https://img-blog.csdn.net/20180910001405649?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxNDQ2MjgyNDEy/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)

这样在合适的时机处调用一下 View#performClick方法之后,错误的警告提示也不存在了。

我们继续思考下,这一次将ImageView设置一个onTouchListener并且重写onTouch方法,返回值为true, 此时的ImageView设置的OnClickListener还会处理点击事件吗? ![这里写图片描述](https://img-blog.csdn.net/20180909234940104?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxNDQ2MjgyNDEy/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70) 现在再重新运行程序,试一试,如下所示: ![这里写图片描述](https://img-blog.csdn.net/20180910001759183?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxNDQ2MjgyNDEy/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)

四、总结

当对一个View既设置了setOnTouchListener()方法,又设置了setOnClickListener()方法的时候,记得在OnTouchListener的onTouch()方法里调用一下performClick()方法。
因为如果你重写了onTouch,并且返回值返回true的话,就有可能把performClick()方法给屏蔽了,这样这些点击操作就没办法完成了。

五、参考链接


作者:欧阳鹏 欢迎转载,与人分享是进步的源泉!
转载请保留原文地址:https://blog.csdn.net/ouyang_peng/article/details/82563779

☞ 本人QQ: 3024665621
☞ QQ交流群: 123133153
☞ github.com/ouyangpeng
[email protected]

如果本文对您有所帮助,欢迎您扫码下图所示的支付宝和微信支付二维码对本文进行打赏。

这里写图片描述

猜你喜欢

转载自blog.csdn.net/qq446282412/article/details/82563779