View 的事件体系

一、View 基础知识

1.1、什么是 View

View 是 Android 中所有控件的基类,不管是简单的 Button 和 TextView 还是复杂的 RelativeLayout 和 ListView,它们的共同基类都是 View。ViewGroup 内部包含了许多个控件,即一组 View,ViewGroup 也继承了 View,这就意味着 View 本身就可以是单个控件也可以是由多个控件组成的一组控件,通过这种关系就形成了 View 树的结构。ViewGroup 内部可以有子 View,这个子 View 同样还可以是 ViewGroup,以此类推。

1.2、View 的位置参数

View 的位置主要由它的四个顶点来决定,分别对应于 View 的四个属性:top、left、right、bottom。这些坐标都是相对于 View 的父容器来说的,是一种相对坐标。在 Android 中,x 轴和 y 轴的正方向分别为右和下。

  • left = getLeft();
  • right = getRight();
  • top = getTop();
  • bottom = getBottom();

从 Android 3.0 开始,View 增加了额外的几个参数:x、y、translationX 和 translationY,其中 x 和 y 是 View 左上角的坐标,而 translationX 和 translationY 是 View 左上角相对于父容器的偏移量。这几个参数也是相对于父容器的坐标,并且 translationX 和 translationY 的默认值是 0。

  • x = getX();
  • y = getY();
  • translationX = getTranslationX();
  • translationY = getTranslationY();

上面所说的位置参数,View 都提供了 get/set 方法。需要注意的是,View 在平移的过程中,top 和 left 表示的是原始左上角的位置信息,其值并不会发生改变,此时发生改变的是 x、y、translationX 和 translationY 这个四个参数。

1.3、MotionEvent 和 TouchSlop

一、MotionEvent

在手指接触屏幕后所产生的一系列事件中,典型的事件类型有如下几种:

  • ACTION_DOWN:手指刚接触屏幕。
  • ACTION_MOVE:手指在屏幕上移动。
  • ACTION_UP:手指从屏幕上离开的一瞬间。

正常情况下,一次手指触摸屏幕的行为会触发一系列点击事件,如下:

  • 点击屏幕后立即离开:DOWN -> UP。
  • 点击屏幕滑动一会再松开:DOWN -> MOVE ->UP。

通过 MotionEvent 对象我们可以得到点击事件发生的 x 和 y 坐标。为此,系统提供了两组方法:

  • getX/getY:返回相对于当前 View 左上角的 x 和 y 坐标。
  • getRawX/getRawY:返回相对于手机屏幕左上角的 x 和 y坐标。

二、TouchSlop

TouchSlop 是系统所能识别出的被认为是滑动的最小距离,换句话说,当手指在屏幕上滑动时,如果两次滑动之间的距离小于这个常量,那么系统就不认为是在进行滑动操作。这是一个常量,和设备有关,在不同设备上这个值可能是不同的,通过 ViewConfiguration.get(getContext()).getScaledTouchSlop(); 可以获取这个常量。当我们在处理滑动时,可以利用这个常量来做一些过滤,比如当两次滑动事件的滑动距离小于这个值,我们就可以认为未达到滑动距离的临界值,因此就可以认为它们不是滑动,这样做可以有更好的用户体验。

1.4、VelocityTracker、GestureDetector 和 Scroller

一、VelocityTracker

速度追踪,用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度。 使用过程很简单,首先,在 View 的 onTouchEvent 方法中追踪当前单击事件的速度:

        VelocityTracker velocityTracker = VelocityTracker.obtain();
        velocityTracker.addMovement(event);

接着就可以获得当前的速度:

        velocityTracker.computeCurrentVelocity(1000);
        int xVelocity = (int) velocityTracker.getXVelocity();
        int yVelocity = (int) velocityTracker.getYVelocity();

需要注意两点:第一点,获取速度之前必须先计算速度,即 getXVelocity 和 getYVelocity 这两个方法的前面必须要调用 computeCurrentVelocity 方法;第二点,这里的速度是指一段时间内手指所滑过的像素数,比如将时间间隔设为 1000 时,在 1s 内,手指在水平方向从左向右滑过 100 像素,那么水平速度就是 100。注意速度可以为负数,当手指从右往左滑动时,水平方向速度即为负值。速度的计算可以用如下公式来表示:

速度 = (终点位置 - 起点位置)/ 时间段

根据上面的公式在加上 Android 系统的坐标系,可以知道,手指逆着坐标系的正方向滑动,所产生的的速度就为负值。另外,computeCurrentVelocity 这个方法的参数表示的是一个时间单元或者说时间间隔,单位是 ms,计算速度时得到的速度就是在这个时间间隔内手指在水平或竖直方向上所滑动的像素数。

最后,当不需要使用的时候,要调用 clear 方法来重置并回收内存:

        velocityTracker.clear();
        velocityTracker.recycle();

二、GestureDetector

手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。要使用 GesturDetector,首先让 View 实现 GestureDetector.OnGestureListener 接口,然后创建一个 GestureDetector 对象。如果需要监听双击行为,那么就实现 GestureDetector.OnDoubleTapListener 接口。

        mGestureDetector = new GestureDetector(context, this);
        //解决长按屏幕无法拖动的现象
        mGestureDetector.setIsLongpressEnabled(false);

接着,接管目标 View 的 onTouchEvent 方法,在待监听 View 的 onTouchEven 方法中添加如下实现。

        boolean consume = mGestureDetector.onTouchEvent(event);
        return consume;

做完了以上两步,就可以有选择的实现 OnGestureListener 和 OnDoubleTapListener 中的方法了。

方法名描述所属接口
onDown手指触摸屏幕的一瞬间,由 1 个 ACTION_DOWN 触发OnGestureListener
onShowPress手指触摸屏幕,尚未松开或拖动,由 1 个 ACTION_DOWN 触发 *注意和 onDown 的区别,它强调的是没有松开或者拖动的状态OnGestureListener
onSingleTapUp手指松开,由 1 个 ACTION_UP 触发,这是单击行为OnGestureListener
onScroll手指按下屏幕并拖动,由 1 个 ACTION_DOWN,多个 ACTION_MOVE 触发,这是拖动行为OnGestureListener
onLongPress用户长久地按着屏幕不放,即长按OnGestureListener
onFiling用户按下触摸屏、快速滑动后松开,由 1 个 ACTION_DOWN、多个 ACTION_MOVE 和 1 个 ACTION_UP 触发,这是快速滑动行为OnGestureListener
onDoubleTap双击,由 2 次连续的单击组成,它不可能和 onSingleTapConfirmed 共存OnDoubleTapListener
onSingleTapConfirmed严格的单击行为 *注意它和 onSingleTapUp 的区别,如果触发了 onSingleTapConfirmed,那么后面不可能再紧跟着另一个单击行为,即这只可能是单击,而不可能是双击中的一次单击OnDoubleTapListener
onDoubleTapEvent表示发生了双击行为,在双击的期间,ACTION_DOWN、ACTION_MOVE 和 ACTION_UP 都会触发此回调OnDoubleTapListener

上表中方法有很多,日常开发中并不是所有的方法都会被时常用到,可根据需求选择。比较常用的有:onSingleTapUP(单击)、onFling(快速滑动)、onScroll(拖动)、onLongPress(长按)和 onDoubleTap(双击)。另外我们在实际开发中,可以不使用 GestureDetector,而只在 View 的 onTouchEvent 方法中实现所需的监听。比如如果只是监听滑动相关的,建议自己在 onTouchEvent 中实现,如果要监听双击这种行为,那么就是用 GestureDetector。

三、Scroller

弹性滑动对象,用于实现 View 的弹性滑动。当使用 View 的 scrollTo/scrollBy 方法来进行滑动时,其过程是瞬间完成的,这个没有过渡效果的滑动用户体验很不好。这个时候就可以使用 Scroller 来实现有过渡效果的滑动,其过程不是瞬间完成的,而是在一定的时间间隔内完成的。Scroller 本身无法让 View 弹性滑动,它需要和 View 的 computeScroll 方法配合使用才能共同完成这个功能。使用它的典型代码是固定的,代码如下:

        mScroller = new Scroller(context);
  
    // 缓慢滑动到指定位置
    private void smoothScrollTo(int destX, int destY) {
        int scrollX = getScrollX();
        int scrollY = getScrollY();
        // 1000ms 内滑向 destX,效果就是慢慢滑动
        mScroller.startScroll(scrollX, 0, delta, 0, 1000);
        invalidate();
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
        }
    }

二、View 的滑动

在 Android 设备上,滑动几乎是应用的标配,不管是下拉刷新还是 SlidingMenu 侧拉菜单,它们的基础都是滑动。通过三种方式可以实现滑动:第一种是通过 View 本身提供的 scrollTo/scrollBy 方法来实现滑动;第二种是通过动画给 View 施加平移效果来实现滑动;第三种是通过改变 View 的 LayouParams 使得 View 重新布局从而实现滑动。

2.1、使用 scrollTo/scrollBy

为了实现 View 的滑动,View 提供了专门的方法来实现这个功能,那就是 scrollTo 和 scrollBy,源码如下:

    public void scrollTo(int x, int y) {
        if (mScrollX != x || mScrollY != y) {
            int oldX = mScrollX;
            int oldY = mScrollY;
            mScrollX = x;
            mScrollY = y;
            invalidateParentCaches();
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            if (!awakenScrollBars()) {
                postInvalidateOnAnimation();
            }
        }
    }
    public void scrollBy(int x, int y) {
        scrollTo(mScrollX + x, mScrollY + y);
    }

从源码可以看出,scrollBy 实际上也是调用了 scrollTo 方法,它实现了基于当前位置的相对滑动,而 scrollTo 则实现了基于原始位置的绝对滑动。scrollTo 和 scrollBy 移动的并不是 View 本身,而是移动的 View 的内容。如果想要移动 View 本身,需要写成 ((View) getParent()).scrollTo 或者 ((View) getParent()).scrollBy,本质就是将 View 作为了其父控件的内容。

在 View 中,有两个关于滑动的属性,mScrollX 和 mScrollY,它们分别可以通过 getScrollX 和 getScrollY 方法得到,是 View 本身相对于其内容的偏移量。我们使用 scrollTo 或 scrollBy 时,也是 View 本身相对于其 内容的滑动,所以要理解滑动的正负值。

在这里插入图片描述

2.2、使用动画

待更。。。

2.3、改变布局参数

待更。。。

三、弹性滑动

关于弹性滑动有一个共同的思想:将一次大的滑动分成若干次小的滑动并在一个时间内完成。

3.1、使用 Scroller

下面是 Scroller 的典型使用方法。

        mScroller = new Scroller(context);
        
    private void smoothScrollTo(int destX) {
        int scrollX = getScrollX();
        int deltaX = destX - scrollX;
        //1000ms 内滑向 destX
        mScroller.startScroll(scrollX, 0, deltaX, 0, 1000);
        invalidate();
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            Log.e("yl", "computeScroll");
            //移动内容
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            //移动本身
            //((View) getParent()).scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
        }
    }

查看 startScroll 的源码,可以看出,调用 startScroll 方法时 Scroller 内部其实什么也没做,它只是保存了我们传递的几个参数。那么 Scroller 到底是如何让 View 弹性滑动的呢?其过程是:当 View 重绘后会在 draw 方法中调用 computeScroll,而 computeScroll 又会去向 Scroller 获取当前的 scrollX 和 scrollY;然后通过 scrollTo 方法实现滑动;接着又调用 postInvalidate 方法来进行第二次重绘,还是会导致 computeScroll 方法被调用。postInvalidate 和 invalidate 效果一样,它们的区别是 postInvalidate 是可以在非 UI 线程中去调用刷新UI的,其实就是利用的 Handler,从下面源码中可以看出;然后继续想 Scroller 获取当前的 scrollX 和 scrollY,并通过 scrollTo 方法滑动到新的位置,如此反复,直到整个滑动过程结束。

    public void startScroll(int startX, int startY, int dx, int dy, int duration) {
        mMode = SCROLL_MODE;
        mFinished = false;
        mDuration = duration;
        mStartTime = AnimationUtils.currentAnimationTimeMillis();
        mStartX = startX;
        mStartY = startY;
        mFinalX = startX + dx;
        mFinalY = startY + dy;
        mDeltaX = dx;
        mDeltaY = dy;
        mDurationReciprocal = 1.0f / (float) mDuration;
    }
    public void invalidate() {
        invalidate(true);
    }

    public void postInvalidate() {
        postInvalidateDelayed(0);
    }
    
    public void postInvalidateDelayed(long delayMilliseconds) {
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
        }
    }
    
    public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
        Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
        mHandler.sendMessageDelayed(msg, delayMilliseconds);
    }

接下来说一说 Scroller 的 computeScrollOffset 方法。这个方法会根据时间的流逝来计算出当前的 scrollX 和 scrollY 的值。计算方法也很简单,大意就是根据时间流逝的百分比来算出 scrollX 和 scrollY 改变的百分比并计算出当前的值,这个过程类似于动画中的插值器的概念,关于插值器可以自行查询。这个方法的返回值也很重要,返回 true 表示滑动还未结束,false 则表示滑动已经结束,因此当这个方法返回 true 时,我们需要继续进行 View 的滑动。

通过上面的分析,来做一个概括:Scroller 本身并不能实现 View 的滑动,它需要配合 View 的 computeScroll 方法才能完成弹性滑动的效果,它不断地让 View 重绘,而每一次重绘距滑动起始时间会有一个时间间隔,通过这个时间间隔 Scroller 就可以计算出 View 当前的滑动位置,知道了滑动位置就可以通过 scrollTo 方法来完成 View 的滑动。就这样,View 的每一次重绘都会导致 View 进行小幅度的 滑动,而多次的小幅度滑动就组成了弹性滑动,这就是 Scroller 的工作机制。

3.2、通过动画

待更。。。

3.3、使用延时策略

使用延时策略实现弹性滑动的方法,核心思想是通过发送一系列延时消息从而达到一种渐进式的效果,具体来说可以使用 Handler 或 View 的 postDelayed 方法,也可以使用线程的 sleep 方法。对于 postDelay 方法来说,可以通过它来延时发送一个消息,然后在消息中来进行 View 的滑动,如果接连不断地发送这种延时消息,那么就可以实现弹性滑动的效果。对于 sleep 方法来说,通过在 while 循环中不断地滑动 View 和 sleep,就可以实现弹性滑动的效果。

四、View 的事件分发机制

4.1、点击事件的传递规则

所谓的点击事件的事件分发,其实就是对 MotionEvent 事件的分发过程,即当一个 MotionEvent 产生了以后,系统需要把这个事件传递给一个具体的 View,而这个传递的过程就是分发过程。点击事件的分发过程由三个很重要的方法来共同完成:dispatchTouchEvent、onInterceptTouchEvent 和 onTouchEvent。其中 onInterceptTouchEvent 方法只有 ViewGroup 中存在;在 ViewGroup 重写了 View 的 dispatchTouchEvent 方法;onTouchEvent 方法在 View 中实现。

对于一个根 ViewGroup 来说,点击事件产生后,首先会传递给它,这时它的 dispatchTouchEvent 就会被调用,如果这个 ViewGroup 的 onInterceptTouchEvent 方法返回 true 就表示它要拦截事件,接着事件就会交给这个 ViewGroup 处理,即它的 onTouchEvent 方法就会被调用;如果这个 ViewGroup 的 onInterceptTouchEvent 方法返回 false,就表示它不拦截事件,这时当前事件就会继续传递给它的子元素,接着子元素的 dispatchTouchEvent 方法就会被调用,如此反复直到事件被最终处理。

当一个 View 需要处理事件时,如果它设置了 OnTouchListener,那么 OnTouchListener 中的 onTouch 方法会被回调。这时事件如何处理还要看 onTouch 的返回值,如果返回 false,则当前 View 的 onTouchEvent 方法会被调用;如果返回 true,那么 onTouchEvent 方法将不会被调用。由此可见,给 View 设置的 OnTouchListener,其优先级比 onTouchEvent 要高。在 onTouchEvent 方法中,如果当前设置的有 OnClickListener,那么它的 onClick 方法会被调用。可以看出,平时我们常用的 OnClickListener,其优先级最低,即处于事件传递的尾端。

当一个点击事件产生后,它的传递过程遵循如下顺序:Activity -> Window -> View,即事件总是先传递给 Activity,Activity 再传递给 Window,最后 Window 再传递给顶级 View,顶级 View 接收到事件后,就会按照事件分发机制去分发事件。考虑一种情况,如果一个 View 的 onTouchEvent 返回 false,那么它的父容器的 onTouchEvent 将会被调用,以此类推。如果所有元素都不处理这个事件,那么这个事件将会最终传递给 Activity 处理,即 Activity 的 onTouchEvent 方法会被调用。

  1. 同一个事件序列是从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的的一系列事件,这个事件序列以 down 事件开始,中间含有数量不定的 move 事件,最终以 up 事件结束。
  2. 正常情况下,一个事件序列只能被一个 View 拦截且消耗。因为同一个事件序列不能分别由两个 View 同时处理,但是通过特殊手段可以做到,比如一个 View 将本该自己处理的事件通过 onTouchEvent 强行传递给其他 View 处理。
  3. 某个 View 一旦决定拦截,那么这一个事件序列都只能由它来处理(如果事件序列能到传递给它的话),并且它的 onInterceptTouchEvent 不会再被调用。
  4. 某个 View 一旦开始处理事件,如果它不消耗 ACTION_DOWN 事件(onTouchEvent 返回了 false),那么同一事件序列中的其他事件都不会再交给它来处理,并且事件将重新交由它的父元素去处理,即父元素的 onTouchEvent 会被调用。
  5. 如果 View 不消耗除 ACTION_DOWN 以外的其他事件,那么这个点击事件会消失,此时父元素的 onTouchEvent 并不会被调用,并且当前 View 可以持续收到后续的事件,最终这些消失的点击事件会传递给 Activity 处理。
  6. ViewGroup 默认不拦截任何事件。Android 源码中 ViewGroup 的 onInterceptTouchEvent 方法默认返回 false。
  7. View 没有 onInterceptTouchEvent 方法,一旦有点击事件传递给它,那么它的 onTouchEvent 方法就会被调用。
  8. View 的 onTouchEvent 默认都会消耗事件(返回 true),除非它是不可点击的(clickable 和 longClickable 同时为 false)。View 的 longClickable 属性默认都为 false,clickable 属性要分情况,比如 Button 的 clickable 属性默认为 true,而 TextView 的 clickable 属性默认为 false。
  9. View 的 enable 属性不影响 onTouchEvent 的默认返回值。哪怕一个 View 是 disable 状态的,只要它的 clickable 或者 longClickable 有一个为 true,那么它的 onTouchEvent 就返回 true。
  10. onClick 会发生的前提是当前 View 是可点击的,并且它收到了 down 和 up 的事件。
  11. 事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子 View,通过 requestDisallowInterceptTouchEvent 方法可以在子元素中干预父元素的事件分发过程,但是 ACTION_DOWN 事件除外。

五、View 的滑动冲突

5.1、常见的滑动冲突场景

  • 场景1——外部滑动方向和内部滑动方向不一致。
  • 场景2——外部滑动方向和内部滑动方向一致。
  • 场景3——上面两种情况的嵌套。
    在这里插入图片描述

场景:1:ViewPager 和 Fragment 配合使用时,左右滑动切换页面,每个页面中往往又是一个上下滑动的列表。这种情况本来是有滑动冲突的,由于 ViewPager 内部已经做了处理,实际开发中就不需要再处理滑动冲突。如果采用的不是 ViewPager 而是 ScrollView 等,那么就需要手动处理滑动冲突,否则就会出现内外两层只有一层能够滑动。除了这种典型情况,还存在其他情况,比如外部上下滑动、内部左右滑动等,但是属于同一类滑动冲突。

场景2:当内外两层都在同一个方向可以滑动时,会存在逻辑问题。因为当手指开始滑动时系统无法知道用户想要哪一层滑动,所以当手指滑动时就会出现问题,要么只有一层能滑动,要么就是内外两层都滑动得很卡顿。实际开发中,这种场景主要是指内外两层同时能左右滑动或者内外两层同时能上下滑动。

场景3:上面两种场景的嵌套。比如,内层有一个场景 1 中的滑动效果,外层又有一个场景 2 中的滑动效果。虽然说这种嵌套的滑动冲突看起来更复杂,但是它是几个单一的滑动冲突的叠加,因此只需要分别处理内层、中层和外层之间的滑动冲突即可,而具体的处理方法其实是和场景 1、场景 2 相同的。

5.2、滑动冲突的处理规则

场景1:当用户左右滑动时,需要让外部的 View 拦截点击事件,当用户上下滑动时,需要让内部的 View 拦截点击事件。此时就可以根据它们的特征(水平滑动还是竖直滑动)来解决滑动冲突,比如根据滑动路径和水平方向所形成的夹角判断,也可以根据水平方向和竖直方向上的距离差判断,某些特殊时候还可以根据水平和竖直方向的速度差来做判断。

场景2:这种情况无法根据滑动的角度、距离或者速度差来做判断,只能根据业务需求,比如当处于某种状态时需要外部 View 响应用户的滑动,而处于另外一种状态时需要内部 View 来响应滑动。即根据业务需求得出相应的处理规则。

场景3:嵌套的滑动规则更加复杂,跟场景 2 一样,更加需要根据业务需求才能得出相应的处理规则。

5.3、滑动冲突的解决方式

针对上面场景1中的滑动,我们可以根据滑动的距离差来判断,这个距离差就是所谓的滑动规则。其实在滑动过程中得到滑动的角度是很简单的,但是如果需要将点击事件交给合适的 View 来处理,就需要用到事件分发机制。针对滑动冲突,这里给出两种解决滑动冲突的方式:外部拦截法和内部拦截法。

只要根据场景 1 的情况来得出通用的解决方案,那么对于场景 2 和场景 3 来说我们只需要修改相关滑动规则的逻辑即可。

1、外部拦截法

所谓外部拦截法是指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要此事件就不拦截,这样就可以解决滑动冲突的问题,这种方法比较符合点击事件的分发机制。外部拦截法需要重写父容器的 onInterceptTouchEvent 方法,在内部做相应的拦截即可,这种方式伪代码如下。

    // 父容器
    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        var interceptor = false
        val x = ev?.x
        val y = ev?.y
        when (ev?.action) {
            MotionEvent.ACTION_DOWN -> {
                interceptor = false   // 必须不拦截,否则后续事件无法再传递给子元素
            }
            MotionEvent.ACTION_MOVE -> {
                if (横向滑动){       //父容器需要当前点击事件
                    interceptor=true
                }else{
                    interceptor=false
                }
            }
            MotionEvent.ACTION_UP -> {
                interceptor=false
            }
        }
        return interceptor
    }

上述代码是外部拦截法的典型逻辑,针对不同的滑动冲突,只需要修改父容器需要当前点击事件的条件即可,其他均不需要做修改也不能修改。

2、内部拦截法

内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交由父容器处理,这种方法和 Android 中的事件分发机制不一致,需要配合 requestDisallowInterceptTouchEvent 方法才能正常工作,使用起来教外部拦截法稍显复杂。我们需要重写子元素的 dispatchTouchEvent 方法,伪代码如下。

    // 子元素
    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        val x = ev?.x
        val y = ev?.y
        when (ev?.action) {
            MotionEvent.ACTION_DOWN -> {
                parent.requestDisallowInterceptTouchEvent(true)
            }
            MotionEvent.ACTION_MOVE -> {
                if (横向滑动) {       // 交给父容器
                    parent.requestDisallowInterceptTouchEvent(false)
                } else {
                    // 自己消耗掉
                }
            }
            MotionEvent.ACTION_UP -> {
            }
        }
        return super.dispatchTouchEvent(ev)
    }
    // 父容器
    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        return ev?.action != MotionEvent.ACTION_DOWN
    }

上述代码是内部拦截法的典型代码,当面对不同的滑动策略时只需要修改里面的条件即可,其他不需要做修改也不能修改。除了子元素需要做处理外,父元素也要默认拦截除了 ACTION_DOWN 以外的其他事件,这样当子元素调用 parent.requestDisallowInterceptTouchEvent(false) 时,父元素才能继续拦截所需的事件。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值