文章目录
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); } }
- 子 View 修改其
思路所示
- 父容器必须确保不拦截
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
中的内容通常是可以左右滑动的,这就导致了滑动冲突。ScrollView
和ViewPager
都希望处理触摸事件,因此会发生冲突,造成滑动不流畅或滑动行为异常。
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]