滑动冲突

311 阅读3分钟

场景一

描述

外部滑动与内部滑动不一致 例如ViewPager+RecyclerView/ListView,外部左右滑动,内部是上下滑动。由于ViewPager已经在内部处理好了这种滑动冲突,因此使用ViewPager的时候,并不需要关注这类问题。但是采用ScrollView的话,就需要手动处理滑动冲突。

处理思想

思想:当用户左右滑动的时候,让外部的View拦截点击事件;当用户上下滑动的时候,让内部的View拦截点击事件。
根据水平滑动与垂直滑动的距离差来判断正在进行哪种滑动

  • 水平滑动的距离差大于垂直滑动的距离差,我们就认为在进行水平滑动,此时让外部View拦截点击事件;
  • 若垂直滑动的距离差大于水平滑动的距离差,我们认为时在进行垂直滑动,此时让内部View拦截点击事件

具体操作

  • 外部拦截法:点击事件都是从父容器向下传递,那么如果父容器需要该事件则拦截,不需要该事件则不拦截
    重写父容器的onInterceptTouchEvent
// 记录上次滑动的坐标
    private int mLastXIntercept = 0;
    private int mLastYIntercept = 0;

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        // 父容器是否拦截点击事件
        boolean intercept = false;
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 必须为false,如果为true,表示父容器拦截了ACTION_DOWN事件,
                // 那么后续的ACTION_MOVE和ACTION_UP事件都会直接交给父容器处理
                intercept = false;
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastXIntercept;
                int deltaY = y - mLastYIntercept;
                if (Math.abs(deltaX) > Math.abs(deltaY)) {
                    // 水平滑动距离大于垂直滑动距离,因此认为正在进行水平滑动,父容器需要拦截点击事件
                    intercept = true;
                } else {
                    intercept = false;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercept = false;
                break;
        }
        mLastXIntercept = x;
        mLastYIntercept = y;
        return intercept;
    }

注意:

  1. ACTION_DOWN事件必须为false,因为如果为true,代表父容器拦截该事件,那么后续的ACTION_MOVEACTION_UP事件都直接交给父容器处理
  2. ACTION_MOVE中根据具体需求来决定父容器是否拦截点击事件
  3. ACTION_UP返回false,如果在父容器中返回true,会导致子View无法接收到ACTION_UP事件,此时子View的onClick方法无法执行(onClick方法是ACTOION_UP下触发的,performClick方法)。父容器则不一样,一旦拦截事件,后续事件都会有它处理
  • 内部拦截法:父容器不拦截任何事件,所有的事件都先传递给子元素,如果子元素需要则消耗掉,如果不需要则向上传递给父容器处理(反向操作,不推荐)
    重写子元素的dispatchToucheEvent方法+requestDisallowInterceptTouchEvent
 // 父容器中实现
    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        if(event.getAction() == MotionEvent.ACTION_DOWN){
            return false;
        } else {
            return true;
        }
    }

    // 子元素中实现
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch(ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                if(父容器需要此点击事件){
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(ev);
    }

注意:

  1. 父容器不能拦截ACTION_DOWN事件,因为ACTION_DOWN事件不受FLAG_DISALLOW_INTERCEPT标记位控制,如果父容器拦截ACTION_DOWN事件,那么所有事件都无法传递到子元素
  2. 滑动的策略的逻辑判断是在子元素的dispatchTouchEvent方法的ACTOPN_MOVE中,如果父容器需要此事件,则调用parent.requestDisallowInterceptTouchEvent(false)方法