Android事件分发时,你浓眉大眼的onTouch()竟然没有执行?

问题背景

在开发需求时,有这么一个场景:Activity中有一个ViewGroup作为Parent,ViewGroup里面又有一个Webview作为Child。当一进入页面时,系统输入法自动弹起,而在点击Parent区域时,需要收起系统输入法。 背景介绍完毕,当时的第一想法就是通过Parent设置setOnTouchListener,然后在onTouch()回调中来实现:

csharp 复制代码
mParent.setOnTouchListener { v, event ->
    //在这里关闭系统输入法
    false 
}

然而运行上述代码发现,onTouch()回调没有执行!懵逼妈妈给懵逼开门,懵逼到家了!没办法,只能来排查一下原因了。(可能有的佬们已经看出了问题所在~ )

回顾一下事件传递

以一个简单的事件传递为例,参与者有Activity、ViewGroup、View三个角色。三个角色对应的方法有:

Activity :dispatchTouchEvent、onTouchEvent ViewGroup :dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent View:dispatchTouchEvent、onTouchEvent

事件传递流程如下:

在学习事件传递时,固有认知是:onTouchListener是在onTouchEvent()之前执行的,如果onTouch()中返回了true,那么后续事件就不再传递了。这里的后续事件指的是什么呢?因为onTouchListener平时用的不多,也没有去深究过,直到这次遇到这个问题,下面再去源码里一探究竟吧。

查看源码

csharp 复制代码
//OnTouchListener接口
public interface OnTouchListener {
    boolean onTouch(View v, MotionEvent event);
}

public void setOnTouchListener(OnTouchListener l) {
   getListenerInfo().mOnTouchListener = l;
}

我们知道事件分发到Parent(ViewGroup)时,首先会执行其dispatchTouchEvent方法:

typescript 复制代码
//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()方法中,看下这个方法内部:

csharp 复制代码
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()方法:

csharp 复制代码
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

写个示例来验证下:

kotlin 复制代码
//自定义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
    }

}
kotlin 复制代码
//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:

ini 复制代码
<?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中:

kotlin 复制代码
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输出如下:

rust 复制代码
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中消费事件,即:

kotlin 复制代码
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日志:

rust 复制代码
//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()之前负责拦截的一步,这样就能正确理解他们的执行时机了。

相关推荐
阿巴斯甜15 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker15 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952716 小时前
Andorid Google 登录接入文档
android
黄林晴17 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_2 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android