问题背景
在开发需求时,有这么一个场景:Activity中有一个ViewGroup作为Parent,ViewGroup里面又有一个Webview作为Child。当一进入页面时,系统输入法自动弹起,而在点击Parent区域时,需要收起系统输入法。背景介绍完毕,当时的第一想法就是通过Parent设置setOnTouchListener,然后在onTouch()回调中来实现:
mParent.setOnTouchListener { v, event ->
//在这里关闭系统输入法
false
}
然而运行上述代码发现,onTouch()回调没有执行!懵逼妈妈给懵逼开门,懵逼到家了!没办法,只能来排查一下原因了。(可能有的佬们已经看出了问题所在~ )
回顾一下事件传递
以一个简单的事件传递为例,参与者有Activity、ViewGroup、View三个角色。三个角色对应的方法有:
Activity:dispatchTouchEvent、onTouchEvent
ViewGroup:dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent
View:dispatchTouchEvent、onTouchEvent
事件传递流程如下:

在学习事件传递时,固有认知是:onTouchListener是在onTouchEvent()之前执行的,如果onTouch()中返回了true,那么后续事件就不再传递了。这里的后续事件指的是什么呢?因为onTouchListener平时用的不多,也没有去深究过,直到这次遇到这个问题,下面再去源码里一探究竟吧。
查看源码
//OnTouchListener接口
public interface OnTouchListener {
boolean onTouch(View v, MotionEvent event);
}
public void setOnTouchListener(OnTouchListener l) {
getListenerInfo().mOnTouchListener = l;
}
我们知道事件分发到Parent(ViewGroup)时,首先会执行其dispatchTouchEvent方法:
//ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//...判断onInterceptTouchEvent是否拦截事件,如果拦截,下面不再执行...
//DOWN事件第一次分发会执行到这里
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
} else {
//......
}
如果Parent中的onInterceptTouchEvent没有拦截事件,DOWN事件第一次分发会执行到dispatchTransformedTouchEvent()方法中,看下这个方法内部:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
//......
if (child == null) {
//1
handled = super.dispatchTouchEvent(event);
} else {
//2
handled = child.dispatchTouchEvent(event);
}
}
可以看到如果Parent中有Child(View),那么继续将事件传到Child的dispatchTouchEvent中;反之没有Child的话,则执行super.dispatchTouchEvent(),而ViewGroup继承自View,所以super.dispatchTouchEvent()也是执行到了View中。那接着就看下View中的dispatchTouchEvent()方法:
public boolean dispatchTouchEvent(MotionEvent event) {
//...省略无关代码...
ListenerInfo li = mListenerInfo;
//1
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//2
if (!result && onTouchEvent(event)) {
result = true;
}
return result;
}
逻辑很清晰,在1处,如果mOnTouchListener.onTouch(this, event)返回了true,那么2处的onTouchEvent(event)就不再执行了,这也解释了上一节中的问题:onTouch()中返回了true,影响的后续事件是onTouchEvent。看到这里,也就明白了开头的问题所在:如果事件已经在Child中消费了,那么Parent中的onTouch、onTouchEvent都不会再执行了;除非Child不消费事件,当由Parent来处理事件时,其对应的onTouch()回调才会触发。
示例Demo
写个示例来验证下:
//自定义Parent,把事件打印出来
class OnTouchLinearLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0,
) : LinearLayout(context, attrs, defStyle) {
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
when (ev?.action) {
MotionEvent.ACTION_DOWN -> {
log("Parent:dispatchTouchEvent -> MotionEvent.ACTION_DOWN")
}
MotionEvent.ACTION_MOVE -> {
log("Parent:dispatchTouchEvent -> MotionEvent.ACTION_MOVE")
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
log("Parent:dispatchTouchEvent -> MotionEvent.ACTION_UP")
}
}
return super.dispatchTouchEvent(ev)
}
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
when (ev?.action) {
MotionEvent.ACTION_DOWN -> {
log("Parent:onInterceptTouchEvent -> MotionEvent.ACTION_DOWN")
}
MotionEvent.ACTION_MOVE -> {
log("Parent:onInterceptTouchEvent -> MotionEvent.ACTION_MOVE")
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
log("Parent:onInterceptTouchEvent -> MotionEvent.ACTION_UP")
}
}
return super.onInterceptTouchEvent(ev)
}
override fun onTouchEvent(ev: MotionEvent?): Boolean {
val isConsume = super.onTouchEvent(ev)
when (ev?.action) {
MotionEvent.ACTION_DOWN -> {
log("Parent:onTouchEvent -> MotionEvent.ACTION_DOWN, isConsume:$isConsume")
}
MotionEvent.ACTION_MOVE -> {
log("Parent:onTouchEvent -> MotionEvent.ACTION_MOVE, isConsume:$isConsume")
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
log("Parent:onTouchEvent -> MotionEvent.ACTION_UP, isConsume:$isConsume")
}
}
return isConsume
}
}
//Child
class OnTouchButton @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0,
) : androidx.appcompat.widget.AppCompatButton(context, attrs, defStyle) {
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
when (ev?.action) {
MotionEvent.ACTION_DOWN -> {
log("Child:dispatchTouchEvent -> MotionEvent.ACTION_DOWN")
}
MotionEvent.ACTION_MOVE -> {
log("Child:dispatchTouchEvent -> MotionEvent.ACTION_MOVE")
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
log("Child:dispatchTouchEvent -> MotionEvent.ACTION_UP")
}
}
return super.dispatchTouchEvent(ev)
}
override fun onTouchEvent(ev: MotionEvent?): Boolean {
val isConsume = false
when (ev?.action) {
MotionEvent.ACTION_DOWN -> {
log("Child:onTouchEvent -> MotionEvent.ACTION_DOWN, isConsume:$isConsume")
}
MotionEvent.ACTION_MOVE -> {
log("Child:onTouchEvent -> MotionEvent.ACTION_MOVE, isConsume:$isConsume")
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
log("Child:onTouchEvent -> MotionEvent.ACTION_UP, isConsume:$isConsume")
}
}
return isConsume
}
}
对应的XML:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.launchmode.example.mode.launchMode.ActivityA">
//Parent
<com.launchmode.example.mode.OnTouchLinearLayout
android:id="@+id/ll_parent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/gray_600"
android:gravity="center">
//Child
<com.launchmode.example.mode.OnTouchButton
android:id="@+id/btn_child"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点我点我,查看Log" />
</com.launchmode.example.mode.OnTouchLinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
Activity中:
class TouchActivity : AppCompatActivity() {
private lateinit var mParent: LinearLayout
private lateinit var mChild: Button
@SuppressLint("ClickableViewAccessibility")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_activity)
mParent = findViewById(R.id.ll_parent)
mChild = findViewById(R.id.btn_child)
//1、Parent设置onTouchListener
mParent.setOnTouchListener { v, event ->
log("Parent:onTouch -> ${event.action}")
false
}
//2、Child设置onTouchListener
mChild.setOnTouchListener { v, event ->
log("Child:onTouch -> ${event.action}")
false
}
}
}
点击Child,log输出如下:
Parent:dispatchTouchEvent -> MotionEvent.ACTION_DOWN
Parent:onInterceptTouchEvent -> MotionEvent.ACTION_DOWN
Child:dispatchTouchEvent -> MotionEvent.ACTION_DOWN
Child:onTouch -> 0
Child:onTouchEvent -> MotionEvent.ACTION_DOWN, isConsume:false
Parent:onTouch -> 0
Parent:onTouchEvent -> MotionEvent.ACTION_DOWN, isConsume:false
可以看到Parent和Child的onTouch都执行了,这是因为在Child的onTouchEvent中强制返回的false,即没有消费事件,所以Parent才有机会去处理事件,进而执行了其onTouch、onTouchEvent方法。
PS:可以看到log日志中只有DOWN事件,这是因为DOWN事件最终没有被消费,那么后面的MOVE、UP等事件也不会再下发了。
修改下代码,设置在Child的onTouchEvent中消费事件,即:
class OnTouchButton @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0,
) : androidx.appcompat.widget.AppCompatButton(context, attrs, defStyle) {
//......
override fun onTouchEvent(ev: MotionEvent?): Boolean {
val isConsume = true //这里改为true,即Child消费事件
//...其他不变...
return isConsume
}
}
点击Child之后,再看下log日志:
//DOWN事件
Parent:dispatchTouchEvent -> MotionEvent.ACTION_DOWN
Parent:onInterceptTouchEvent -> MotionEvent.ACTION_DOWN
Child:dispatchTouchEvent -> MotionEvent.ACTION_DOWN
Child:onTouch -> 0
Child:onTouchEvent -> MotionEvent.ACTION_DOWN, isConsume:true
//UP事件
Parent:dispatchTouchEvent -> MotionEvent.ACTION_UP
Parent:onInterceptTouchEvent -> MotionEvent.ACTION_UP
Child:dispatchTouchEvent -> MotionEvent.ACTION_UP
Child:onTouch -> 1
Child:onTouchEvent -> MotionEvent.ACTION_UP, isConsume:true
可以看到Child中消费了DOWN、UP事件,而Parent没有机会再处理事件,因此其onTouch、onTouchEvent也就不会执行了。
结论
通过分析源码和运行示例Demo,明白了为什么Parent(ViewGroup)中的onTouch没有执行,根本原因就是Child把事件消费了,导致事件不再往Parent中传了。知道了问题的原因,解决起来就简单了:可以监听Child的onTouch或者直接在Parent的dispatchTouchEvent中处理即可。
最后再总结下onTouch:不管是Parent(ViewGroup)还是Child(View),可以直观认为onTouch回调都是在其执行到onTouchEvent()之前负责拦截的一步,这样就能正确理解他们的执行时机了。
来源:公众号 作者:代码说 原文地址:Android事件分发时,你浓眉大眼的onTouch()竟然没有执行?
来源:
互联网
本文观点不代表码客-全球程序员交流社区立场,不承担法律责任,文章及观点也不构成任何投资意见。
评论列表