Android解决滑动冲突
滑动冲突的产生
在讨论解决滑动冲突的方法之前,我们需要知道在哪些场景之下会产生滑动冲突。滑动冲突的产生是两个组件之间(或者更多),且这两个组件是包含关系(父控件和子控件),当父控件和子控件都可以对滑动事件进行拦截处理,而我们并没有对他们的滑动冲突进行处理,那么此时只有一个控件可以对滑动事件来进行响应,我们平常称这种情况为父控件和子控件产生了滑动冲突。主要有以下三个场景:
滑动冲突场景一
父控件的滑动方向与子控件的滑动方向不同,这种场景的解决思路比较简单,例如我们平常使用的ViewPager+RecyclerView(假设viewPager是左右滑动,而RecyclerView是上下滑动),这种就是滑动我们滑动冲突的场景一,但是我们使用这种架构没有发生滑动冲突呢,原因是由于在viewPager内部帮助我们实现了解决滑动冲突的方法。而如果我们不使用ViewPager,来使用其他父控件可以滑动并且内部为RecyclerView或者ListView列表,此时就会发生滑动冲突。
对于滑动冲突场景一的具体解决方法为外部拦截法,下面进行具体介绍:
外部拦截法
所谓的外部拦截法指的是对于整个事件序列都要先经过父控件的拦截处理,当我们需要此事件则进行拦截,如果不需要则交由子控件进行处理。(如果不理解事件分发机制的,请学习一下Android的事件分发机制)。我们只需要重写我们父控件的onInterceptTouchEvent()方法来决定是否拦截事件,假设外部为ViewPager(左右滑动,并在此处进行假设ViewPager没有处理滑动冲突,实际ViewPager源码上已解决滑动冲突问题),内部为RecyclerView(上下滑动),则我们重写ViewPager的onInterceptTouchEvent()方法,具体代码逻辑如下所示:
package com.it.test;
import android.content.Context;
import android.view.MotionEvent;
import androidx.annotation.NonNull;
import androidx.viewpager.widget.ViewPager;
public class MyViewPager extends ViewPager {
private float startX;
private float startY;
//使用directionSign来作为拦截的标志,0代表没有初始化,1代表父控件拦截,其他情况代表子控件拦截
private int directionSign = 0; //只有在滑动开始时需要进行判断由谁进行拦截,之后的这一次事件中无需判断
public MyViewPager(@NonNull Context context) {
super(context);
}
//onInterceptTouchEvent返回true表示该viewPager拦截事件(不再向子控件中进行分发),返回false代表不拦截,进行分发事件(交由子控件RecyclerView处理)
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean sign = false; //该标志标示是否拦截事件
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
//按下屏幕事件
sign = false; //此处必须为false,如果为true,则后期滑动的事件将默认会被viewPager拦截,且不在调用onInterceptTouchEvent()方法
//进行记录当前的坐标
startX = ev.getX();
startY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
//滑动屏幕事件,具体解决滑动冲突的思路在此
if(checkFatherNeed(ev)){ //检查父控件ViewPager是否需要拦截,如果需要则进行修改sign = true
sign = true;
}else{
sign = false;
}
break;
case MotionEvent.ACTION_UP:
//松开屏幕事件
sign = false; //此处为true或false意义不大,一般置为false
break;
}
return sign;
}
private boolean checkFatherNeed(MotionEvent event) {
if(directionSign == 0){
//没有初始化,对directionSign进行初始化
float currentX = event.getX();
float currentY = event.getY();
if(Math.abs(currentX - startX) > Math.abs(currentY-startY)){
//代表水平滑动内容多,进行拦截事件
directionSign = 1;
return true;
}else{
//代表水平滑动内容多,交给子控件拦截事件
directionSign = 2;
return false;
}
}else if(directionSign == 1){
//父控件拦截
return true;
}else{
//子控件拦截
return false;
}
}
}
一般的解决思路就是判断手指滑动的方向(可以根据水平滑动的距离和竖直滑动距离来进行比较)来判断交给谁来拦截并且处理事件。(左右滑动交给ViewPager,上下滑动交给RecyclerView)
滑动冲突场景二
滑动冲突的场景二是外部滑动方向和内部滑动滑动方向一致,例如上下滑动的ScrollView里面嵌套上下滑动的RecyclerView。该场景的具体解决办法是内部拦截法。
内部拦截法
内部拦截法是指我们需要通过重写子控件的dispatcherTouchEvent分发方法,可以对事件的状态判断来进行分发事件的处理权交给父控件还是子控件。(比如说当处于某一状态时,我们要让子控件进行处理事件,其他状体交给父控件)。
其具体实现思路如下:
我们需要重写子控件的dispatcherTouchEvent方法,重写父控件的onInterceptTouchEvent方法,需要保证当手指按下的一瞬间,即产生ACTION_DOWN事件时,需要保证父控件不对其进行拦截,然后子控件的分发时需要进行parent.requestDisallowInterceptTouchEvent(true)就可以不让父控件来拦截事件。当我们调用了parent.requestDisallowInterceptTouchEvent(false)就可以让父控件进行拦截事件。
具体代码如下:
子控件
package com.it.test;
import android.content.Context;
import android.view.MotionEvent;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
public class MyRecyclerview extends RecyclerView {
float x;
float y;
public MyRecyclerview(@NonNull Context context) {
super(context);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
float x = ev.getX();
float y = ev.getY();
getParent().requestDisallowInterceptTouchEvent(true); //父控件之后不再拦截事件
break;
case MotionEvent.ACTION_MOVE:
if(父控件需要此类事件){
getParent().requestDisallowInterceptTouchEvent(false); //父控件可以进行拦截事件
}
break;
}
return super.dispatchTouchEvent(ev);
}
}
父控件
package com.it.test;
import android.content.Context;
import android.view.MotionEvent;
import androidx.annotation.NonNull;
import androidx.viewpager.widget.ViewPager;
public class MyViewPager extends ViewPager {
public MyViewPager(@NonNull Context context) {
super(context);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(ev.getAction() == MotionEvent.ACTION_DOWN){
return false; //不拦截
}else{
return true; //除了按下事件其他进行全部拦截(前提是子控件的parent.requestDisallowInterceptTouchEvent(false))
}
}
}
滑动冲突场景三
滑动冲突的场景三就是以上两种情况的结合或者同时出现,解决办法就是利用内部拦截和外部拦截的联合使用。