​ 我们在完成android需求时,可能会遇到多种控件同时可以滑动的场景,这时就可能会出现滑动冲突。在学习完android事件的分发机制之后,我们来学习滑动冲突的处理办法。

基础

常见的滑动冲突场景

  • 场景一:外部滑动方向和内部不一致。

  • 场景二:外部滑动方向和内部一致。

  • 场景三:以上两种情况的嵌套。

    场景一的应用场景示例:ViewPager和Fragment的配合。(ViewPager内部处理了滑动冲突)

    场景二的应用场景示例:SrollerView里嵌套ListView。

处理规则

场景1

因为内外两层的滑动方向不一致,所以当用户进行外层滑动时,需要让外部的View拦截点击事件,当用户进行内层的滑动时,需要让内部View拦截点击事件。这时我们可以根据滑动的特征来解决滑动冲突。即根据滑动的方向来判断拦截事件的View

这个问题就变得很简单了——我们可以根据滑动过程中两个点的坐标来得出滑动的方向

例如:

  • 水平方向和竖直方向的距离差
  • 滑动路径和水平方向形成的夹角
  • 水平和竖直方向的速度差

场景2

这个场景无法根据场景一的判断条件来做出判断,但我们一般能在业务上找到突破点。比如在某种状态时需要外部View响应用户的滑动,在另一种状态时需要内部View响应。

解决方式

抛开滑动规则,我们需要找到一种不依赖具体的滑动规则的通用的解决方法。

外部拦截法

即点击事件都先经过父容器的拦截处理。如果父容器需要此事件就进行拦截,否则不拦截。

需要重写父容器的onInterceptTouchEvent方法,在方法内做相应的拦截。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
public boolean onInterceptTouchEvent (MotionEvent event) {
boolean intercept = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: //1
intercept = false;
break;
case MotionEvent.ACTION_MOVE:
if( *父容器需要的当前点击事件* ){
intercept = true;
}else{
intercept = false;
}
break;
case MotionEvent.ACTION_UP: //2
intercept = false;
break;
}
lastInterceptX = x;
lastInterceptY = y;
return intercept;
}

针对不同的滑动冲突,只需要修改父容器需要的当前点击事件这个条件即可。

1:不可拦截ACTION_DOWN事件,一旦拦截,后续MOVE和UP事件都会交由父容器处理

2:必须返回false.如果返回true,并且滑动事件交给子View处理,那么子View将接收不到ACTION_UP事件,子View的onClick事件也无法触发。而父View不一样,如果父View在ACTION_MOVE中开始拦截事件,那么后续ACTION_UP也将默认交给父View处理!

内部拦截法

父容器不拦截任何事件,所有事件都传递给子元素,根据子元素对事件的消耗情况判断是否交由父容器处理。

与Android中事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作

需要重写子元素的diapatchTouchEvent方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
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:
int deltaX = x - lastX;
int deltaY = y - lastY;
if( *父容器需要此类点击事件* ){
getParent().requestDisallowInterceptTouchEvent(false); //将事件传递给父元素
}
break;
case MotionEvent.ACTION_UP:
break;
lastX = x;
lastY = y;
return super.dispatchTouchEvent(event);
}

父元素要默认拦截除了ACTION_DOWN以外的其他事件,这样当子元素调用parent.requestDisallowInterceptTouchEvent(false)方法时(即子元素不对该事件进行处理,交由父容器处理),父元素才能继续拦截所需事件。

1
2
3
4
5
6
7
8
9
@Override 
public boolean onInterceptTouchEvent(MotionEvent event) {
int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}

为什么父容器不能拦截ACTION_DOWN事件呢?

因为ACTION_DOWN事件不受FLAG_DISALLOW_INTERCEPT这个标记位的控制,所以一旦父容器拦截了ACTION_DOWN事件,则所有事件都无法传递到子元素中去。

实例

1.场景一

外部拦截

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//处理滑动冲突
@Override
public boolean onInterceptTouchEvent (MotionEvent event) {
boolean intercept = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
intercept = false;
Log.d("this"," false");
if(!scroller.isFinished()) {
scroller.abortAnimation();
}
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - lastInterceptX;
int deltaY = y - lastInterceptY;
//viewGroup拦截水平滑动
if(Math.abs(deltaX) - Math.abs(deltaY) > 0){
intercept = true;
Log.d("this","true");
}else{
intercept = false;
Log.d("this","false");
}
break;
case MotionEvent.ACTION_UP:
intercept = false;
break;
}
lastX = x;
lastY = y;
lastInterceptX = x;
lastInterceptY = y;
return intercept;
}

内部拦截

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//子元素重写的ListView
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: { //不允许父布局拦截
mHorizontalView1.requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (Math.abs(deltaX) > Math.abs(deltaY)) { //要让父布局拦截
mHorizontalView1.requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//父元素HorizontalView
@Override
public boolean onInterceptTouchEvent (MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
if(event.getAction() == MotionEvent.ACTION_DOWN){
lastX = x;
lastY = y;
if(!scroller.isFinished()){
scroller.abortAnimation();
return true;
}
return false;
}else{
return true;
}
}

由于内部拦截比较复杂,一般不推荐使用。

未完待续……