Android View事件分发机制的理解
背景
在我们的平常开发中,肯定会遇到滑动冲突的情况,然而每次可能都需要翻阅下别人的博客来加深自己的印象或者copy别人的代码后虽然问题可能会得到解决,但是因为没掌握核心原理,总还是会觉得有点虚。今天我们就以一个实际例子的方式,结合源码,彻底搞清楚View事件的分发机制,让我们以后硬起来。
例子:
首先是一个MainActivity布局,从上到下摆放了五个大小一模一样的自定义FragmentLayout,每个FragmentLayout 里都重写了影响View事件分发的三个重要的方法dispatchTouchEvent(),onInterceptTouchEvent(),onTouchEvent()。
xml代码如下:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.hcc.toucheventdemo.MyFrameLayoutOne
android:id="@+id/frame_layout_1"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.hcc.toucheventdemo.MyFrameLayoutFive
android:id="@+id/frame_layout_5"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</com.hcc.toucheventdemo.MyFrameLayoutOne>
<com.hcc.toucheventdemo.MyFrameLayoutTwo
android:id="@+id/frame_layout_2"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.hcc.toucheventdemo.MyFrameLayoutFour
android:id="@+id/frame_layout_4"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</com.hcc.toucheventdemo.MyFrameLayoutTwo>
<com.hcc.toucheventdemo.MyFrameLayoutThree
android:id="@+id/frame_layout_3"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
自定义FrameLayout的代码如下:
package com.hcc.toucheventdemo
import android.content.Context
import android.util.AttributeSet
import android.util.Log
import android.view.LayoutInflater
import android.view.MotionEvent
import android.widget.FrameLayout
/**
* Created by hecuncun on 2022/4/9
*/
class MyFrameLayoutOne(context: Context,attributeSet: AttributeSet):FrameLayout(context,attributeSet) {
init {
LayoutInflater.from(context).inflate(R.layout.frame_layout,this,true)
}
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
Log.e("HCC","MyFrameLayoutOne dispatchTouchEvent")
when(ev?.action){
MotionEvent.ACTION_DOWN ->{
Log.e("HCC","MyFrameLayoutOne ACTION_DOWN")
}
MotionEvent.ACTION_MOVE->{
Log.e("HCC","MyFrameLayoutOne ACTION_MOVE")
}
MotionEvent.ACTION_UP->{
Log.e("HCC","MyFrameLayoutOne ACTION_UP")
}
}
return super.dispatchTouchEvent(ev)
}
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
Log.e("HCC","MyFrameLayoutOne onInterceptTouchEvent")
return super.onInterceptTouchEvent(ev)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
Log.e("HCC","MyFrameLayoutOne onTouchEvent")
return super.onTouchEvent(event)
}
}
MainActivity的代码如下:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
}
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
Log.e("HCC","MainActivity dispatchTouchEvent")
when(ev?.action){
MotionEvent.ACTION_DOWN ->{
Log.e("HCC","MainActivity ACTION_DOWN")
}
MotionEvent.ACTION_MOVE->{
Log.e("HCC","MainActivity ACTION_MOVE")
}
MotionEvent.ACTION_UP->{
Log.e("HCC","MainActivity ACTION_UP")
}
}
return super.dispatchTouchEvent(ev)
}
}
好了,准备工作已经完成,接下来,开始我们的问题:
问题1:当我们快速点击屏幕后,请叙述一下View事件的方法调用顺序?
咱们先来看下日志截图:
从日志我们可以得出如下几点结论:
① 触摸事件的传递从Activity
开始 . 起点是MainActivity的dispatchTouchEvent()
②事件传递会倒序遍历回调Activity布局中的每个FragmenLayout,回调dispatchTouchEvent(),onInterceptTouchEvent(),onTouchEvent()
③ 如果FragmenLayout有子View则会默认在调用完自己的dispatchTouchEvent(),onInterceptTouchEvent()后再调用子View的dispatchTouchEvent(),onInterceptTouchEvent(),onTouchEvent(),再调用自己的onTouchEvent()
④ 因为默认FragmenLayout dispatchTouchEvent(),onInterceptTouchEvent(),onTouchEvent()都返回false,所以View事件最终交由MainActivity的dispatchTouchEvent()消费
从源码中找寻原因:
首先我们来看下view事件是如何传递到我们的根布局的:先说结论触摸事件的传递从Activity
开始,经过PhoneWindow
,到达顶层视图DecorView
。DecorView
调用了ViewGroup.dispatchTouchEvent()
。
//Activity.class
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
//如果PhoneWindow 消费了事件就返回true
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
//'获得PhoneWindow对象'
public Window getWindow() {
return mWindow;
}
final void attach(...) {
...
//'构造PhoneWindow'
mWindow = new PhoneWindow(this, window, activityConfigCallback);
...
}
//Activity将事件传递给PhoneWindow:
public class PhoneWindow extends Window implements MenuBuilder.Callback {
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
}
//PhoneWindow将事件传递给DecorView
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
public boolean superDispatchTouchEvent(MotionEvent event) {
//'事件最终由ViewGroup.dispatchTouchEvent()分发触摸事件'
return super.dispatchTouchEvent(event);
}
}
//'事件最终由ViewGroup.dispatchTouchEvent()分发触摸事件'
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//接下来是事件分发的关键 我们待会再来分析
}
}
接下来重点分析ViewGroup的dispatchTouchEvent:
先说结论:ViewGroup dispatchTouchEvent() 会在ACTION_DOWN事件传递时根据自身的onInterceptTouchEvent()来决定是否进行拦截ACTION_DOWN,如果不拦截ACTION_DOWN,则会分发事件给孩子,倒序遍历并转换触摸坐标并分发给孩子,跳过不在点击范围的孩子和不能接受点击事件的孩子, 如果没有孩子愿意消费触摸事件,则自己消费,然后有孩子愿意消费触摸事件,将其插入触摸链,遍历触摸链分发触摸事件给所有想接收的孩子 ,如果已经将触摸事件分发给新的触摸目标,则返回true,他们消费触摸事件的方式一摸一样,都是通过View.dispatchTouchEvent()调用View.onTouchEvent()或OnTouchListener.onTouch(),onInterceptTouchEvent()返回true,导致onTouchEvent()被调用,因为onTouchEvent()返回true,导致dispatchTouchEvent()返回true ,ACTION_DOWN发生时,ViewGroup.dispatchTouchEvent()会将愿意消费触摸事件的孩子存储在触摸链中,后序事件会分发给触摸链上的对象。
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//如果是ACTION_DOWN 就取消并清空TouchTarget,重置TouchState
//这个可以重置FLAG_DISALLOW_INTERCEPT标志位,就是ViewGroup的子View 不能通过 parent.requestDisallowInterceptTouchEvent(true) 来拦截如果是ACTION_DOWN事件的原因
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
final boolean intercepted;//是否拦截标识
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {//如果是ACTION_DOWN 或者是ACTION_DOWN的后续事件(mFirstTouchTarget!=null 代表有子View消费了ACTION_DOWN)
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//如果是ACTION_DOWN 则肯定会走onInterceptTouchEvent()事件,因为FLAG_DISALLOW_INTERCEPT这个标识被重置了。ACTION_DOWN的后续事件则会根据子View的requestDisallowInterceptTouchEvent()调用来决定是否走onInterceptTouchEvent()
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
//不拦截
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
//不是ACTION_DOWN 并且 mFirstTouchTarget==null ,
//代表ViewGroup本身拦截了如果是ACTION_DOWN事件
intercepted = true;
}
//分发事件给孩子
if (!canceled && !intercepted) {
final int childrenCount = mChildrenCount;
//有子View
if (newTouchTarget == null && childrenCount != 0){
final View[] children = mChildren;
//倒序遍历
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
//跳过 不再点击范围的子View和不能接受点击事件的子View
if (!child.canReceivePointerEvents() || !isTransformedTouchPointInView(x, y, child, null)){
ev.setTargetAccessibilityFocus(false);
continue;
}
//转换触摸坐标并分发给孩子(child参数不为null)
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
//有孩子愿意消费触摸事件,将其插入“触摸链”'
newTouchTarget = addTouchTarget(child, idBitsToAssign);
//表示已经将触摸事件分发给新的触摸目标
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
//没子View消费触摸事件
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
//如果没有孩子愿意消费触摸事件,则自己消费(child参数为null)
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
//遍历触摸链分发触摸事件给所有想接收的孩子
while (target != null) {
final TouchTarget next = target.next;
//如果已经将触摸事件分发给新的触摸目标,则返回true
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
//如果事件被拦截则cancelChild为true
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
//将触摸事件分发给触摸链上的触摸目标
//将ACTION_CANCEL事件传递给孩子
handled = true;
}
if (cancelChild) {
//如果发送了ACTION_CANCEL事件,将孩子从触摸链上摘除 如果是ACTION_UP事件,则将触摸链清空
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
//返回触摸事件是否被孩子或者自己消费的布尔值
return handled;
}
}
/**
是否消费事件*/
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {
// Canceling motions is a special case. We don‘t need to perform any transformations
// or filtering. The important part is the action, not the contents.
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
//将ACTION_CANCEL事件传递给孩子
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
}
问题2:如果MyFrameLayoutTwo 的onTouchEvent() 返回true 那事件怎么传递呢?
如图:MyFrameLayoutTwo 的onTouchEvent() 返回true ,事件被MyFrameLayoutTwo 消费,MyFrameLayoutOne 就收不到事件了,MyFrameLayoutFour 是MyFrameLayoutTwo的孩子,因为孩子MyFrameLayoutFour 没消费事件onTouchEvent() 返回false,则由MyFrameLayoutTwo 消费onTouchEvent() 返回true.
问题3:如果MyFrameLayoutTwo 的onInterceptTouchEvent()返回true,onTouchEvent()返回false 那事件怎么传递呢?
如图:MyFrameLayoutTwo 的onInterceptTouchEvent()返回true,MyFrameLayoutFour 是MyFrameLayoutTwo的孩子但没收到事件,因为MyFrameLayoutTwo 的onTouchEvent()返回false,则事件继续向Activity的根部局的下个孩子MyFrameLayoutOne传递,然后传递到了MyFrameLayoutOne的孩子MyFrameLayoutFive,MyFrameLayoutFive的onTouchEvent()返回false,回调MyFrameLayoutOne的onTouchEvent(),因最后都没消费,事件回到了Activcity的DispatchTouchEvent.