04. 滑动冲突处理

发布于:2025-03-07 ⋅ 阅读:(25) ⋅ 点赞:(0)

00.思维树

  • 什么是滑动冲突处理?
  • 怎样解决?
  • 什么是外部拦截法?
  • 什么是内部拦截法?

01.什么是滑动冲突

1.1 什么是滑动冲突?

滑动冲突指的是:当父容器和子 View都可以响应滑动手势时,系统无法判断应该让哪一个控件处理滑动事件,导致滑动行为出现异常。

1.2 一个简单的例子

假设场景是这样的:

  • 父容器是一个垂直方向滑动的 ScrollView
  • 子 View 是一个可以横向滑动的 HorizontalScrollView

当用户用手指在子 View 上滑动时,用户很难做到完全垂直或水平,多数时候是斜着的。此时因为既包含水平,又包含垂直,导致父容器和子View都可以响应该滑动事件。

那我们该怎么处理呢?他有以下两种处理方法。

02.外部拦截法

  • 父容器根据需要在onInterceptTouchEvent方法中对触摸事件进行选择性拦截,如果父容器返回 true,那么这个事件就会被父容器处理,子 View 不再接收到该事件。思路可以看以下伪代码

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 不拦截 ACTION_DOWN,交给子 View 处理
                return false;
            
            case MotionEvent.ACTION_MOVE:
                int deltaX = (int) (event.getX() - mLastXIntercept);
                int deltaY = (int) (event.getY() - mLastYIntercept);
                
                // 如果是垂直滑动,父容器可以拦截事件
                if (Math.abs(deltaY) > Math.abs(deltaX)) {
                    return true; // 父容器拦截事件
                } else {
                    return false; // 水平滑动,交给子 View 处理
                }
            
            case MotionEvent.ACTION_UP:
                // 不拦截 ACTION_UP,交给子 View 处理
                return false;
    
            default:
                return super.onInterceptTouchEvent(event);
        }
    }
    
  • 思路如下所示

    • 在**ACTION_MOVE 事件**中:根据移动的x和y举例判断,如果移动的y距离大于x,那么说明用户倾向于进行垂直滑动,父容器就可以拦截事件。

03.内部拦截法

  • 内部拦截法其核心思想是让 父容器不主动拦截事件,而是通过一个标记来判断是否拦截,这个标记是!disallowIntercept,如果为真,启用拦截。

  • 那么首先,它这个为false,不拦截,把所有事件先传递给子 View。子 View 来决定是否要自己消费事件或者交给父容器处理。关键方法是getParent().requestDisallowInterceptTouchEvent(false);

  • 这个方法会使得父View中上述的条件为true,启用父View的拦截,子View就接收不到后续的事件列了。

  • 思路可以看以下伪代码:

    • 子 View 修改其 dispatchTouchEvent 方法
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
    
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                // 禁止父容器拦截当前事件序列,确保子 View 获取完整的事件流
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            }
    
            case MotionEvent.ACTION_MOVE: {
                int deltaX = x - mLastX; // 水平滑动距离
                int deltaY = y - mLastY; // 垂直滑动距离
    
                // 根据滑动方向决定事件处理权
                if (Math.abs(deltaY) > Math.abs(deltaX)) {
                    // 如果是垂直滑动,父容器需要处理事件,允许父容器拦截当前事件列
                    getParent().requestDisallowInterceptTouchEvent(false);
                } else {
                    // 如果是水平滑动,子 View 自己处理事件
                    // 注意:无需特殊处理,保持父容器不拦截即可
                }
                break;
            }
    
            case MotionEvent.ACTION_UP: {
                // 这里通常不需要特殊处理
                break;
            }
            default:
                break;
        }
    
        // 更新上一次的触摸坐标
        mLastX = x;
        mLastY = y;
    
        // 子 View 自己处理事件或继续传递
        return super.dispatchTouchEvent(event);
    }
    
    
    • 父容器的 onInterceptTouchEvent
    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        int action = event.getAction();
    
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                // 不拦截 ACTION_DOWN,必须交给子 View
                return false;
    
            case MotionEvent.ACTION_MOVE:
                // 如果子 View 不再禁止拦截,父容器可以尝试拦截事件
                //如果子 View 已经调用了 requestDisallowInterceptTouchEvent(false),则父容器会有机会拦截 ACTION_MOVE 事件。
                return true;
    
            default:
                return super.onInterceptTouchEvent(event);
        }
    }
    
    
  • 思路所示

    • 父容器必须确保不拦截 ACTION_DOWN 事件,否则整个事件序列无法传递到子 View。后续事件(如 ACTION_MOVE)的拦截权由子 View 通过 requestDisallowInterceptTouchEvent动态 控制。
    • 滑动策略的逻辑放在子 View 的 dispatchTouchEvent 方法的 ACTION_MOVE 事件中,子 View 接收到所有事件,并基于滑动方向、业务逻辑等条件判断是自己处理事件还是交给父容器。如果需要交给父容器,则调用 parent.requestDisallowInterceptTouchEvent(false)。该方法会使得父容器拦截代码判断条件 !disallowIntercept为真,启用拦截。

04.滑动冲突实例

  • 场景解释:

    • 为了能使整个Activity界面能够上下滑动,使用了ScrollView,将Tablayout和ViewPager的联合包裹在LinearLayout中,作为一部分。
  • 代码如下所示

    <?xml version="1.0" encoding="utf-8"?>
    <ScrollView
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">
    
            <ImageView
                android:layout_width="match_parent"
                android:layout_height="300dp"
                android:background="@drawable/bg_autumn_tree_min"/>
    
            <include layout="@layout/include_reflex_view"/>
    
            <android.support.design.widget.TabLayout
                android:id="@+id/tab_layout"
                android:layout_width="match_parent"
                android:layout_height="50dp"/>
    
            <android.support.v4.view.ViewPager
                android:id="@+id/vp_content"
                android:layout_width="match_parent"
                android:layout_height="match_parent"/>
    
        </LinearLayout>
    
    </ScrollView>
    
  • 滑动冲突:由于 ScrollView 是一个可以上下滚动的容器,而 ViewPager 中的内容通常是可以左右滑动的,这就导致了滑动冲突。ScrollViewViewPager 都希望处理触摸事件,因此会发生冲突,造成滑动不流畅或滑动行为异常。

05.外部拦截法解决滑动冲突

  • 滑动方向不同之以ScrollView与ViewPager为例的外部解决法

    • 从 父View 着手,重写 onInterceptTouchEvent 方法,在 父View 需要拦截的时候拦截,不要的时候返回false,代码大概如下
    举例子:以ScrollView与ViewPager为例
    public class MyScrollView extends ScrollView {
    
        public MyScrollView(Context context) {
            super(context);
        }
    
        public MyScrollView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
    
        @TargetApi(21)
        public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
            super(context, attrs, defStyleAttr, defStyleRes);
        }
    
        private float mDownPosX = 0;
        private float mDownPosY = 0;
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            final float x = ev.getX();
            final float y = ev.getY();
            final int action = ev.getAction();
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                    mDownPosX = x;
                    mDownPosY = y;
                    break;
                case MotionEvent.ACTION_MOVE:
                    final float deltaX = Math.abs(x - mDownPosX);
                    final float deltaY = Math.abs(y - mDownPosY);
                    
                    if (deltaX > deltaY) {
                        return false;// 倾向于左右滑动,所以不拦截
                    } else {
                    	return true;//竖直滑动,进行拦截
                    }
                    
            }
            return super.onInterceptTouchEvent(ev);
        }
    }
    

06.内部拦截法解决滑动冲突

  • 从子View着手,父View 先不要拦截任何事件,所有的 事件传递给子View,如果子View需要此事件就消费掉,不需要此事件的话就交给 父View 处理。

  • 实现思路 如下,重写 子View 的dispatchTouchEvent方法,在Action_down动作中通过方法requestDisallowInterceptTouchEvent(true) 先请求 父View 不要拦截事件,这样保证子View能够接受到Action_move事件,再在Action_move动作中根据自己的逻辑是否要拦截事件,不要的话再交给 父View 处理

    public class MyViewPager extends ViewPager {
    
        private static final String TAG = "yc";
    
        int lastX = -1;
        int lastY = -1;
    
        public MyViewPager(Context context) {
            super(context);
        }
    
        public MyViewPager(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            int x = (int) ev.getRawX();
            int y = (int) ev.getRawY();
            int dealtX = 0;
            int dealtY = 0;
            switch (ev.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    dealtX = 0;
                    dealtY = 0;
                    // 保证子View能够接收到Action_move事件
                    getParent().requestDisallowInterceptTouchEvent(true);
                    break;
                case MotionEvent.ACTION_MOVE:
                    dealtX += Math.abs(x - lastX);
                    dealtY += Math.abs(y - lastY);
                    Log.i(TAG, "dealtX:=" + dealtX);
                    Log.i(TAG, "dealtY:=" + dealtY);
                 
                    if (dealtX >= dealtY) {
                    //左右滑,禁止父容器拦截
                        getParent().requestDisallowInterceptTouchEvent(true);
                    } else {
                    //上下滑,让父容器拦截后续事件列
                        getParent().requestDisallowInterceptTouchEvent(false);
                    }
                    lastX = x;
                    lastY = y;
                    break;
                case MotionEvent.ACTION_CANCEL:
                    break;
                case MotionEvent.ACTION_UP:
                    break;
            }
            return super.dispatchTouchEvent(ev);
        }
    }
    

其他介绍

01.关于我的博客

  • csdn:http://my.csdn.net/qq_35829566

  • 掘金:https://juejin.im/user/499639464759898

  • github:https://github.com/jjjjjjava

  • 简书:http://www.jianshu.com/u/92a2412be53e

  • 邮箱:[934137388@qq.com]