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

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

问题背景

在开发需求时,有这么一个场景: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(thisevent)) {
        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()竟然没有执行?

来源: 互联网
本文观点不代表码客-全球程序员交流社区立场,不承担法律责任,文章及观点也不构成任何投资意见。

赞 ()

相关推荐

  • 置顶 开发项目接单群,免费入群了

    无论你是Android、ios、java、php,或者你是产品经理、老板,都可以免费入群接单或者发布项目,全程不收取任何费用。

    2025年02月21日 15点27分
  • 鸿蒙Next-AttributeModifier结合@Styles和@Extend深度解析使用

    声明式语法引入了@Styles和@Extend两个装饰器,可以解决复用相同自定义样式的问题,但是存在以下受限场景:

    2025年03月03日 15点40分
  • ConstraintLayout之layout_constraintDimensionRatio属性详解

    layout_constraintDimensionRatio 是 ConstraintLayout 提供的一个强大功能,它可以让 View 按照固定的宽高比例自适应尺寸。使用这个属性,可以在 ConstraintLayout 中根据已知的宽度或高度,自动计算另一个维度,确保 View 保持特定的宽高比。

    2025年03月03日 15点39分
  • Android事件分发时,你浓眉大眼的onTouch()竟然没有执行?

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

    2025年03月02日 15点10分
  • Android SDK封装与发布实战指南

    Android SDK封装与发布实战指南

    2025年03月02日 15点05分
  • 鸿蒙Next开发-添加水印以及点击穿透设置

    在鸿蒙Next中,为App全局添加水印可以通过以下方式实现,其中通过窗口添加水印是一种常见且高效的方式。以下是具体方案和实现细节:

    2025年02月26日 23点15分
  • 鸿蒙Next开发-普通函数和箭头函数 this指向的区别以及对UI刷新的影响

    鸿蒙Next开发-普通函数和箭头函数 this指向的区别以及对UI刷新的影响

    2025年02月26日 23点14分
  • 深入探索ArkUI @Builder与@BuilderParam的进阶应用

    在ArkUI的组件化开发体系中,@Builder和@BuilderParam这对装饰器组合扮演着UI模块化的重要角色。二者的差异与配合体现了声明式UI的核心思想:

    2025年02月26日 23点12分
  • Deepseek推荐:Android 开发者需要掌握的系统知识大纲

    一、操作系统基础1. Linux 内核机制内容介绍 Android 基于 Linux 内核,核心机制包括进程管理、内存管理、文件系统、Binder 驱动等。

    2025年02月26日 23点09分
  • Android App 厂商角标适配

    本篇介绍一下笔者在维护IM应用时,设置App角标的相关经验。同时这里设置角标都是基于系统厂商的Launcher,没有适配三方的Launcher应用,因为我们统计下来发现近些年使用三方Launcher应用比较少了,大部分用户还是以系统Launcher为主。所在在我们的项目中,主要是适配各个厂商。

    2025年02月26日 23点07分
  • 鸿蒙Next-方法装饰器以及防抖方法注解实现

    以下是关于 鸿蒙Next(HarmonyOS NEXT)中 MethodDecorator 的详细介绍及使用指南,结合了多个技术来源的实践总结:

    2025年02月26日 22点58分
  • DevEco Studio常用快捷键以及如何跟AndroidStudio的保持同步

    DevEco Studio是华为推出的用于开发HarmonyOS应用的集成开发环境,它提供了丰富的快捷键以提高开发效率,以下为你详细介绍不同操作场景下的常用快捷键:

    2025年02月26日 22点56分
  • Android | 利用ItemDecoration绘制RecyclerView分割线

    RecyclerView.ItemDecoration 是 Android 提供的一种扩展机制,用于为 RecyclerView 的每个子项(Item)添加装饰(Decoration)。它通常用于绘制分割线、边距、背景等,目的是增强 RecyclerView 的显示效果。

    2025年02月26日 22点52分
  • Android 布局优化:利用 ViewStub 和 Merge 提升性能

    提升界面渲染性能是一个至关重要的任务,尤其是在应用启动时,渲染界面需要快速且流畅。为了优化 UI 渲染速度,Android 提供了许多工具,其中 ViewStub 和 Merge 标签是非常有效的布局优化手段。通过合理使用这两者,可以延迟加载不必要的视图、减少布局的嵌套层级,从而加速应用的启动和运行。

    2025年02月26日 22点47分
  • 玩转 ImageView.ScaleType:图片的缩放与裁剪技巧

    ImageView 是最常用的控件之一,它用于展示各种类型的图片。为了能够根据需求调整图片的显示效果,Android 提供了 ImageView.ScaleType 枚举,它可以灵活地控制图片如何适应 ImageView 的尺寸。本文将探讨 ImageView.ScaleType 的不同选项、使用场景及其实现技巧。

    2025年02月26日 22点45分
  • Android加快你的编译速度

    工欲善其事,必先利其器。如果每次运行项目都要花费5-10分钟,那人的心态都要崩了。

    2025年02月25日 15点40分

发表回复

评论列表

点击查看更多

    联系我们

    在线咨询: QQ交谈

    微信:dxmcpjl

    邮件:1529097251#qq.com

    工作时间:周一至周五,9:30-18:30,节假日休息

    微信