【Android】一个demo理解dispatchTouchEvent、onInterceptTouchEvent与onTouchEvent
简述:
事件分发机制是指Android中触摸事件MotionEvent的传递机制,传递流程是Activity->Window->DecorView->ViewGroup->View。其中有三个重要的方法:dispatchTouchEvent,onInterceptTouchEvent(ViewGroup有,View没有),onTouchEvent。
- dispatchTouchEvent用于分发事件,ViewGroup在此方法中,会将事件分发给对应子View。当事件被子View或者当前ViewGroup处理时,返回true。
- onInterceptTouchEvent用于父View拦截子View事件,若拦截成功,即返回true,则子View将不再处理后序的事件。默认是不拦截的。
- onTouchEvent用于响应点击,返回true代表事件被当前View处理,返回false则向上传递。
示例:
首先自定义两个View,一个继承自View,一个继承自LinearLayout:
TestView.kt
package com.example.toucheventtest.viewsimport android.content.Context
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.view.View
import com.example.toucheventtest.constant.Utilsclass TestView: View {companion object {const val TAG = "TestView"}constructor(context: Context) : super(context) {}constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {}override fun onTouchEvent(event: MotionEvent?): Boolean {val viewTag = "$TAG onTouchEvent"when (event?.action) {MotionEvent.ACTION_DOWN -> {Log.d(Utils.LOG_TAG, "$viewTag down")}MotionEvent.ACTION_MOVE -> {Log.d(Utils.LOG_TAG, "$viewTag move")}MotionEvent.ACTION_UP -> {Log.d(Utils.LOG_TAG, "$viewTag up")}MotionEvent.ACTION_CANCEL -> {Log.d(Utils.LOG_TAG, "$viewTag cancel")}}return super.onTouchEvent(event)}override fun dispatchTouchEvent(event: MotionEvent?): Boolean {val viewTag = "$TAG dispatchTouchEvent"Log.w(Utils.LOG_TAG, "$viewTag Start =>")val dispatched = super.dispatchTouchEvent(event);Log.w(Utils.LOG_TAG, "$viewTag End => $dispatched")return dispatched}
}
TestLinearLayout.kt
package com.example.toucheventtest.viewsimport android.content.Context
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.widget.LinearLayout
import com.example.toucheventtest.constant.Utilsclass TestLinearLayout: LinearLayout {companion object {const val TAG = "TestLinearLayout"}constructor(context: Context) : super(context) {}constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {}override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {val viewTag = "$TAG onInterceptTouchEvent"when (ev?.action) {MotionEvent.ACTION_DOWN -> {Log.d(Utils.LOG_TAG, "$viewTag down")}MotionEvent.ACTION_MOVE -> {Log.d(Utils.LOG_TAG, "$viewTag move")}MotionEvent.ACTION_UP -> {Log.d(Utils.LOG_TAG, "$viewTag up")}MotionEvent.ACTION_CANCEL -> {Log.d(Utils.LOG_TAG, "$viewTag cancel")}}return super.onInterceptTouchEvent(ev)}override fun onTouchEvent(event: MotionEvent?): Boolean {val viewTag = "$TAG onTouchEvent"when (event?.action) {MotionEvent.ACTION_DOWN -> {Log.d(Utils.LOG_TAG, "$viewTag down")}MotionEvent.ACTION_MOVE -> {Log.d(Utils.LOG_TAG, "$viewTag move")}MotionEvent.ACTION_UP -> {Log.d(Utils.LOG_TAG, "$viewTag up")}MotionEvent.ACTION_CANCEL -> {Log.d(Utils.LOG_TAG, "$viewTag cancel")}}return super.onTouchEvent(event)}override fun dispatchTouchEvent(event: MotionEvent?): Boolean {val viewTag = "$TAG dispatchTouchEvent"Log.w(Utils.LOG_TAG, "$viewTag Start =>")val dispatched = super.dispatchTouchEvent(event);Log.w(Utils.LOG_TAG, "$viewTag End => $dispatched")return dispatched}
}
然后使用自定义的ViewGroup包裹自定义的View:
<?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=".TouchEventTestActivity"><com.example.toucheventtest.views.TestLinearLayoutandroid:orientation="vertical"android:layout_width="200dp"android:layout_height="200dp"android:background="#ff00ff"app:layout_constraintTop_toTopOf="parent"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintRight_toRightOf="parent"><com.example.toucheventtest.views.TestViewandroid:id="@+id/childView"android:layout_width="80dp"android:layout_height="80dp"android:layout_gravity="center"android:background="#00ffff"/></com.example.toucheventtest.views.TestLinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
效果图:
方法调用顺序:
- 不设置setOnClickListener,setOnTouchListener,setClickable。
点击/拖拽ViewGroup:
点击/拖拽View:
结论:
- 点击或拖拽ViewGroup时仅调用了ViewGroup的dispatchTouchEvent并返回false,不调用View的dispatchTouchEvent,即不会将事件分发给View。并且会调用自身的onTouchEvent。
- 点击或拖拽View时先调用了ViewGroup的dispatchTouchEvent,再调用了View的dispatchTouchEvent,View的dispatchTouchEvent先返回false,ViewGroup的dispatchTouchEvent再返回false,呈现递归调用。
- 无论是点击或拖拽ViewGroup还是View,在调用ViewGroup的dispatchTouchEvent之后,都会调用一次ViewGroup的onInterceptTouchEvent。
- 点击或拖拽ViewGroup时,会在onInterceptTouchEvent之后调用ViewGroup的onTouchEvent。
- 点击或拖拽View时,会在onInterceptTouchEvent之后调用View的onTouchEvent,再调用ViewGroup的onTouchEvent。
- ViewGroup和View的仅接收到down事件,且点击与拖拽没有区别。
- 设置View可点击,ViewGroup不可点击
点击/拖拽ViewGroup:
点击View:
拖拽View:
结论:
- 点击或拖拽ViewGroup时仅调用了ViewGroup的dispatchTouchEvent并返回false,不调用View的dispatchTouchEvent,即不会将事件分发给View。
- 点击或拖拽View时先调用了ViewGroup的dispatchTouchEvent,再调用了View的dispatchTouchEvent,View的dispatchTouchEvent先返回true,ViewGroup的dispatchTouchEvent再返回true。
- 点击或拖拽ViewGroup时,在调用ViewGroup的dispatchTouchEvent之后,会调用一次ViewGroup的onInterceptTouchEvent,且仅在down事件时被调用。
- 点击或拖拽View时,在调用ViewGroup的dispatchTouchEvent之后,会调用一次ViewGroup的onInterceptTouchEvent,且在down、move以及up时都会调用。
- 点击或拖拽ViewGroup时,会在onInterceptTouchEvent之后调用ViewGroup的onTouchEvent,且仅在down时调用。
- 点击或拖拽View时,会在onInterceptTouchEvent之后调用View的onTouchEvent,且在down、move以及up时都会调用。
- Touch事件分发在拖拽时总是满足down->若干个move->up这样的响应顺序,点击时无move事件仅down->up。
- 设置ViewGroup可点击,View不可点击
点击ViewGroup:
拖拽ViewGroup:
点击View:
拖拽View:
结论:
- 点击或拖拽ViewGroup时仅调用了ViewGroup的dispatchTouchEvent并返回false,不调用View的dispatchTouchEvent,即不会将事件分发给View。
- 点击或拖拽View时先调用了ViewGroup的dispatchTouchEvent,再调用了View的dispatchTouchEvent,View的dispatchTouchEvent先返回false,ViewGroup的dispatchTouchEvent再返回true。
- 无论是点击或拖拽ViewGroup还是View,在调用ViewGroup的dispatchTouchEvent之后,都会调用一次ViewGroup的onInterceptTouchEvent。
- 点击或拖拽ViewGroup时,会调用ViewGroup的onTouchEvent,且在down、move以及up时都会调用。
- 点击或拖拽View时,会在onInterceptTouchEvent之后调用View的onTouchEvent,仅在down时被调用。再调用ViewGroup的onTouchEvent,且在down、move以及up时都会调用。
- Touch事件分发在拖拽时总是满足down->若干个move->up这样的响应顺序,点击时无move事件仅down->up。
- 设置均为可点击
点击ViewGroup:
拖拽ViewGroup:
点击View:
拖拽View:
结论:
- 点击或拖拽ViewGroup时仅调用了ViewGroup的dispatchTouchEvent并返回true,不调用View的dispatchTouchEvent,即不会将事件分发给View。
- 点击或拖拽View时先调用了ViewGroup的dispatchTouchEvent,再调用了View的dispatchTouchEvent,View的dispatchTouchEvent先返回true,ViewGroup的dispatchTouchEvent再返回true。
- 点击或拖拽ViewGroup时,在调用ViewGroup的dispatchTouchEvent之后,会调用一次ViewGroup的onInterceptTouchEvent,且仅在down事件时被调用。
- 点击或拖拽View时,在调用ViewGroup的dispatchTouchEvent之后,会调用一次ViewGroup的onInterceptTouchEvent,且在down、move以及up时都会调用。
- 点击或拖拽ViewGroup时,会调用ViewGroup的onTouchEvent,且在down、move以及up时都会调用。
- 点击或拖拽View时,会在onInterceptTouchEvent之后调用View的onTouchEvent,且在down、move以及up时都会调用。
- Touch事件分发在拖拽时总是满足down->若干个move->up这样的响应顺序,点击时无move事件仅down->up。
- ViewGroup拦截down事件,onInterceptTouchEvent在down事件时返回true
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {val viewTag = "$TAG onInterceptTouchEvent"when (ev?.action) {MotionEvent.ACTION_DOWN -> {Log.d(Utils.LOG_TAG, "$viewTag down")return true}MotionEvent.ACTION_MOVE -> {Log.d(Utils.LOG_TAG, "$viewTag move")}MotionEvent.ACTION_UP -> {Log.d(Utils.LOG_TAG, "$viewTag up")}MotionEvent.ACTION_CANCEL -> {Log.d(Utils.LOG_TAG, "$viewTag cancel")}}return super.onInterceptTouchEvent(ev)}
拖拽ViewGroup:
拖拽View:
tips:点击不再列举,去掉move事件分发部分就是点击后的流程。
结论:
- 点击或拖拽ViewGroup&点击或拖拽View表现上一致。
- 点击或拖拽ViewGroup&点击或拖拽View,仅调用了ViewGroup的dispatchTouchEvent并返回true,不调用View的dispatchTouchEvent,即不会将事件分发给View。
- 点击或拖拽ViewGroup&点击或拖拽View,在调用ViewGroup的dispatchTouchEvent之后,会调用一次ViewGroup的onInterceptTouchEvent,且仅在down事件时被调用。
- 点击或拖拽ViewGroup&点击或拖拽View,会调用ViewGroup的onTouchEvent,且在down、move、up时都被调用。
- ViewGroup拦截move事件,onInterceptTouchEvent在move事件时返回true
拖拽ViewGroup:
tips:点击不再列举,去掉move事件分发部分就是点击后的流程。
点击View:
拖拽View:
结论:
- 点击或拖拽ViewGroup,仅调用了ViewGroup的dispatchTouchEvent并返回true,不调用View的dispatchTouchEvent,即不会将事件分发给View。在调用ViewGroup的dispatchTouchEvent之后,会调用一次ViewGroup的onInterceptTouchEvent,且仅在down事件时被调用。还会会调用ViewGroup的onTouchEvent,且在down、move、up时都被调用。
- 点击View时先调用了ViewGroup的dispatchTouchEvent,再调用ViewGroup的onInterceptTouchEvent,接着再调用View的dispatchTouchEvent,以及View的onTouchEvent,且在down和up时都会被调用。View的dispatchTouchEvent先返回true,ViewGroup的dispatchTouchEvent再返回true。
- 拖拽View时先调用了ViewGroup的dispatchTouchEvent,再调用ViewGroup的onInterceptTouchEvent,在down和以及第一个move事件时调用。接着再调用View的dispatchTouchEvent,以及onTouchEvent,分发down事件,View的dispatchTouchEvent先返回true,ViewGroup的dispatchTouchEvent再返回true。move事件被拦截后,向View分发一个cancel事件。后续的move、up事件不再向View分发,不再调用View的dispatchTouchEvent&onTouchEvent以及ViewGroup的onInterceptTouchEvent,转而调用ViewGroup的dispatchTouchEvent&onTouchEvent。
- ViewGroup拦截up事件,onInterceptTouchEvent在up事件时返回true
拖拽ViewGroup:
tips:点击不再列举,去掉move事件分发部分就是点击后的流程。
点击View:
拖拽View:
结论:
- 点击或拖拽ViewGroup,仅调用了ViewGroup的dispatchTouchEvent并返回true,不调用View的dispatchTouchEvent,即不会将事件分发给View。在调用ViewGroup的dispatchTouchEvent之后,会调用一次ViewGroup的onInterceptTouchEvent,且仅在down事件时被调用。还会会调用ViewGroup的onTouchEvent,且在down、move、up时都被调用。
- 点击View时先调用了ViewGroup的dispatchTouchEvent,再调用ViewGroup的onInterceptTouchEvent,接着再调用View的dispatchTouchEvent,以及View的onTouchEvent,down事件正常分发,up事件被拦截后向View分发了cancel。View的dispatchTouchEvent先返回true,ViewGroup的dispatchTouchEvent再返回true。
- 拖拽View时先调用了ViewGroup的dispatchTouchEvent,再调用ViewGroup的onInterceptTouchEvent,在down、move以及up时都会调用。接着再调用View的dispatchTouchEvent以及View的onTouchEvent,down、 move事件正常分发,up事件被拦截后向View分发了cancel。View的dispatchTouchEvent先返回true,ViewGroup的dispatchTouchEvent再返回true。