Android自定义View

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

2019-08-01  本文已影响2人  ImWiki

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

Android 手势

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

复写代码

我们新建一个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 就不再执行。

上一篇下一篇

猜你喜欢

热点阅读