android ViewGroup点击事件分发机制

android ViewGroup点击事件分发机制

前言

好久没有写博客了,今天在看书的同时我想把我所学的用写博客的形式记录下来。这样又便于日后查看,并且还能在写博客的同时加深自己的印象。上一篇博客我们主要介绍的是view的事件分发,这一篇文章我们主要介绍一些viewgroup的事件分发。

源码分析

我们知道当一个点击事件发生后它的传递过程遵循如下的顺序:activity-> window->view。即事件总是先传递给activity,activity传递给window,最后window传递给顶级view。低级view接收到事件后,就会按照事件分发机制去分发事件。

上面我们已经说了最先收到点击事件的是activity所以我们首先看一下activity是怎么传的点击事件。我们先从activity dispatchTouchEvent开始分析

 public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

首先事件开始交给activity所属的window进行事件分发,如果返回true,那么整个事件分发就结束了,若返回false意味着没有人处理事件,所有view的onTouchEvent都返回false,那么activity的onTouchEvent就会被调用。

接下来我们看一下window是怎么将事件传递给viewgroup的。通过源码我们知道window是一个抽象类,而window的 superDispatchTouchEvent(MotionEvent event);方法也是个抽象方法,因此我们必须找到window的真正实现类才行。

    public abstract boolean superDispatchTouchEvent(MotionEvent event);

其实window的真正的实现类其实就是PhoneWindow,这一点从源码中就可以看出,在window的说明中有一段这样的话:

/**
 * ....
 * <p>The only existing implementation of this abstract class is
 * android.view.PhoneWindow, which you should instantiate when needing a
 * Window.
 */

从注释就可以看出phonewindow是window的唯一实现类接下来我们就看一PhoneWindow是怎么处理的点击事件的。

   @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

通过上面的源码可以看出PhoneWindow直接将事件传递给了DecorView那么这个DecorView到底是什么呢?其实DecorView就是window的顶级View我们通过setContentView设置的view就是它的子view。到这里事件已经被传递到我们的顶级View中,而我们的DecorView实际上继承的FrameLayout,所以这时候我们就应该关心一下ViewGroup的事件传递了。

接下来我们看一下viewgroup的事件传递。首先会调用ViewGroup的dispatchTouchEvent方法,然后的逻辑是这样的:如果顶级viewGroup拦截了事件即onInterceptTouchEvent返回true,则事件有ViewGroup自己处理,这是后如果这是了触摸事件即通过setOnTouchEvent的事件的mOnTouchListener,则onTouch就会被调用,否则onTouchEvent会被调用。也就是说ontouch事件会屏蔽掉onTouchEvent事件。如果在onTouchEvent中设置了onClickListener,则onClick就会被调用,如果设置了长按事件则onLongClick就会被调用。如果ViewGroup不拦截事件,则事件会传递给它所在的点击事件链上的子view,这是View的dispatchTouchEvent就会被调用。到这时就会事件从我们的顶级view传递给了下一级view这时候走的就是view的事件传递流程了,如果不清楚view的事件分发给可以移步到我的这一篇博客:https://blog.csdn.net/guojingbu/article/details/82598022

首先我们看一下viewGroup对点击事件的分发过程,其主要实现在ViewGroup的dispatchTouchEvent方法中,由于这个方法的代码比较长,我们分段说一下。首先看一下下面这一段代码:

            // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    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.
                intercepted = true;
            }

上面摘取的这段代码描述的是当前view是否拦截点击事件的逻辑。从上面的代码可以看出,ViewGroup在如下两种情况判断是否拦截当前事件:
事件类型为ACTION_DOWN或者mFirstTouchTarget!=null。ACTION_DOWN事件到是好理解,那么mFirstTouchTarget!=null是什么意思呢?这个从后面的逻辑可以看出,当事件由ViewGroup的子元素成功处理时,mFirstTouchTarget会被赋值并指向子元素,当ViewGroup不拦截事件交由子view处理时则:mFirstTouchTarget!=null。一旦当前ViewGroup拦截了事件则:mFirstTouchTarget!=null就不成立了。那么当ACTION_MOVE和ACTION_UP事件来到时,由于(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) 这个条件为false,将导致ViewGroup的onInterceptTouchEvent不会再调用,并且同一序列中其他事件都会交由它去处理。

这里有一种特殊情况,那就是FLAG_DISALLOW_INTERCEPT这个标记位,这个标记位是通过requestDisallowInterceptTouchEvent 方法来设置的,这个方法一般用于子view中。FLAG_DISALLOW_INTERCEPT这个标记一旦设置后,ViewGroup将无法拦截除ACTION_DOWN以外的其他点击事件。为什么说处理ACTION_DOWN事件呢?是因为在ViewGroup事件分发时,古国是ACTION_DOWN就会重置FLAG_DISALLOW_INTERCEPT这个标记,将导致子View设置的这个标记无效。因此当面对ACTION_DOWN事件时,ViewGroup总是会调用自己的onInterceptTouchEvent方法来询问自己是否拦截事件,这一点从源码中可以看出。在下面的代码中,ViewGroup会在ACTION_DOWN事件到来时做重置状态的操作,而在resetTouchState方法中会对FLAG_DISALLOW_INTERCEPT进行重置,因此子View中调用并不能影响ViewGroup对ACTION_DOWN事件的处理。

 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) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }
            

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

接下来我们再看一下当ViewGroup不拦截事件的时候,事件会向下分发交由它的子view进行处理这一段代码如下:

 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;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);

                            // If there is a view that has accessibility focus we want it
                            // to get the event first and if not handled we will perform a
                            // normal dispatch. We may do a double iteration but this is
                            // safer given the timeframe.
                            if (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }
							//1、如果这个view不是visible的并且没有动画播放 那么这个view 就接收到点击事件。2、如果点击的坐标不在子view区域内那么这个子view也接收不到点击事件
                            if (!canViewReceivePointerEvents(child)
                                    || ! ocd(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }

                            newTouchTarget = getTouchTarget(child);
                            //如果有子View处理即newTouchTarget 不为null则跳出循环。
                            if (newTouchTarget != null) {
                                // Child is already receiving touch within its bounds.
                                // Give it the new pointer in addition to the ones it is handling.
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            resetCancelNextUpFlag(child);
                            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();
                                //如果子view处理了点击事件那么通过调用addTouchTarget 方法mFirstTouchTarget就会被赋值为该子view
                                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();
                    }

从上面的代码可以看出,首先viewgroup会遍历所有的子view,然后来判断子view是否能接收到点击事件。衡量点击事件是否接收到主要有两个点:一是子view是否在播放动画,二是点击的坐标是否落在子view的区域内。如果子view满足这两个条件事件就会传递给该子view去处理。可以看到dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)实际上调用的是子view的dispatchTouchEvent(event)方法,在它的内部有如下一段内容,而再上面的代码中child传递的不是null,因此它会直接调用子view的dispatchTouchEvent(event),这样事件就传递给子view处理了。

 if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }

如果子元素的dispatchTouchEvent(event)返回true,那么mFirstTouchTarget就会被赋值并调出for循环如下代码所示:

 newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;

如果子元素的dispatchTouchEvent(event)返回的false那么Viewgroup就会继续遍历把事件分发给下一个子元素(如果有下一个的话)。
其实mFirstTouchTarget的赋值在addTouchTarget方法的内部。从addTouchTarget方法内部的代码可以看出其实mFirstTouchTarget是一种单链表的结构。mFirstTouchTarget的是否为空会直接影响viewgroup对事件的处理策略。如果mFirstTouchTarget==null的话,那么Viewgroup就会拦截统一事件序列中的所有事件。

    /**
     * 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;
    }

如果遍历了所有的子元素后,事件都没有被合适的处理,这是后包含两种情况一种是Viewgroup没有子view;第二种是子元素处理了点击事件,但是在dispatchTouchEvent方法中返回了false这个一般是子元素的onTouchEvent方法返回了false。在这两种情况viewgroup会自己处理点击事件。

        // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
                        ..............

那么mFirstTouchTarget==null ,这时就会调用dispatchTransformedTouchEvent 注意这里的child参数为null,从前面的分析可知道,它会调用super.dispatchTouchEvent(event),由于viewgroup是view的子类所以从这开始点击事件就会交由View来处理。请移步到我的另一篇博客《android View的事件分发》https://blog.csdn.net/guojingbu/article/details/82598022

总结结论

(1)、当ViewGroup决定拦截事件后,那么后续的点击事件将默认给它处理不在调用onInterceptTouchEvent方法,
(2)、通过requestDisallowInterceptTouchEvent 方法可以在子view中干预父View的事件分发过程。但是ACTION_DOWN事件除外。FLAG_DISALLOW_INTERCEPT这个标记的作用是让ViewGroup不在拦截事件,这里的前提是ViewGroup不拦截ACTION_DOWN事件。这一条结论对我们有什么好处呢?其实FLAG_DISALLOW_INTERCEPT标记位的作用给我们提供了一个解决滑动冲突的思路。
(3)、ViewGroup在决定不拦截事件后就会遍历递归所有的子元素找到可以处理点击事件的子元素。如果找到那么就会跳出循环以后的同一事件序列中的所有事件都会交个这个子元素去处理。如果找不到可以处理事件的子元素或者ViewGroup没有子元素那么就由ViewGroup自己处理(这里的处理其实是调用VIew的dispatchTouchEvent)。
(4)、ViewGroup默认是不拦截任何事件的。这一点可以从android源码中ViewGroup的 public boolean onInterceptTouchEvent(MotionEvent ev)中看到它默认返回的是false。

以上就是我对ViewGroup源码的分析,有不对的地方还请大家多多指正谢谢!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值