美文网首页Android自定义View
Android 源码剖析:View 的 Touch 事件分发

Android 源码剖析:View 的 Touch 事件分发

作者: ImWiki | 来源:发表于2019-08-01 00:48 被阅读2次

    这篇文章讲 View 的 Touch 事件分发,内容比较简单。是从源码去分析 View 的 onTouchEventsetOnClickListener.onClicksetOnTouchListener.onTouchsetOnTouchListener.onTouch``中的布尔返回值关系。

    Android 手势

    手指触发到 View 到离开手指主要经历了三个阶段,分别是:

    • ACTION_DOWN(0) 手指按下时
    • ACTION_MOVE(2) 手指移动时
    • ACTION_UP(1) 手指离开时
    复写代码

    我们新建一个TouchView开启今天的实验,当我们复写onTouchEvent方法或者调用setOnTouchListener方法时候,Android Studio 有警告提示,提示意思就是我们也应该复写performClick方法。

    Custom view TouchView overrides onTouchEvent but not

    Custom view TouchView has setOnTouchListener called on it but does not override performClick

    既然是提示必定有它的道理,那么我们根据要求也复写了performClick

    TouchView#onTouchEvent should call TouchView#performClick when a click is detected more...

    onTouch should call View#performClick when a click is detected more...

    我们复写了performClick,却提示另外一个警告,意思就是说我们应该在onTouchEventonTouch方法中去调用performClick,那么这个也是我们今天所需要研究的一个问题之一,为何要在这两个方法中调用performClick

    测试事件分发的顺序
    public class TouchView extends View {
        ...略去构造方法
        public static String toActionString(int action) {
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                    return "ACTION_DOWN";
                case MotionEvent.ACTION_UP:
                    return "ACTION_UP";
                case MotionEvent.ACTION_MOVE:
                    return "ACTION_MOVE";
            }
            return null;
        }
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            Log.e("onTouchEvent", TouchView.toActionString((event.getAction())));
            return super.onTouchEvent(event);
        }
        @Override
        public boolean dispatchTouchEvent(MotionEvent event) {
            Log.e("dispatchTouchEvent", TouchView.toActionString((event.getAction())));
            return super.dispatchTouchEvent(event);
        }
        @Override
        public boolean performClick() {
            Log.e("performClick", "performClick");
            return super.performClick();
        }
    
            TouchView touchView = findViewById(R.id.touch_view);
            touchView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Log.e("onClick","onClick");
                }
            });
            touchView.setOnTouchListener(new View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    Log.e("onTouch", TouchView.toActionString((event.getAction())));
                    return false;
                }
            });
    

    上面我们复写了onTouchEventdispatchTouchEventperformClick,调用实现了setOnClickListenersetOnTouchListener方法,手指点击 View 然后离开,看看打印的顺序。

    注意:下面的例子我是刻意稍稍停留手指才离开,如果是点击手指马上离开,是不会有ACTION_MOVE

    dispatchTouchEvent: ACTION_DOWN
    onTouch: ACTION_DOWN
    onTouchEvent: ACTION_DOWN
    dispatchTouchEvent: ACTION_MOVE
    onTouch: ACTION_MOVE
    onTouchEvent: ACTION_MOVE
    dispatchTouchEvent: ACTION_UP
    onTouch: ACTION_UP
    onTouchEvent: ACTION_UP
    performClick: performClick
    onClick: onClick
    

    可以看到事件的分发是从dispatchTouchEvent > onTouch > onTouchEvent > performClick
    当把onTouch的返回值从false 改成 true,再次运行。

    dispatchTouchEvent: ACTION_DOWN
    onTouch: ACTION_DOWN
    dispatchTouchEvent: ACTION_MOVE
    onTouch: ACTION_MOVE
    dispatchTouchEvent: ACTION_UP
    onTouch: ACTION_UP
    

    当把dispatchTouchEvent的返回值改成 true,再次运行。

    dispatchTouchEvent: ACTION_DOWN
    dispatchTouchEvent: ACTION_MOVE
    dispatchTouchEvent: ACTION_UP
    

    可以看到当 onTouch 返回 true 的时候,onTouchEvent 和 performClick 和 onClick 都不再调用,这个也是为何IDE会提示我们为何要复写并调用performClick的原因,如果我们自己处理手势相关的问题,那么点击的事件也应该由我们自行去分发,避免点击无效,除非我们不需要点击事件。我们一会儿会去源代码分析 onTouch 的返回值 对 onTouchEvent的影响。

    带着问题出发

    下面就带着这三个问题出发,阅读源代码,理解 View 的事件分发过程。

    1. onClick 和 onLongClick 的实现原理。
    2. dispatchTouchEvent > onTouch > onTouchEvent > performClick的过程,事件是如何是层层被消费掉的?
    3. onLongClick 的返回值对onClick的影响?
    4. 实验案例:实现当触摸超过5秒钟,点击事件无效。

    源码分析 dispatchTouchEvent

        public boolean dispatchTouchEvent(MotionEvent event) {
            ... 略去无关代码
            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;
                }
            }
            ... 略去无关代码
            return result;
        }
    

    由于本文只分析触摸上面提出的几个问题,所以略去部分不相干的代码,这个方法就变得非常简单了,这个代码当中可以看到li.mOnTouchListener.onTouch(this, event)返回true时候,result就变成了true,如果result = true,那么就不再调用onTouchEvent(event)方法,所以就解析了,onTouch为何会拦截掉onTouchEvent。

    源码分析 onTouchEvent

    这个方法比较复杂,View 大多数手势相关的操作都是在这里完成,onClick 和 onLongClick 都是在这个方法实现,由于代码很多,所以我就精简保留了重要的代码,和原来方法差别有点大。

        public boolean onTouchEvent(MotionEvent event) {
            // final float x = event.getX();
            // final float y = event.getY();
            // final int viewFlags = mViewFlags;
            final int action = event.getAction();
    
            final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
    
            if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
                switch (action) {
                    case MotionEvent.ACTION_UP:
                        if (!clickable) {
                            removeTapCallback();
                            removeLongPressCallback();
                            mInContextButtonPress = false;
                            mHasPerformedLongPress = false;
                            mIgnoreNextUpEvent = false;
                            break;
                        }
                        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();
                            }
                            // 如果 长按事件执行了并且返回true,那么点击事件将会不再生效
                            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)) {
                                        performClickInternal();
                                    }
                                }
                            }
                            removeTapCallback();
                        }
                        break;
    
                    case MotionEvent.ACTION_DOWN:
                        if (!clickable) {
                            checkForLongClick(0, x, y);
                            break;
                        }
                        // 判断是否在滚动的容器中
                        boolean isInScrollingContainer = isInScrollingContainer();
    
                        // 对于滚动容器中的视图,将按下的反馈延迟一段时间,以防这是滚动。
                        if (isInScrollingContainer) {
                            mPrivateFlags |= PFLAG_PREPRESSED;
                            if (mPendingCheckForTap == null) {
                                mPendingCheckForTap = new CheckForTap();
                            }
                            mPendingCheckForTap.x = event.getX();
                            mPendingCheckForTap.y = event.getY();
                            postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                        } else {
                            // 如果不是在一个滚动的容器中,立即显示反馈
                            setPressed(true, x, y);
                            checkForLongClick(0, x, y);
                        }
                        break;
                    case MotionEvent.ACTION_MOVE:
                        // 判断手指是否已经离开的当前 View 的区域,如果离开了就移除长按相关的事件
                        if (!pointInView(x, y, mTouchSlop)) {
                            removeTapCallback();
                            removeLongPressCallback();
                        }
                        break;
                }
                return true;
            }
            return false;
        }
    
    

    CheckForTap除了处理Pressed事件,最重要就是和CheckForLongPress一样是处理长按事件,CheckForTap 的使用是在可滚动的容器中使用,通过延迟 100 毫秒,判断避免手指只是用于滑动。

    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());

        private final class CheckForTap implements Runnable {
            public float x;
            public float y;
    
            @Override
            public void run() {
                mPrivateFlags &= ~PFLAG_PREPRESSED;
                setPressed(true, x, y);
                checkForLongClick(ViewConfiguration.getTapTimeout(), x, y);
            }
        }
    

    调用checkForLongClick并不会马上触发onLongClick事件,而是延迟 500 毫秒才执行。

        private void checkForLongClick(int delayOffset, float x, float y) {
            if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {
                mHasPerformedLongPress = false;
    
                if (mPendingCheckForLongPress == null) {
                    mPendingCheckForLongPress = new CheckForLongPress();
                }
                mPendingCheckForLongPress.setAnchor(x, y);
                mPendingCheckForLongPress.rememberWindowAttachCount();
                mPendingCheckForLongPress.rememberPressedState();
                postDelayed(mPendingCheckForLongPress,
                        ViewConfiguration.getLongPressTimeout() - delayOffset);
            }
        }
    

    onLongClick返回true的时候,mHasPerformedLongPress 就变成了 true,这个属性会影响 onTouchEvent的,onClick事件将会不再调用。

        private final class CheckForLongPress implements Runnable {
            ...
            @Override
            public void run() {
                if ((mOriginalPressedState == isPressed()) && (mParent != null)
                        && mOriginalWindowAttachCount == mWindowAttachCount) {
                    if (performLongClick(mX, mY)) {
                        mHasPerformedLongPress = true;
                    }
                }
            }
            ...
        }
    
    总结

    从上一节onTouchEvent的源码,我们可以得知,实际上在按下去的时候就已经添加了长按事件(如果是在可以滚动的父容器,会先延迟100毫秒添加长按事件)但是并没有立刻执行,而是延迟500毫秒,如果在500毫秒内,手指离开了 View 的区域,将会取消分发长按事件,否则长按事件就会正常执行。当长按事件返回 true 时候,onClick 就不再执行。

    相关文章

      网友评论

        本文标题:Android 源码剖析:View 的 Touch 事件分发

        本文链接:https://www.haomeiwen.com/subject/vrvblctx.html