Android中典型的滑动冲突
立泉在Android
的View树
中,事件分发自上而下,每一层都可以拦截事件、处理事件,并把处理结果上报。常见的滑动
操作其实就是View
接收触控事件
,再根据触控坐标的变化去移动指定View
的位置。
View
和ViewGroup
嵌套组合构成完整的用户界面,其中有一种典型的滑动冲突,即父View
和子View
都要接收触控事件,但是父View
只上下滑动,子View
只左右滑动,如下图所示:
如果了解Android
的事件分发、拦截机制,可以很清晰的实现这种类型的滑动操作。以本例来说,蓝色
的ViewGroup
应该在触控事件MotionEvent
到达时,判断相邻的2次事件是否是垂直的滑动事件,如果是,就拦截,调用自己的onTouchEvent()
来移动本View
。如果不是垂直滑动事件,就不拦截,将事件分发给红色
的子View
,它会调用自身的onTouchEvent()
来移动自己。
原理很简单,但具体的实现还有一些细节需要处理,尤其是下面的这些问题。
ACTION_DOWN事件的处理
ACTION_DOWN
是一个触控事件序列
的开始,也是最特殊的一个事件,它必须被处理,而它应该何时被处理,何时被拦截,则是解决滑动冲突的关键逻辑。
之前在关于事件分发、拦截机制的文章里提过,ViewGroup
一旦拦截ACTION_DOWN
,如果不处理则同序列后续事件
会在上层被直接拦截,如果处理则同序列后续事件
会直接调用该ViewGroup
的onTouchEvent()
进行处理,而不会再向下分发。这样的话,只要ViewGroup
拦截了ACTION_DOWN
事件,无论它是否处理,其子View
都不可能接收到同事件序列的后续事件
,所以绝不可以在ViewGroup
中拦截ACTION_DOWN
事件。
既然父View
不可以拦截ACTION_DOWN
事件,而它又必须被处理,也就是说子View
必须处理ACTION_DOWN
事件。
非ACTION_DOWN事件的拦截
同样提到的,如果一个事件序列
的某个非ACTION_DOWN
事件被ViewGroup
拦截,无论它是否被处理,这整个事件序列的后续事件都会跳过该ViewGroup
的onInterceptTouchEvent()
,而直接调用其onTouchEvent()
来处理。也就是说,一旦父View
判断这是一个垂直滑动操作而开始拦截事件,则无论它有没有处理后续的事件,子View
都不可能再接收到同序列事件
了。在视图上表现为,手指首先垂直滑动,父View
垂直移动,这是正常的,如果手指开始左右滑动,会发现子View
没有左右移动,是因为这些触控事件并没有被传递到子View
。
另一种情况,在一次触控事件序列
中,如果父View
判断相邻的事件是左右滑动,它就不会拦截,并将这些事件发送给子View
处理,子View
会在onTouchEvent()
中进行水平移动操作,目前都是正常的,但如果手指开始垂直滑动,会发现父View
判断出这是垂直滑动,而开始拦截事件,引起相对应的垂直移动,但子View
此时却收不到事件了。
我认为这种逻辑很奇怪,一次完整的触控事件序列应该只分配给一个View
进行处理,当手指落下并开始滑动时就应该判断出用户的意图是滑动哪个具体的View
,而在后续的滑动事件到达时,不应该再由另一个View
来处理这些事件。所以,在确定此事件序列已经交给子View
处理的情况下,要使用requestDisallowInterceptTouchEvent(true)
禁止父View
再拦截本事件序列的后续事件。
代码示例
首先是蓝色的ViewGroup
:
class EventViewGroup : RelativeLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
/**
* 用于判断是否拦截的触控坐标记录
*/
private var xIntercept = 0f
private var yIntercept = 0f
/**
* 用于判断滑动的触控坐标记录
*/
private var xPoint = 0f
private var yPoint = 0f
// ViewGroup判断是否要拦截事件,否则所有事件均交给View处理
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
var result = when (ev?.action) {
MotionEvent.ACTION_DOWN -> {
LogUtil.e("$id onTouchEvent ACTION_DOWN")
// 记录ACTION_DOWN事件坐标
xIntercept = ev.rawX
yIntercept = ev.rawY
xPoint = ev.rawX
yPoint = ev.rawY
// 不拦截ACTION_DOWN事件
false
}
MotionEvent.ACTION_MOVE -> {
LogUtil.d("$id onInterceptTouchEvent ACTION_MOVE")
// 只拦截竖向事件
val moveY = ev.rawY - yIntercept
val moveX = ev.rawX - xIntercept
xIntercept = ev.rawX
yIntercept = ev.rawY
if (moveX == moveY && moveX == 0f) {
// 收到重复事件,不拦截
false
} else if (Math.abs(moveY) >= Math.abs(moveX)) {
// 垂直距离大于水平距离,拦截
true
} else {
// 其余情况不拦截
false
}
}
MotionEvent.ACTION_UP -> {
LogUtil.d("$id onInterceptTouchEvent ACTION_UP")
false
}
else -> false
}
LogUtil.d("$id onInterceptTouchEvent $result")
return result
}
/**
* 处理事件
*/
override fun onTouchEvent(event: MotionEvent?): Boolean {
// 处理所有收到的事件
var result = true
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
LogUtil.d("$id onTouchEvent ACTION_DOWN")
}
MotionEvent.ACTION_MOVE -> {
var offsetY = event.rawY - yPoint
var offsetX = event.rawX - xPoint
LogUtil.d("$id onTouchEvent ACTION_MOVE offsetX = $offsetX offsetY = $offsetY")
// 垂直移动
offsetTopAndBottom(offsetY.toInt())
yPoint = event.rawY
xPoint = event.rawX
}
MotionEvent.ACTION_UP -> {
LogUtil.d("$id onTouchEvent ACTION_UP")
}
}
LogUtil.d("$id onTouchEvent $result")
return result
}
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
LogUtil.d("$id event.rawX = ${ev?.rawX} event.rawY = ${ev?.rawY}")
var result = super.dispatchTouchEvent(ev)
LogUtil.d("$id dispatchTouchEvent $result")
return result
}
}
然后是红色的View
:
class EventView : ImageView {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
/**
* 用于判断滑动的触控坐标记录
*/
private var xPoint = 0f
private var yPoint = 0f
/**
* 处理事件
*/
override fun onTouchEvent(event: MotionEvent?): Boolean {
// 处理所有收到的事件
var result = true
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
LogUtil.e("$id onTouchEvent ACTION_DOWN")
// 记录ACTION_DOWN事件坐标
xPoint = event.rawX
yPoint = event.rawY
}
MotionEvent.ACTION_MOVE -> {
var offsetX = event.rawX - xPoint
LogUtil.e("$id onTouchEvent ACTION_MOVE offsetX = $offsetX")
// 水平移动
if (offsetX != 0f) {
offsetLeftAndRight(offsetX.toInt())
// 不允许父View再拦截本事件序列的后续事件
parent.requestDisallowInterceptTouchEvent(true)
}
xPoint = event.rawX
yPoint = event.rawY
}
MotionEvent.ACTION_UP -> {
LogUtil.e("$id onTouchEvent ACTION_UP")
}
}
LogUtil.e("$id onTouchEvent $result")
return result
}
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
var result = super.dispatchTouchEvent(ev)
LogUtil.e("$id dispatchTouchEvent $result")
return result
}
}