当有滑动效果的多个View嵌套使用的时候,就有可能导致滑动冲突问题。
其本质其实是,滑动事件分发出了问题,即我们希望一个滑动事件在某种状态下交由A View来处理,却交给了B View来处理。解决问题的方法,当然就是根据具体场景的特点,通过滑动轨迹或者业务状态,判断出各种情况下的滑动操作与目标View的对应关系,然后通过View的onInterceptTouchEvent()等方法将滑动事件在各个状态下分发给对应的目标View。
常见场景有3种,但解决思路总结起来都是上面那一套:
1.内部View和外部View滑动方向不一致:通过滑动轨迹的方向就可以判断出当前应该分发给谁。
2.内部View和外部View滑动方向一致:要结合业务逻辑状态判断出当前应该分发给谁。
3.多层View嵌套,既有内部外部滑动方向不一致的情况,也有滑动方向一致的情况:将以上两种方法结合起来。
View的滑动冲突问题
- 场景一:外部控件和内部控件的滑动方向不一致
- 场景二:外部控件和内部控件的滑动方向一致
- 场景三:嵌套以上情况
滑动冲突的处理规则
- 场景一:当用户上下滑动的时候,需要让外部的View拦截事件,当用户左右滑动的时候,需要让内部的View拦截事件。
- 场景二:通常需要内部View响应View的滑动。
解决方法(方案一和方案二任意选一个就行了)
方案一:外部拦截法
所谓外部拦截法指事件都经过父容器(父布局)的拦截处理,如果父容器(父布局)需要此事件就拦截,如果子View需要此事件就不拦截。
外部拦截法需要重写父容器的onInterceptTouchEvent()方法.
在onInterceptTouchEvent()方法中,ACTION_DOWN事件父容器必须返回false,这样事件才会传递到子元素;其次ACTION_MOVE事件根据具体需求判断是否拦截,如果父容器需要拦截就返回true,否则返回false;如果父容器在ACTION_UP时返回true,则子元素无法收到UP事件导致onClick事件失效。
父容器(父布局)
int lastInterceptX;
int lastInterceptY;
@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;
}
lastInterceptX = x;
lastInterceptY = y;
return intercepted;
}
方案二:内部拦截法
内部拦截法指父容器(父布局)不拦截事件,所有的事件都传递到子元素(子布局),如果子元素需要此事件就直接消耗,否则交给父容器(父布局)处理,需要使用requestDisallowInterceptTouchEvent()方法。
子元素的dispatchTouchEvent()必须在ACTION_DOWN事件调用requestDisallowInterceptTouchEvent(true),这样才能保证子元素能收到ACTION_MOVE事件,在move事件做逻辑操作。
父容器的onInterceptTouchEvent()方法里的ACTION_DOWN事件不能拦截,这样子元素才能收到事件。
父容器(父布局)
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}
子元素(子布局)
int lastX;
int lastY;
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
parent.requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int dx = x - lastX;
int dy = y - lastY;
if (/*父容器需要此事件*/) {
parent.requestDisallowInterceptTouchEvent(false);
} else {
parent.requestDisallowInterceptTouchEvent(true);
}
break;
case MotionEvent.ACTION_UP:
break;
}
lastX = x;
lastY = y;
return super.dispatchTouchEvent(event);
}
测试案例
在AndroidX中很多内置空间已经处理了一些事件冲突,但是仍然还有一些需要处理。
ScrollView嵌套RecyclerView
public class MyScrollView extends ScrollView {
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
//ScrollView的滚动需要在onTouchEvent()里做一些准备,直接调用一次
onTouchEvent(ev);
return false;
} else {
return true;
}
}
}
public class MyScrollView extends ScrollView {
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
//ScrollView的滚动需要在onTouchEvent()里做一些准备,直接调用一次
onTouchEvent(ev);
return false;
} else {
return true;
}
}
}
ScrollView嵌套EditText
public class MyEditText extends androidx.appcompat.widget.AppCompatEditText {
@Override
public boolean onTouchEvent(MotionEvent event) {
if (canVerticalScroll(this)) {
getParent().requestDisallowInterceptTouchEvent(true);
if (event.getAction() == MotionEvent.ACTION_UP) {
getParent().requestDisallowInterceptTouchEvent(false);
}
}
return super.onTouchEvent(event);
}
private boolean canVerticalScroll(EditText editText) {
//滚动的距离
int scrollY = editText.getScrollY();
//控件里内容的总高度
int scrollRange = editText.getLayout().getHeight();
//控件实际的高度
int scrollExtent = editText.getHeight() - editText.getCompoundPaddingTop() - editText.getCompoundPaddingBottom();
//内容的高度与控件实际高度的差值
int scrollDifference = scrollRange - scrollExtent;
if (scrollDifference == 0) {
return false;
}
return (scrollY > 0) || (scrollY < scrollDifference - 1);
}
}
如对此有疑问,请联系qq1164688204。
推荐Android开源项目
项目功能介绍:RxJava2 和Retrofit2 项目,使用Kotlin+RxJava2+Retrofit2+MVP架构+组件化/Kotlin+Retrofit2+协程+MVVM架构+组件化,添加自动管理token 功能,添加RxJava2 生命周期管理,集成极光推送、阿里云Oss对象存储和高德地图定位功能。
项目地址:https://gitee.com/urasaki/RxJava2AndRetrofit2