View 事件分发机制-再总结

前言

关于View的事件分发机制,网上已经有很多的资料,但是纸上得来终觉浅呀,所以自己动手理了一遍,并写下该文章作为记录。此文名为再总结,就是希望站在巨人的肩旁上,留下自己的一点文字。

背景知识

页面构成

在 Android 系统中,一个负责交互的页面一般用 Activity 类来实现。
一个 Activity 会持有一个 PhoneWindow,而 PhoneWindow 有持有一个 DecorView,这个 DecorView 就是整个页面的根View了,DecorView 其实是一个 ViewGroup,其内部含有为 title 和 content 两个Layout区域;
当我们调用 Activity 的 setContentView() 函数传进的 xml 布局文件,将会作为 DecorView 中的 content 区域的子 View 。

Activity 构成的一种案例:

第0层:DecorView
-第1层,第1个View:LinearLayout (screen_simple.xml)
--第2层,第1个View:ViewStub
--第2层,第2个View:FrameLayout(装载 activity_my.xml 的父容器,id 为 content)
---第3层,第1个View:LinearLayout(activity_my.xml )
----第4层,第1个View:TextView
触摸事件分类

当我们在手机屏幕上进行点击,长按或者滑动等操作,对于 Android 系统来说,就是一次事件序列,该事件序列中含有三种事件类型:

  • Down
  • Move
  • Up

这一事件序列,会从 Activity 传递到 DecorView,再传递给 DecorView 的各个子 View,直到找到处理事件的子View,称为 target。

从 Activity 到 DecorView 的传递的源码,可以参考该链接

用伪代码来看整体逻辑
引用 该链接 的伪代码来表示整体逻辑

ViewGroup 类的 dispatchTouchEvent 函数的伪代码:

// ViewGroup

public boolean dispatchTouchEvent(MotionEvent ev) {
    // 事件是否被消费
    boolean consume = false;

    // 判断是否拦截事件
    if ( !onInterceptTouchEvent(ev) ){ 
		// 不拦截,调用子 View 的 dispatchTouchEvent 方法
		for循环:consume = child.dispatchTouchEvent(ev);
    }
		
    // 如果拦截,或者没有子 View 消费,consume 都为 false
    // 此时会调用自身的 onTouchEvent 方法
    if ( !consume ) onTouchEvent(ev); 

    // 返回值表示事件是否被消费,
    // true 即事件被消费,false 则调用父 View 的 onTouchEvent方法继续处理
    return consume;
}

View 类的 dispatchTouchEvent 函数的伪代码:
// View

public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean consume = false;//事件是否被消费

    if ( onTouch(ev) ) { //判断是否拦截事件
        return true;
    }
		
    if ( onTouchEvent(ev) ) { //判断是否拦截事件
        return true;
    }

    // 返回值表示事件是否被消费,
    // true 即事件被消费,false 则调用父 View 的 onTouchEvent方法继续处理
    return consume;
}

从该段伪代码中提取出:
  • ViewGroup#dispatchTouchEvent
    • onInterceptTouchEvent 拦截判断
    • child.dispatchTouchEvent 分发子类
      • View#onTouch
      • View#onTouchEvent – onClick
    • onTouchEvent 自处理

整体时序图

本次事件的分发,分为 7 个步骤来分析,在看源码前,先看看整体时序图:

Activity DecorView ViewGroup childView dispatchTouchEvent 跟 ViewGroup 的 0-3 步一样 dispatchTouchEvent 0,重置 mFirstTouchTarget 和 mGroupFlags 1,onInterceptTouchEvent 2,for : 遍历子 child 3,dispatchTransformedTouchEvent(.., child, ...) 4,子 child.dispatchTouchEvent 5,onTouchEvent 6,performClickInternal -- onClick 返回 true/false 7,dispatchTransformedTouchEvent(.., null, ...) super.dispatchTouchEvent( event ) ( 父类View )onTouchEvent 返回 true/false 跟 ViewGroup 的 7 步一样 返回 true/false Activity DecorView ViewGroup childView

该时序图中的7个步骤跟下面的分析时对应的。 下面进入源码环节,从根 ViewGroup 开始看:

第一,判断是否拦截

事件传到 ViewGroup 的分发函数 dispatchTouchEvent :

// 代码段一:
// ViewGroup 类
// 该段函数共3个关键点,1-1 至 1-3 

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean handled = false;
        
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // Handle an initial down.
            if (actionMasked == MotionEvent.ACTION_DOWN) {
            
                // 置空 mFirstTouchTarget
                cancelAndClearTouchTargets(ev);
                
                // 重置 mGroupFlags:
                resetTouchState();
            }

            // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                // 能进来这里,只有两种情况:
                // down,mFirstTouchTarget 为空,刚刚被重置
                // move/up,mFirstTouchTarget 不为空,有子 View 消费事件

                // 1-1
                // 当第一种情况:down 事件时,上述 resetTouchState 函数重置 mGroupFlags,
                // disallowIntercept 为 false,表示允许拦截,走下面的 onInterceptTouchEvent 函数判断是否拦截
                
                // 当第二种情况:move/up 事件时,若前一个事件传到子 View 时,
                // 调用 getParent().requestDisallowInterceptTouchEvent(true) 改变了 mGroupFlags 的值;
                // 则后一个事件到此处,disallowIntercept 为 true,不允许拦截,不走 onInterceptTouchEvent 函数,直接放行
                // 否则,disallowIntercept 为 false,允许拦截,走 onInterceptTouchEvent 函数判断是否拦截
                // 本类此处代码往后,不会改动 mGroupFlags 中的 FLAG_DISALLOW_INTERCEPT 标志位,
                // 所以只有子 View 会改动该标志位了。
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

                if (!disallowIntercept) {
                    // down 事件才会进这里
                    // 1-2
                    intercepted = onInterceptTouchEvent(ev); // 默认返回false,不拦截
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // 1-3
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
                // 之前的 down 事件,如果有子 View 处理,则已经赋值了 mFirstTouchTarget,使其不为空,
                // 则不会走这里;
                // 若 move/up 走到这里的,则 mFirstTouchTarget 为空,没有子 View 处理事件
                // 表示本 ViewGroup 继续处理此次 touch 事件。
            }
	
	// 省略代码 ......

注意:
注释1-1处的 FLAG_ DISALLOW_INTERCEPT 变量,子View 可以通过 requestDisallowInterceptTouchEvent 方法,设置该变量,干预 父ViewGroup 的事件分发过程(ACTION_DOWN事件除外),而这就是我们处理滑动冲突常用的关键方法。接着看看重置 mGroupFlags 的函数和 拦截判断函数。

goto resetTouchState()onInterceptTouchEvent(ev)

// 代码段1.1:
// ViewGroup.java

    /**
     * Resets all touch state in preparation for a new cycle.
     */
    private void resetTouchState() {
        clearTouchTargets();
        resetCancelNextUpFlag(this);
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        mNestedScrollAxes = SCROLL_AXIS_NONE;
    }

    @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            // We're already in this state, assume our ancestors are too
            return;
        }

        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT; 
        } else {
             // 重置,跟 resetTouchState 函数一致
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }

        // Pass it up to our parent
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }

mGroupFlags 变量是一个 32 位 int 值,其第 20 位是 0 或者 1,就代表着 disallowIntercept 变量为 false 或者 true,在代码中通过「&」位运算来确认:
disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0。
重置则通过取反再「&」位运来实现: mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT,
这样 mGroupFlags 变量第 20 位会被置为 0。

第二,遍历子 View,逐个分发

ViewGroup#dispatchTouchEvent 函数的剩余部分:

// 代码段2:
// ViewGroup.java
// 该段函数共6个关键点,2-1 至 2-6
    ......

    TouchTarget newTouchTarget = null;
    boolean alreadyDispatchedToNewTouchTarget = false;
	if (!canceled && !intercepted) {

        // ......

        if (actionMasked == MotionEvent.ACTION_DOWN
                || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
            final int actionIndex = ev.getActionIndex(); // always 0 for down
            final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                    : TouchTarget.ALL_POINTER_IDS;

            // Clean up earlier touch targets for this pointer id in case they
            // have become out of sync.
            removePointersFromTouchTargets(idBitsToAssign);

            final int childrenCount = mChildrenCount;
            if (newTouchTarget == null && childrenCount != 0) {
                final float x = ev.getX(actionIndex);
                final float y = ev.getY(actionIndex);
                // Find a child that can receive the event.
                // Scan children from front to back.
                final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                final boolean customOrder = preorderedList == null
                        && isChildrenDrawingOrderEnabled();
                final View[] children = mChildren;
                // 2-1,倒序遍历,即从最外层的 View 往内层遍历
                for (int i = childrenCount - 1; i >= 0; i--) {
                    final int childIndex = getAndVerifyPreorderedIndex(
                            childrenCount, i, customOrder);
                    final View child = getAndVerifyPreorderedView(
                            preorderedList, children, childIndex);

                    // 。。。

                    // 2-2,触摸点是否在 View 的范围内,
                    // 该 View 是否在播放动画,均不符合,就下一个
                    if (!canViewReceivePointerEvents(child) 
                            || !isTransformedTouchPointInView(x, y, child, null)) { 
                        ev.setTargetAccessibilityFocus(false);
                        continue;
                    }

                    // 2-3,逐个把事件传递给子 View,若其中某个子 View 返回 true,
                    // 则说明处理了点击事件,进入函数体处理后,break 掉循环遍历子 View
                    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();
                        // 2-4,记录处理事件的子 View ,
		                // 在 addTouchTarget 函数内部,赋值 target 给 mFirstTouchTarget,
		                // 并返回引用给 newTouchTarget
                        newTouchTarget = addTouchTarget(child, idBitsToAssign);
                        alreadyDispatchedToNewTouchTarget = true;
                        break;
                    }

                    // The accessibility focus didn't handle the event, so clear
                    // the flag and do a normal dispatch to all children.
                    ev.setTargetAccessibilityFocus(false);
                }
                if (preorderedList != null) preorderedList.clear();
            }

            // 2-5,若没有找到子 View 处理事件,则指向最新添加的子 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;
            }
        }
    }
    // 到此,分发事件给子View的工作结束了

    // Dispatch to touch targets.
    if (mFirstTouchTarget == null) {
                // 2-6
                // 没有子View处理事件
                // No touch targets so treat this as an ordinary view.
                // 注意传入的 child 参数为 null,内部会调用 super.dispatchTouchEvent(event);
                // 即走 View#dispatchTouchEvent --> onTouchEvent
                handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
    }
    
    ......


先看 注释2-4处 的 addTouchTarget 函数: **如果找到了处理事件的子 View,将其引用赋值给 mFirstTouchTarget** goto `addTouchTarget(child, idBitsToAssign)`
// 代码段2.1:
// ViewGroup.java

    /**
     * Adds a touch target for specified child to the beginning of the list.
     * Assumes the target child is not already present.
     */
    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

再回看注释2-3处。

第三,真正的分发事件给子 View

接着 注释2-3,goto dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)

// 代码段3:
// ViewGroup.java

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

        // 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) {
                // 子类不存在,调用父类 View(注意不是父视图)的分发逻辑,如代码段4 一致
                // ViewGroup 继承 View 类,所以也具有父类的特性
                handled = super.dispatchTouchEvent(event);
            } else {
                // 3-1,子类存在,调用子类的 dispatchTouchEvent 分发函数
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }

接着注释3-1处…

第四,事件传递到子 View

接着 注释3-1处:
goto child.dispatchTouchEvent(event)

// 代码段4:
// View.java

    public boolean dispatchTouchEvent(MotionEvent event) {
    
        .....

        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    // 4-1,优先 onTouch 函数
                    && li.mOnTouchListener.onTouch(this, event)) { 
                result = true;
            }

            // 4-2,其次 onTouchEvent 函数
            if (!result && onTouchEvent(event)) { 
                result = true;
            }
        }

	// 。。。
        return result;
    }

接着注释4-2处…

第五:子 View 的 onTouchEvent 函数

接着 注释4-2处:goto onTouchEvent(event)

// 代码段5:
// View.java

    public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();
	   // 5-1,只要设置点击或长按监听的其中一个,就会处理事件并返回 true
        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

	  // 。。。

        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
		     // 。。。
                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {
                                // Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    // 5-2 Up 事件时,执行处理点击的逻辑
                                    performClickInternal(); 
                                }
                            }
                        }

		     // 。。。
                    break;

                case MotionEvent.ACTION_DOWN:
		     // 。。。
                    break;

                case MotionEvent.ACTION_CANCEL:
		     // 。。。
                    break;

                case MotionEvent.ACTION_MOVE:
		     // 。。。
                    break;
            }

            return true;
        }

        return false;

接着注释5-2…

第六,onClick 函数处理点击的逻辑

接着注释5-2:goto performClickInternal()

// 代码段6:
// View.java

    private boolean performClickInternal() {
        // Must notify autofill manager before performing the click actions to avoid scenarios where
        // the app has a click listener that changes the state of views the autofill service might
        // be interested on.
        notifyAutofillManagerOnClick();

        return performClick();// 接着这看
    }

goto performClick()

    public boolean performClick() {
        // We still need to call this method to handle the cases where performClick() was called
        // externally, instead of through performClickInternal()
        notifyAutofillManagerOnClick();

        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this); // 6-1,回调
            result = true;
        } else {
            result = false;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

        notifyEnterOrExitForAutoFillIfNeeded(true);

        return result;
    }

注释6-1处,执行了 mOnClickListener 的 onClick 函数,
就是通过 View 的函数 setOnClickListener 设置进来的,详情:

    public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }

再回看代码段2 的 注释2-6处...

第七,没有子 View 消费事件时,自身尝试消费

接着 注释2-6处:
goto dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS)

// 代码段7:
// ViewGroup.java

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

        // 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) {
                // 7-1
                // 子类不存在,调用父类 View(注意不是父视图)的分发逻辑,如代 码段5 一致
                // ViewGroup 继承 View 类,所以也具有父类的特性
                handled = super.dispatchTouchEvent(event);
            } 
            // 。。。

            event.setAction(oldAction);
            return handled;
        }

注释7-1处,因为 child == null,走的是父类的 super.dispatchTouchEvent,也就是 View#dispatchTouchEvent,而 dispatchTouchEvent 函数最终会走到 View#onTouchEvent函数。也就是说,如果没有子 View 消费事件,就让本 ViewGroup 来决定是否消费事件。

到此,基本流程就走完了。


整体流程图

我自己画的流程图有点丑,可以看考 该链接 的文章看里面的流程图,画得非常好,值得学习!


另一个流程图,引用自 该链接


小结

ViewGroup # dispatchTouchEvent 函数的步骤细节
  • 1⃣️ 重置

    • 如果是 down 事件,就走重置逻辑
    • 重置 目标子View,mFirstTouchTarget == null
    • 重置 不拦截flag,mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT
  • 2⃣️ 拦截判断,条件一:down 事件就拦截

    • disallowIntercept 重置为 false,走 4⃣️ intercepted = onInterceptTouchEvent(ev),默认返回 false
  • 3⃣️ 拦截判断,条件二:move/up事件,有目标子View,则拦截

    • disallowIntercept 在 down 事件时重置为 false,走 4⃣️ intercepted = onInterceptTouchEvent(ev),默认返回 false
    • 若上一个事件传到目标子View时,子View 调用getParent().requestDisallowInterceptTouchEvent(true),将 disallowIntercept置为 true,走 5⃣️ intercepted = false
  • 6⃣️ 分发

    • 如果 intercepted == false,即不拦截
    • 落点判断:若在落点内,则分发
    • 分发事件给子View,child.dispatchTouchEvent
    • 若返回 true,则将该 child 记录为 目标子View
  • 7⃣️ 尝试自消化

    • 没有目标子View,mFirstTouchTarget == null
    • dispatchTransformedTouchEvent(ev, canceled, null, … ) 第三个参数 child 为 null
      • if( child == null ) super.dispatchTouchEvent --> View#dispatchTouchEvent --> View#onTouchEvent
      • 即走 onTouchEvent 函数来确定自身是否消化这个事件
      • 如果自身不消化,return false 给父容器

一次事件序列在 ViewGroup#dispatchTouchEvent 函数的过程
事件步骤备注
down1⃣️ 2⃣️ 4⃣️ 6⃣️记录消费事件的目标子View
move3⃣️ 4⃣️ 6⃣️
3⃣️ 4⃣️ 6⃣️
move3⃣️ 4⃣️ 6⃣️目标子View中,请求不拦截
move3⃣️ 5⃣️ 6⃣️之后都不走步骤4⃣️,走步骤5⃣️ intercepted = false ,直接不拦截
3⃣️ 5⃣️ 6⃣️
up3⃣️ 5⃣️ 6⃣️

  • 若该 ViewGroup 没有子View,且被触摸点命中 ,则过程步骤如下:
事件步骤备注
down1⃣️ 2⃣️ 4⃣️ 6⃣️ 7⃣️没有目标子View,走 onTouchEvent 函数 ,若返回 false,不处理事件,则该ViewGroup自身不会被加入到父容器的 mFirstTouchTarget 中,后续的 move/up 事件,都会直接走7⃣️,返回 false,把事件返回父容器处理
move7⃣️
7⃣️
up7⃣️
View # dispatchTouchEvent 函数的大致步骤
  • onTouch 函数处理

    • 返回 true,函数结束
  • onTouchEvent 函数处理

    • 设置了点击或长按的监听,处理事件并返回 true,函数结束
      • case MotionEvent.ACTION_UP:performClickInternal
      • case MotionEvent.ACTION_DOWN:checkForLongClick – CheckForLongPress#run – performLongClick
      • 返回 true

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值