Understanding of Android View Event Distribution Mechanism

Understanding of Android View Event Distribution Mechanism

background

In our normal development, we will definitely encounter sliding conflicts. However, every time we may need to read other people's blogs to deepen our impression or copy other people's code, although the problem may be solved, but because we do not master the core In principle, it will always feel a little empty. Today, we will use a practical example, combined with the source code, to thoroughly understand the distribution mechanism of View events, so that we can be hard in the future.

example:

The first is a MainActivity layout. Five custom FragmentLayouts of the same size are placed from top to bottom. Each FragmentLayout rewrites three important methods that affect the distribution of View events, dispatchTouchEvent() , onInterceptTouchEvent() , onTouchEvent( ) .

The xml code is as follows:

<?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>
复制代码

The code to customize FrameLayout is as follows:

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)
    }
}
复制代码

The code of MainActivity is as follows:

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)
    }
}
复制代码

Ok, the preparations are done, next, let's start our question:

Question 1: When we quickly click on the screen, please describe the method calling sequence of the View event?

Let's take a look at the log screenshot first: 微信图片_20220410011603.pngFrom the log, we can draw the following conclusions:

① The delivery of touch events starts from the Activitybeginning. The starting point is the dispatchTouchEvent() of MainActivity

②Event delivery will traverse each FragmenLayout in the callback Activity layout in reverse order, callback 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 那事件怎么传递呢?

微信图片_20220410033631.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.

Guess you like

Origin juejin.im/post/7084686187201298440