最近实际应用中遇到了滑动冲突的相关问题, 在解决过程中, 有些需要注意的问题, 特别记录一下
一应用场景
在解决具体问题之前, 先介绍下实际应用场景及问题状况
从图中可以看出, 一个 ScrollView 内部嵌套三个 RecyclerView, 其中两个 RecyclerView 是横向, 一个 RecyclerView 是纵向
在这个场景下, 出现了滑动冲突问题, 主要表现为横向 RecyclerView 滑动不灵敏, 纵向 RecyclerView 滑动卡顿
二问题分析
1. 横向 RecyclerView 滑动不灵敏
该问题所产生的滑动冲突如上图所示
针对该问题, 解决的方案是根据当前滑动方向, 水平还是垂直来判断这个事件到底该交给谁来处理
一般情况下根据滑动路径形成的夹角 (或者说是斜率如下图) 水平和竖直方向滑动速度差来判断
2. 纵向 RecyclerView 滑动卡顿
该问题所产生的滑动冲突如上图所示
针对该问题, 一般情况下必需通过业务逻辑来进行判断, 决定到底谁来处理该事件
三滑动冲突解决方法
针对滑动冲突, 一般有两个解决方法
1. 外部拦截法
事件都先经过父容器的拦截处理, 如果不需要此事件就不拦截, 这样就可以解决滑动冲突的问题外部拦截法需要重写父容器的
onInterceptTouchEvent()
方法, 在内部完成相应的拦截即可
- @Override public boolean onInterceptTouchEvent(MotionEvent ev) {
- boolean intercepted = false;
- int x = (int) ev.getX();
- int y = (int) ev.getY();
- switch (ev.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;
- }
- }
- return intercepted;
- }
ACTION_DOWN 这个事件里父容器必须返回 false, 即不拦截 ACTION_DOWN 事件, 因为一旦拦截了那么后续的 ACTION_MOVEACTION_UP 都由父容器去处理, 事件就无法传到子 view 了
ACTION_MOVE 事件可以根据需要来进行拦截或者不拦截
ACTION_UP 这个事件必须返回 false, 就会导致子 View 无法接受到 UP 事件, 这个时候子元素中的 onClick()事件就无法处触发
2. 内部拦截法
父容器不拦截任何事件, 所有的事件都传递给子元素, 如果子元素需要此事件就直接消耗掉, 否则就交由父容器进行处理这种方法需要配合
requestDisallowInterceptTouchEvent()
方法才能正常工作
主要是修改子 view 的
dispatchTouchEvent()
方法
- @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:
- {
- if (父容器需要此类事件) {
- getParent().requestDisallowInterceptTouchEvent(false);
- }
- break;
- }
- case MotionEvent.ACTION_UP:
- {
- break;
- }
- }
- return super.dispatchTouchEvent(ev);
- }
父容器需要重写
onInterceptTouchEvent()
方法
- @Override
- public boolean onInterceptTouchEvent(MotionEvent ev) {
- int action = ev.getAction();
- if(action == MotionEvent.ACTION_DOWN){
- return false;
- }else {
- return true;
- }
- }
父容器拦截 ACTION_DOWN 以外的其他事件, 因为 ACTION_DOWN 事件不受 FLAG_DISALLOW_INTERCEPT 这个标记的控制, 所以一旦父容器拦截了 ACTION_DOWN 事件那么所有的事件都无法传到子 view 中去了, 这样内部拦截法就不起作用了
四问题解决
下面就来实际解决本文中遇到的滑动冲突问题通过上述分析可知, 本文所遇到的问题通过外部拦截法, 重写 ScrollView 的
onInterceptTouchEvent()
方法即可快速简单的解决
- public class FScrollView extends ScrollView {
- private float mLastXIntercept = 0f;
- private float mLastYIntercept = 0f;
- public FScrollView(Context context, AttributeSet attrs) {
- super(context, attrs);
- }@Override public boolean onInterceptTouchEvent(MotionEvent ev) {
- boolean intercepted = false;
- float x = ev.getX();
- float y = ev.getY();
- int action = ev.getAction() & MotionEvent.ACTION_MASK;
- switch (action) {
- case MotionEvent.ACTION_DOWN:
- {
- intercepted = false;
- // 初始化 mActivePointerId
- super.onInterceptTouchEvent(ev);
- break;
- }
- case MotionEvent.ACTION_MOVE:
- {
- // 横坐标位移增量
- float deltaX = x - mLastXIntercept;
- // 纵坐标位移增量
- float deltaY = y - mLastYIntercept;
- if (Math.abs(deltaX) < Math.abs(deltaY)) {
- intercepted = true;
- } else {
- intercepted = false;
- }
- break;
- }
- case MotionEvent.ACTION_UP:
- {
- intercepted = false;
- break;
- }
- }
- mLastXIntercept = x;
- mLastYIntercept = y;
- return intercepted;
- }
- }
- Math.abs(deltaX) < Math.abs(deltaY)
表示横向位移增量小于于竖向位移增量, 即竖直滑动, 则 ScrollView 拦截事件
super.onInterceptTouchEvent(ev)
, 初始化 mActivePointerId, 避免出现
Invalid pointerId=-1 in onTouchEvent
问题
纵向 RecyclerView 的滑动被拦截, 交给 ScrollView 处理, 需要测量高度, 会默认加载所有 item, 相当于 LinearLayout, 从而导致复用效率大大降低所以如果情况复杂, 建议采用头布局
来源: https://juejin.im/post/5aa8c2f1f265da237c689946