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

相关推荐
robotx2 小时前
安卓线程相关
android
消失的旧时光-19432 小时前
Android 面试高频:JSON 文件、大数据存储与断电安全(从原理到工程实践)
android·面试·json
dalancon3 小时前
VSYNC 信号流程分析 (Android 14)
android
dalancon3 小时前
VSYNC 信号完整流程2
android
dalancon3 小时前
SurfaceFlinger 上帧后 releaseBuffer 完整流程分析
android
用户69371750013844 小时前
不卷AI速度,我卷自己的从容——北京程序员手记
android·前端·人工智能
程序员Android5 小时前
Android 刷新一帧流程trace拆解
android
墨狂之逸才5 小时前
解决 Android/Gradle 编译报错:Comparison method violates its general contract!
android
阿明的小蝴蝶6 小时前
记一次Gradle环境的编译问题与解决
android·前端·gradle
汪海游龙6 小时前
开源项目 Trending AI 招募 Google Play 内测人员(12 名)
android·github