【学习】Android中View的事件体系(下)——滑动冲突

View的滑动冲突

常见的滑动冲突场景

1.外部滑动方向和内部滑动方向不一致
2.外部滑动方向和内部滑动方向一致
3.上面两种情况嵌套

在这里插入图片描述

1.主要是将ViewPager和Fragment配合使用所组成的页面滑动效果,主流应用几乎都会使用这个效果。可通过左右滑动来切换页面,而每个页面内部往往是ListView。本来是有滑动冲突的,但ViewPager帮我们内部处理了。如果使用的是ScrollView等,就必须手动处理,否则内外两层只有一层能够滑动,因为两者之间的滑动事件有冲突。

2.当内外两层都在同一个方向可以滑动的时候,显然存在逻辑问题,当手指开始滑动时,系统无法知道用户想让哪一层滑动。

3.上面两种情况的嵌套,它由几个单一的滑动冲突叠加,只需要分别处理内层,中层,外层的滑动冲突即可。

滑动冲突的处理规则

对于场景1.当用户左右滑动时,需要让外部的View拦截点击事件,当上下滑动时,让内部的View拦截点击事件,判断方向有很多种办法,比如根据滑动路径和水平方向所形成的夹角,水平与数值方向上的速度差也可以

对于场景2.比较特殊,无法根据滑动的角度、距离差和速度差来做判断,但是一般都能在业务上找到突破点,当处于某种状态时需要外部View响应滑动,当处于另一种状态时需要内部View响应另一种滑动

对于场景3.同样只能在业务上找到突破点

滑动冲突的解决方式

针对场景1

1.外部拦截法

点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要此事件就不拦截,这种方法比较符合点击事件的分发机制。外部拦截需要重写父容器的onInterceptTouchEvent,在内部做相应的拦截即可

在这里插入图片描述

在这里插入图片描述

上图为外部拦截法的典型逻辑,针对不同的滑动冲突,只需要修改父容器需要当前点击事件这个条件即可。在onInterceptTouchEvent方法中,首先是ACTION_DOWN事件,父容器必须返回false,即不拦截ACTION_DOWN事件,因为一旦父容器拦截DOWN事件,那么后续的MOVE和UP事件都会直接交给父容器处理,这个时候事件没法再传递给子元素。其次是ACION_MOVE事件,这个事件可以根据需要来决定是否拦截,如果父容器需要拦截就返回true,否则返回false。最后是ACTION_UP事件,这里必须要返回false,因为该事件本身没有多大意义。

假设事件交由子元素处理,如果父容器在UP事件时返回了true,就会导致子元素无法接收到UP事件,这个时候子元素的onClick事件就无法触发,但是父容器比较特殊,一旦它开始拦截任何一个事件,那么后续的事件都会交给它来处理,而UP事件作为最后一个事件也必定可以传递给父容器,即使父容器的onInterceptTouchEvent方法在UP时返回了false

2.内部拦截法

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

在这里插入图片描述

在这里插入图片描述

上述代码是内部拦截法的典型代码,当面对不同滑动策略时只需要修改里面的条件即可。除了子元素需要做处理以外,父元素也要默认拦截除了DOWN事件以外的其他事件,这样当子元素调用parent.requestDisallowInterceptTouchEvent(false)方法时,父元素才能继续拦截所需事件。父容器是不能拦截ACTION_DOWN事件的,因为DOWN事件并不受FLAG_DISALLOW_INTERCEPT这个标记位的控制,所以一旦父容器拦截ACTION_DOWN事件,那么所有的时间都无法传递到子元素中去,这样内部拦截就无法起作用了。

探究requestDisallowInterceptTouchEvent方法

        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            // We're already in this state, assume our ancestors are too
            return;
        }

        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }

        // Pass it up to our parent
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }

官方用了mGroupFlags和FLAG_DISALLOW_INTERCEPT按位运算,最终得到(mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0这一bool值来标识是否拦截。mGroupFlags是一个16机制整型,可以标识很多状态,每一位标识一个状态,FLAG_DISALLOW_INTERCEPT的值为0x800000,也就是1000000000000000000,它们进行与运算只有第20位有效,因为其他全是0。

回顾dispatchTouchEvent部分源码

// Handle an initial down.
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }
  // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
 
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }

dispathEvent每次接收到点击事件时,会初始化触摸状态,然后判断disallowIntercept是否为true,如果为true不执行onInterceptTouchEvent。

探究resetTouchState方法
/**
     * Resets all touch state in preparation for a new cycle.
 
     */
    private void resetTouchState() {
        clearTouchTargets();
        resetCancelNextUpFlag(this);
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;//重置标识为false
        mNestedScrollAxes = SCROLL_AXIS_NONE;
    }

它重置了mGroupFlags标识为false,这就解释了为什么在子View中的构造方法或生命周期方法调用parent.requestDisallowInterceptTouchEvent会失效

父元素作如下修改
!在这里插入图片描述

在这里插入图片描述

考虑一种情况,当用户正在水平滑动,但是在水平滑动之前如果用户再迅速进行竖直滑动,就会导致界面在水平方向无法滑动到终点而处于一种中间状态,为了避免这种不良体验,当水平方向正在滑动时,下一个序列的点击事件仍然交给父容器处理,这样水平方向就不会停留在中间状态了。abortAnimation用于优化滑动体验。

而场景2的解决办法和场景1一样,只是滑动规则不同。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值