Android 中典型的滑动冲突
立泉Android 触控事件在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
在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("ViewGroup 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("ViewGroup 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("ViewGroup onInterceptTouchEvent ACTION_UP")
false
}
else -> false
}
LogUtil.d("ViewGroup onInterceptTouchEvent $result")
return result
}
/**
* 处理事件
*/
override fun onTouchEvent(event: MotionEvent?): Boolean {
// 处理所有收到的事件
var result = true
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
LogUtil.d("ViewGroup onTouchEvent ACTION_DOWN")
}
MotionEvent.ACTION_MOVE -> {
var offsetY = event.rawY - yPoint
var offsetX = event.rawX - xPoint
LogUtil.d("ViewGroup onTouchEvent ACTION_MOVE offsetX = $offsetX offsetY = $offsetY")
// 垂直移动
offsetTopAndBottom(offsetY.toInt())
yPoint = event.rawY
xPoint = event.rawX
}
MotionEvent.ACTION_UP -> {
LogUtil.d("ViewGroup onTouchEvent ACTION_UP")
}
}
LogUtil.d("ViewGroup onTouchEvent $result")
return result
}
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
LogUtil.d("ViewGroup event.rawX = ${ev?.rawX} event.rawY = ${ev?.rawY}")
var result = super.dispatchTouchEvent(ev)
LogUtil.d("ViewGroup 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("View onTouchEvent ACTION_DOWN")
// 记录 ACTION_DOWN 事件坐标
xPoint = event.rawX
yPoint = event.rawY
}
MotionEvent.ACTION_MOVE -> {
var offsetX = event.rawX - xPoint
LogUtil.e("View 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("View onTouchEvent ACTION_UP")
}
}
LogUtil.e("View onTouchEvent $result")
return result
}
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
var result = super.dispatchTouchEvent(ev)
LogUtil.e("View dispatchTouchEvent $result")
return result
}
}