深入理解事件分发机制之解决滑动冲突

外部拦截法,即对父容器的onInterceptTouchEvent进行处理,父容器要是需要这个事件就拦截,不要就不拦截

public boolean onInterceptTouchEvent(MotionEvent event) {
    boolean intercepted = false;
    
    int x = (int) event.getX();
    int y = (int) event.getY();
    
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
            intercepted = false;
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            if (父容器需要这个事件) {
                intercepted = true;
            } else {
                intercepted = false;
            }
            break;
        }
        case MotionEvent.ACTION_UP: {
            intercepted = false;
            break;
        }
        default:
            break;
    }
    mLastXintercept = x;
    mLastYIntercept = y;
    return intercepted;
}
先明白一点,down事件必须为false。因为如果down事件拦截了,那么接下来的事件move,up不管怎么设置,也一并会交给父容器处理。move事件看你需求,如果需要就截留。up事件无所谓了,因为你前面的一旦打算截留,up事件也会一并交给父容器处理。


内部拦截法,通过调用父容器的requestDisallowInterceptTouchEvent方法,如果子元素需要这个事件就消耗掉,否则就让父容器处理

public boolean onInterceptTouchEvent(MotionEvent event) {
    int x = (int) event.getX();
    int y = (int) event.getY();

    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
            parent.requestDisallowInterceptTouchEvent(true);
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            int deltaX = x - mLastX;
            int deltaY = y - mLastY;
            if (父容器需要这个事件) {
                parent.requestDisallowInterceptTouchEvent(false);
            }
            break;
        }
        case MotionEvent.ACTION_UP: {
            break;
        }
        default:
            break;
    }
    
    mLastX = x;
    mLastY = y;
    return super.dispatchTouchEvent(event);
}
父容器的requestDisallowInterceptTouchEvent方法相当于使得它的拦截机制失效了,所以这是子容器控制父容器的一个手段。不过requestDisallowInterceptTouchEvent方法却无法使得对down事件的拦截也失效(具体原因是FLAG_DISALLOW_INTERCEPT标记位的重置)。所以父容器中还是需要保证他的down事件是返回false,表示不拦截的。



第一种冲突情况,父容器类似ViewPager(因为ViewPager已经帮我们处理好冲突了,所以我们用另外类似的控件举例子,下面称 类ViewPager),子元素是ListView。这种情况就面临一个问题,你是水平滑类ViewPager呢还是竖直滑ListView呢?显然,如果你想滑动的是ListView,虽然你已经发出了向下的手势,但是你有可能水平方向上移动了一点点,这个时候类ViewPager水平方向上也会移动,这个时候的用户体验就不是很好了。所以这就是你需要处理的地方。你应该做的是,判断水平的移动的距离大还是竖直移动的距离大,如果水平的大,那就把事件交给类ViewPager,如果竖直的大,就把事件交给ListView。

采用外部拦截法。

case MotionEvent.ACTION_MOVE: {
    int deltaX = x - mLastXIntercept;
    int deltaY = y - mLastYIntercept;
    if (Math.abs(deltaX) > Math.abs(deltaY)) {
        intercepted = true;
    } else {
        intercepted = false;
    }
    break;
}

mLastXintercept = x;
mLastYIntercept = y;
这里通过旧坐标点和当前坐标点判断位移,就好了。如果水平大,就拦截,类Viewpager消费,反之ListView消费。


考虑这样一种情况,加深你的理解。如果你一开始是水平滑动,但是你松开之前,又选择了上下滑动,这个时候会发生什么?自然是依然交给父容器处理。因为在最开始的判断中,你的水平比竖直移动的大,那么就这次事件就设置为拦截了已经。那么这一串down-move-up,都是只会让父容器,这个类ViewPager来响应了。你的竖直滑动是无效的。


此外,还要考虑一种情况。假如你的水平滑动,类ViewPager滑到了一半,但是这个时候你突然松开,又进行了上下滑动,那么这个类ViewPager就会很尴尬,处于中间的位置,这是很不友好的用户体验。所以我们还要让当水平滑动未结束的时候,接下来的事件依然交给父容器来进行。这里就是,他会判断,如果你的滑动没结束,那么直接拦截,不多bb。(如果你希望ListView也是这样好的用户体验,可以如法炮制)

case MotionEvent.ACTION_DOWN: {
    if (!mScroller.isFinished()) {
        mScroller.abortAnimation();
        intercepted = true;
    }
    break;
}

(此外这种冲突用内部拦截法也是如法炮制,简单的很)



第二种冲突情况,同方向冲突

最外面一个scrollView,可上下滑动,而里面有一个Header和一个ListView。


像这样。

为什么要弄这样一个布局呢?有时候因为业务需求,你需要给ListView添加一个头。你当然可以添加到ListView的里面,不过你肯定尝试过把这个头放到外面过,并且在外面套上一个滑动容器,结果失败了。这里就来解决这个问题。

他的滑动规则是:当Header显示的时候,事件交给ScrollView。当Header刚好隐藏,ListView的顶部在ScrollView的顶部且此时的手势是向下的时候,事件也交ScrollView。此外就交给ListView。而滑动事件落在Header上的时候,(它是一个ViewPager轮播图),那么事件就交给了Header了。此外水平位移大于竖直位移,事件也不会拦截。

case MotionEvent.ACTION_MOVE: {
    int deltaX = x - mLastXIntercept;
    int deltaY = y - mLastYIntercept;
    if (mDisallowInterceptTouchEventOnHeader && y <= getHeaderHeight()) {
        intercepted = 0;
    } else if (Math.abs(deltaY) <= Math.abs(deltaX)) {
        intercepted = 0;
    } else if (mStatus == STATUS_EXPANDED && deltaY <= -mTouchSlop) {
        intercepted = 1;
    } else if (mGiveUpTouchEventListener != null) {
        if (mGiveUpTouchEventListener.giveUpTouchEvent(event) && deltaY >=
                mTouchSlop) {
            intercepted = 1;
        }
    }
    break;
}
第一个if,如果落在header内(通过y <= getHeaderHeight()判断,前面那个就是子类是否希望无效化父类的拦截机制,你懂的),不拦截;

第二个if,如果水平位移大于竖直位移,不拦截;

第三个if,如果Header没有被完全隐藏(即有header的区域显示在界面内),并且是向上滑动(位移是负数且小于最小可识别滑动距离),scrollView拦截。

第四个if,如果ListView滑动到顶部了(即header刚好被遮住)并且向下滑动,scrollView拦截。


更复杂点的情况其实也一样,明白规则,根据外部和内部两个模板就可以解决。不过在这之前,还是梳理下需求为好(比如第二种冲突,完全可以把header作为一个item 放到listView里),这样首先就可以节省不小的工作量。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值