04. 滑动冲突处理

文章目录

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.关于我的博客

相关推荐
*星星之火*2 小时前
【GPT入门】第5课 思维链的提出与案例
android·gpt
夏天的味道٥2 小时前
使用 Java 执行 SQL 语句和存储过程
java·开发语言·sql
EasyCVR3 小时前
EasyRTC嵌入式视频通话SDK的跨平台适配,构建web浏览器、Linux、ARM、安卓等终端的低延迟音视频通信
android·arm开发·网络协议·tcp/ip·音视频·webrtc
韩家老大3 小时前
RK Android14 在计算器内输入特定字符跳转到其他应用
android
冰糖码奇朵4 小时前
大数据表高效导入导出解决方案,mysql数据库LOAD DATA命令和INTO OUTFILE命令详解
java·数据库·sql·mysql
好教员好4 小时前
【Spring】整合【SpringMVC】
java·spring
浪九天5 小时前
Java直通车系列13【Spring MVC】(Spring MVC常用注解)
java·后端·spring
堕落年代6 小时前
Maven匹配机制和仓库库设置
java·maven
功德+n6 小时前
Maven 使用指南:基础 + 进阶 + 高级用法
java·开发语言·maven
张拭心6 小时前
2024 总结,我的停滞与觉醒
android·前端