滑动冲突非常的常见,可能刚开始接触的时候确实有点让人摸不着头脑,但分析总结之后就会发现还是有一定的规律可循。
滑动冲突解决的关键点在于对事件分发机制的理解,对于事件的分发机制,上一篇文章(闲聊自定义控件之事件分发)已经对其进行了介绍,如果对该知识点缺乏了解的话建议先读一下上篇文章。
滑动冲突的分类
滑动冲突一般分为两类,内外部滑动方向一致、内外部滑动方向不一致。至于由它们嵌套或者平行组合的情况,也可以通过合理拆分后,对单一冲突点进行归类。滑动冲突之所以要分类,主要是方便对拦截条件的总结,但无论哪种情况,整体的套路是一致的。
解决滑动冲突的套路
解决滑动冲突的套路无非就是对事件进行合理的拦截,根据拦截位置一般分为外部拦截和内部拦截。外部拦截相对简单,推荐优先使用,但某些情况下外部拦截实现不了(如,两类滑动冲突平行组合),因此两种方案都需掌握。
外部拦截核心代码
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.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;
default:
break;
}
mLastX = x;
mLastY = y;
return intercepted;
}
其实原理很简单,就是通过重写onInterceptTouchEvent方法,默认是不拦截事件的,一旦满足外层布局需要拦截事件的条件,就对事件进行拦截,子View是否能够获得事件完全由外层布局决定。
内部拦截核心代码
内部拦截代码分为两部分,分别针对外层布局和内部View的。
外层布局代码如下:
public boolean onInterceptTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
xDown = (int) event.getX();
yDown = (int) event.getY();
return false;
}
return true;
}
主要目的是不拦截down事件和默认拦截非down事件。因为,如果拦截了down事件,那么子View将收不到后续事件序列,无法进行相应的拦截判断。默认拦截其它事件是为确保一旦进入这个方法就可以把其它事件截获。
内部View代码如下:
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
getParent().requestDisallowInterceptTouchEvent(true);//外层不拦截
}
break;
case MotionEvent.ACTION_MOVE:
if (外层布局拦截条件) {
getParent().requestDisallowInterceptTouchEvent(false);//外层拦截
}
break;
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
内部View的核心是在获取down事件时,调用parent的requestDisallowInterceptTouchEvent方法,让其放过所有事件。然后一旦满足需要拦截的条件则再次调用requestDisallowInterceptTouchEvent方法,让其进入外层布局的onInterceptTouchEvent方法(原因上篇文章有讲,跟这个FLAG_DISALLOW_INTERCEPT标志有关)。外层布局的onInterceptTouchEvent方法经过重写,默认拦截掉了除down之外的所有事件,所以外层布局就可以将事件截获。
外部拦截演示
效果图如下(上半部分):
图上红蓝色块分别代表可随意移动的外层布局和可横向移动的内层View,仅为演示解决事件冲突问题,所以直接使用了layout方法动态改变位置。
红色色块继承自Framelayout,事件拦截的核心代码如下:
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
xDown = (int) event.getX();
yDown = (int) event.getY();
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
if (Math.abs(x - mLastX) < Math.abs(y - mLastY)) {
intercepted = true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
default:
break;
}
mLastX = x;
mLastY = y;
return intercepted;
}
Math.abs(x - mLastX) < Math.abs(y - mLastY)这个条件就是外层布局要拦截的事件的条件,如果Y方向移动的距离大于X方向则移动外层布局。否则的话则不拦截事件,将事件传递给内层View。
红色色块移动相关代码如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
xMove = x;
yMove = y;
onceMoveX = xMove - mLastX;
onceMoveY = yMove - mLastY;
layout(getLeft() + onceMoveX, getTop() + onceMoveY, getRight() + onceMoveX, getBottom() + onceMoveY);
break;
}
return true;
}
X和Y方向都能够移动,如果触摸在外层布局上,则两个方向都可移动。如果触摸在内层View上则需要根据事件拦截情况进行判断。
蓝色色块继承自View,不参与事件拦截的相关处理,因此只有移动的相关代码,比较简单,具体如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
xDown = x;
break;
case MotionEvent.ACTION_MOVE:
xMove = x;
onceMove = xMove - xDown;
layout(getLeft() + onceMove, getTop(), getRight() + onceMove, getBottom());
break;
}
return true;
}
内部拦截演示
效果图如下(下半部分):
图上的绿蓝黄分别代表可随意移动的外层布局,可横向移动的内部View以及可竖向移动的内部View。之所以做内部拦截的演示是有时候并行组合的滑动冲突,外部拦截很大可能无法搞定。
绿色色块色块继承自Framelayout,事件拦截的核心代码如下:
public boolean onInterceptTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
xDown = (int) event.getX();
yDown = (int) event.getY();
return false;
}
return true;
}
基本上就是我们内部拦截的套路,没有什么可讲。
绿色色块移动相关代码:
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
xMove = x;
yMove = y;
onceMoveX = xMove - xDown;
onceMoveY = yMove - yDown;
layout(getLeft() + onceMoveX, getTop() + onceMoveY, getRight() + onceMoveX, getBottom() + onceMoveY);
break;
}
return true;
}
跟外部拦截的基本一致,比较简单。
蓝色色块和绿色色块都继承自View,因为移动方向不同,所以事件拦截的条件以及移动方向有关方法稍有不同,但原理一致。为了便于同外部拦截法进行对照,选用蓝色色块进行讲解
蓝色色块事件拦截的核心代码如下:
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
getParent().requestDisallowInterceptTouchEvent(true);//外层不拦截
}
break;
case MotionEvent.ACTION_MOVE:
if (Math.abs(x - mLastX) < Math.abs(y - mLastY)) {
getParent().requestDisallowInterceptTouchEvent(false);//外层拦截
}
break;
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
基本上是内部拦截的套路,Math.abs(x - mLastX) < Math.abs(y - mLastY)是外层布局的拦截条件,即Y方向移动大于X方向移动。
蓝色色块的移动相关代码跟外部拦截一致。
整体的布局代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.zhou.eventdemo.ParentViewOuter
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_gravity="left"
android:background="#ff0000">
<com.zhou.eventdemo.HorizontalViewOuter
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#0000ff" />
</com.zhou.eventdemo.ParentViewOuter>
<com.zhou.eventdemo.ParentViewInner
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_gravity="right"
android:background="#00ff00">
<com.zhou.eventdemo.HorizontalViewInner
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#0000ff" />
<com.zhou.eventdemo.VerticalViewInner
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginLeft="100dp"
android:layout_marginTop="100dp"
android:background="#ffff00" />
</com.zhou.eventdemo.ParentViewInner>
</LinearLayout>