Android View事件分发机制的理解

724 阅读6分钟

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事件的方法调用顺序?

咱们先来看下日志截图:

1.png 从日志我们可以得出如下几点结论:

① 触摸事件的传递从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,到达顶层视图DecorViewDecorView调用了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 那事件怎么传递呢?

2.png 如图:MyFrameLayoutTwo 的onTouchEvent() 返回true ,事件被MyFrameLayoutTwo 消费,MyFrameLayoutOne 就收不到事件了,MyFrameLayoutFour 是MyFrameLayoutTwo的孩子,因为孩子MyFrameLayoutFour 没消费事件onTouchEvent() 返回false,则由MyFrameLayoutTwo 消费onTouchEvent() 返回true.

问题3:如果MyFrameLayoutTwo 的onInterceptTouchEvent()返回true,onTouchEvent()返回false 那事件怎么传递呢?

微信图片_20220410034309.png 如图:MyFrameLayoutTwo 的onInterceptTouchEvent()返回true,MyFrameLayoutFour 是MyFrameLayoutTwo的孩子但没收到事件,因为MyFrameLayoutTwo 的onTouchEvent()返回false,则事件继续向Activity的根部局的下个孩子MyFrameLayoutOne传递,然后传递到了MyFrameLayoutOne的孩子MyFrameLayoutFive,MyFrameLayoutFive的onTouchEvent()返回false,回调MyFrameLayoutOne的onTouchEvent(),因最后都没消费,事件回到了Activcity的DispatchTouchEvent.