View 的滑动冲突解决办法

147 阅读3分钟

谈起 Android 中 View 的滑动冲突,我们先要了解一下 View 的事件分发机制,因为理解事件分发机制是解决滑动冲突的理论基础。

View 的点击事件分发概述

点击事件的分发过程由三个很重要的方法来共同完成:

  • disptchTouchEvent:进行事件分发
  • onInterceptTouchEvent:判断是否拦截
  • onTouchEvent:处理点击事件 上面三个方法用伪代码表示如下
public boolean dispatchTouchEvent(MotionEvent ev) {
  boolean consume = false;
    if (onInterceptTouchEvent(ev)) {
        consume = onTouchEvent(ev);
    }else{
      consume = child.dispatchTouchEvent(ev);
    }
    return consume;
}

通过上面的伪代码,大致可以了解点击事件的传递规则:
当一个 ViewGroup 接收到一个事件的时候,首先会调用 dispatchTouchEvent() 方法进行事件分发,如果 onInterceptTouchEvent() 返回 true,则代表当前 View 会拦截事件,则直接回调 onTouchEvent() 方法进行事件处理。如果不拦截,则直接回调子 View 的 dispatchTouchEvent() 方法。如此反复,一直到最里面的子 View。
当一个点击事件产生后,它的传递过程遵循以下顺序:Activity => Window => View,即事件总是先传递给 Activity,Activity 再传递给 Window,最后 Window 再传递给顶层 DecorView,然后遵循上面的方式一直在最里层 View。
而处理事件则从最里层 View 不断回传给自己的外层 View,如果一直没有 View 进行处理,则直接会回传到 Activity 中。

常见的滑动冲突场景

滑动冲突的场景一般有下面两种情况:

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

滑动冲突场景.png 还有的场景是上面两种情况的嵌套,这种场景稍微要复杂。但是不管多复杂的滑动冲突,它们之间的区别仅仅是滑动的规则不同而已。
对于场景一,滑动规则比较简单:根据滑动方向是水平方向还是竖直方向来判断到底由谁来拦截事件。那如何来判断滑动的方向呢?可以根据两个方向的距离差、速度差来做判断。
对于场景二,由于是同一方向的滑动,无法从物理上做出判断,所以这时候需要在业务上来寻找突破点。业务上能确定在某种状态下需要由相应的 View 来拦截处理事件。

滑动冲突的解决办法

确定了滑动规则之后,那么开始着手解决滑动冲突,其实无外乎就两种方法:外部拦截法和内部拦截法。

外部拦截法

外部拦截法是在父容器进行拦截处理。重写父容器的 onInterceptTouchEvent 方法,在此方法做相应的拦截即可。代码如下:

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;
	}
	mLastXIntercept = x;
	mLastYIntercept = y;
	return intercepted;
}

内部拦截法

内部拦截法是指父容器不拦截任何事件,所有的事件都传递到子元素,由子元素来决定是否消费事件。需要配合 requestDisallowInterceptTouchEvent 方法来完成。

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 deltaX = x - mLastX;
			int deltaY = y - mLastY;
			if (父容器需要此类点击事件) {
				parent.requestDisallowInterceptTouchEvent(false);
			}
			break;
		}
		case MotionEvent.ACTION_UP: {
			break;
		}
		default:
			break;
	}

	mLastX = x;
	mLastY = y;
	return super.dispatchTouchEvent(event);
}