Android事件分发——onInterceptTouchEvent 不响应 MotionEvent.ACTION_MOVE 事件

最近空闲的时候喜欢看看之前的东西,温故而知新。发现一个之前忽略的问题。自己学着总结一下。这些年没有自己总结自己的知识体系是最大的失误。

问题

自己在自定义控件一个侧滑控件的时候发现,在一个继承了ViewGroup的自定义控件中,onInterceptTouchEvent没有响应MOVE事件和UP事件。
示例demo中,控件继承的是ViewGroup,

public class SlideView extends ViewGroup{
    ...
        @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {

        Log.v("qhh_slide", "============================== onInterceptTouchEvent ==============================");
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mXDown = ev.getRawX();
                mLastXMove = mXDown;
                Log.d("qhh_slide", ">>> onInterceptTouchEvent ACTION_DOWN mLastXMove " + mLastXMove);
                break;
            case MotionEvent.ACTION_MOVE: //需要好好总结事件分发
                mXMove = ev.getRawX();
                mLastXMove = mXMove;
                Log.d("qhh_slide", ">>> onInterceptTouchEvent ACTION_MOVE mLastXMove " + mLastXMove);
                float diff = Math.abs(mXMove - mXDown);
                if (diff >= mTouchSlop) {
                    return true;
                }
                break;
            case MotionEvent.ACTION_UP:
                Log.d("qhh_slide", ">>> onInterceptTouchEvent ACTION_UP mLastXMove " + mLastXMove);
                break;
            default:
                break;
        }

        return   super.onInterceptTouchEvent(ev);;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        Log.v("qhh_slide", "============================== onTouchEvent ==============================");
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                mXMove = event.getRawX();
                int srolledX = (int) (mLastXMove - mXMove);

                //Log.i("qhh_move", ">>> getScrollX() = " + getScrollX());

                /*if(getScrollX() + mViewWidth >= mRightBorder){
                    //scrollTo(mLeftBorder,0);
                    return true;
                }else if(getScrollX() + mRightChildW >= mLeftBorder){
                    //scrollTo(mRightBorder,0);
                    return true;
                }*/

                scrollBy(srolledX, 0);
                mLastXMove = mXMove;

                break;
            case MotionEvent.ACTION_UP:

                /*int rightThrehold = mViewWidth + mRightChildW / 2;
                Log.d("qhh_up", ">>> rightThrehold = " + rightThrehold + " , getScrollX() " + getScrollX());
                if (getScrollX() + mViewWidth >= rightThrehold) { //左滑
                    Log.d("qhh_up", ">>> left");
                    mScroller.startScroll(getScrollX(), 0, mRightChildW - getScrollX(), 0);
                } else {
                    mScroller.startScroll(getScrollX(), 0, -getScrollX(), 0);
                }*/

                float xUp = event.getRawX();

                float dx = xUp - mXDown;

                if(dx < 0){
                    mScroller.startScroll(getScrollX(), 0, mRightChildW - getScrollX(), 0);
                }else{
                    mScroller.startScroll(getScrollX(), 0, -getScrollX(), 0);
                }

                invalidate();

                break;
            default:
                break;
        }

        return super.onTouchEvent() ;
    }
}

单纯是这么写,就会出现 onInterceptTouchEvent中就没有办法响应MOVE事件。具体原因留在后边慢慢梳理分析。

问题分析

聚焦问题,先分析事件在 onInterceptTouchEvent 和 onTouchEvent中的传递关系以及影响。简单的梳理了一下事件传递的流程图,


图1-简易事件传递流程.jpg

分析

情形一:
onInterceptTouchEvent 返回值为 false ,则在 onInterceptTouchEvent 中只收到 down 事件,并且将事件传递到自身的 onTouchEvent 。onTouchEvent 返回 false,则事件down之后的事件,该view都不再响应,直接传递到更上一层的View进行处理。反之,返回的是true,则本身自己消费move、up等事件。

情形二:
onInterceptTouchEvent 返回值为 true , 而在 onIntercepterTouchEvent 中还是只收到 down 事件。并且其余的事件则不会往子控件分发传递,会在本身的 onTouchEvent 中进行消费。最后事件在onTouchEvent中的消费逻辑和情形一中的一样。

情形三:
最终会发现,onInterceptTouchEvent 没有响应 move 和 up 事件,这就是文章前面说的问题。但是记得在郭霖大佬的一篇自定义ViewPager的博客中 (https://blog.csdn.net/guolin_blog/article/details/48719871)是可以响应 move 和 up。还是老样子,先来画画图。

图2-情形三.jpg

通过情形三可以知道,要是想在 ParentView 中 onInterceptorTouchEvent 判断 MOVE 事件,则需要 ChildrenView 中的 onTouchEvent 返回 true 。那么问题来了,如果是 ParentView 和 ChildrenView 都是自己自定义的可以这样实现。但是自己只是负责自定义 ParentView 这一层,ChildrenView 随意放任何控件,该怎么处理呢?
然后看到 郭霖 大佬的博客文章,他确实是在 onInterceptorTouchEvent 中通过判断 MOVE 来拦截事件。但是我这里却拦截不到。这种情况就和子控件有关系了。对比了一下,他的子控件是 Button 而我的是 TextView 。那就来分析分析这两个控件。

Button 和 TextView 的源码分析

看了一下源码,主要关注两个控件的 onTouchEvent 。由于 Button控件是继承自 TextView控件,故而先看看TextView中的 onTouchEvent 。主要看看 onTouchEvent retrun true 的条件是什么。
在 TextView 源码中看到,onTouchEvent 有一处return 和 Editor 有关。

        final int action = event.getActionMasked();
        if (mEditor != null) {
            mEditor.onTouchEvent(event);

            if (mEditor.mSelectionModifierCursorController != null
                    && mEditor.mSelectionModifierCursorController.isDragAcceleratorActive()) {
                return true;
            }
        }

在看看 Editor 中类的解释

/**
 * Helper class used by TextView to handle editable text views.
 *
 * @hide
 */

在这里看到这个并不是我想要的答案,因为不可能所有的控件都会有这个 Editor ,因此不符合我预期想要的设置。在接着往下看,看到 final boolean superResult = super.onTouchEvent(event); , TextView 的 super 则是 View 中的 onTouchEvent ,接着跟进。在 View 中的 onTouchEvent 去查找,return true 的条件。
首先第一处看到的是和 clickable 有关的。

public boolean onTouchEvent(MotionEvent event) {
  //... 省略无关代码
  final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

  if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return clickable;
        }
  
  if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
      //...省略无关代码
      return true;
  }
}

再看看 CLICKABLE 在源码中的注释:

    /**
     * 

Indicates this view can be clicked. When clickable, a View reacts * to clicks by notifying the OnClickListener.

* {@hide} */ static final int CLICKABLE = 0x00004000;

这就是和点击事件有关的变量,然后再想到每个 View 都算是继承 View 的,并且都能通过 setClickable 设置该变量,响应点击事件。

    /**
     * Enables or disables click events for this view. When a view
     * is clickable it will change its state to "pressed" on every click.
     * Subclasses should set the view clickable to visually react to
     * user's clicks.
     *
     * @param clickable true to make the view clickable, false otherwise
     * @attr ref android.R.styleable#View_clickable
     * @see #isClickable()
     */
    public void setClickable(boolean clickable) {
        setFlags(clickable ? CLICKABLE : 0, CLICKABLE);
    }

可以知道,通过 setClickable 设置了 ChildrenView 的 clickable ,则ChildrenView 在 onTouchEvent 中是会返回 true 的。知道了这点,可以在进一步分析 TextView 和 Button 来验证我们的想法。
在 TextView 中的 onTouchEvent 中没有看到可以直接返回 true 的条件,并且看不到设置了 clickable 。Ctrl + F 了一下 clickable 关键字 。在 TextView 的构造方法中发现一段注释。

/*
         * Views are not normally clickable unless specified to be.
         * However, TextViews that have input or movement methods *are*
         * clickable by default. By setting clickable here, we implicitly set focusable as well
         * if not overridden by the developer.
         */
        a = context.obtainStyledAttributes(
                attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
        boolean canInputOrMove = (mMovement != null || getKeyListener() != null);
        boolean clickable = canInputOrMove || isClickable();
        boolean longClickable = canInputOrMove || isLongClickable();
        int focusable = getFocusable();

注释中解释,除非特殊指定,否则不能够单击Views。所以感觉这里应该是 TextView clickable 是 false。Button 是默认可点击的事件,进入源码看看。 然而 Button 的源码少得可怜。所以,又好奇那里设置了 clickable 呢?
看到构造函数中的注释以及代码,发现 Button 其实是由自己的 styles 的,但是点不进去,所以需要借助源码查看工具。使用线上 http://androidxref.com
找了一番,以及上网搜索了一番,该 style 在源码的路径

/frameworks/base/core/res/res/values/themes.xml

搜索找到 buttonStyle


@style/Widget.Button

在找到 Widget.Button ,路径在:/frameworks/base/core/res/res/values/styles.xml


至此,可以看到 Button 实际是在 style 文件中配置了 clickable 为 true 。

实际代码操作验证

在文章中最开始的 SlideView demo中修改一下代码。在 onLayout 的时候给每个子view设置了 clickable 。

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        Log.i("qhh", "onLayout changed " + changed);
        if (changed) {
            int childCount = getChildCount();
            int previousWidth = 0;
            for (int i = 0; i < childCount; i++) {
                View childView = getChildAt(i);

                /*childView.layout(i * childView.getMeasuredWidth() + previousWidth, 0,
                        (i + 1) * childView.getMeasuredWidth() + previousWidth,
                        childView.getMeasuredHeight());*/

                childView.setClickable(true);

                childView.layout(previousWidth, 0,
                        childView.getMeasuredWidth() + previousWidth,
                        childView.getMeasuredHeight());
                previousWidth += childView.getMeasuredWidth();
            }

            mLeftBorder = getChildAt(0).getLeft();
            mRightBorder = getChildAt(childCount - 1).getRight();
            mRightChildW = getChildAt(childCount - 1).getWidth();
            mViewWidth = getWidth();

            Log.i("qhh_move", ">>> mLeftBorder = " + mLeftBorder);
            Log.i("qhh_move", ">>> mRightChildW = " + mRightChildW);
            Log.i("qhh_move", ">>> width = " + mViewWidth);
        }

    }

这样之后,在 ParentView 中 onIntercepterTouchEvent 返回 false 的情况下可以响应到 move等事件。

参考的文章

https://blog.csdn.net/Zheng548/article/details/84028561
https://blog.csdn.net/xiayu98020214/article/details/80277835
https://blog.csdn.net/guolin_blog/article/details/48719871

你可能感兴趣的:(Android事件分发——onInterceptTouchEvent 不响应 MotionEvent.ACTION_MOVE 事件)