View 事件分发机制

Android事件分发机制完全解析,带你从源码的角度彻底理解(上)

Android事件分发机制完全解析,带你从源码的角度彻底理解(下)

Android ViewGroup和View的传递事件

事件分发之ViewGroup.dispatchTouchEvent(Android5.0

我承认看了好几天代码了但是还是很懵。但是大概了解到的是,把ViewGroup的 dispatchTouchEvent() 搞清楚。再把View自带的dispatchTouchEvent()搞清楚。这两点是相当重要的。因为他们可以把View整个传递机制的特点展现出来。

憋了一天大约看出来个门道了,事件传递的特点是沉下去再浮上来。也就是,view 大致可分为 容器类view, 和 非容器类的view。在硬件上的用户事件接收上来的时候,就进行分发,,这段分发过程暂时不要去考究。但最终一趟下来就分发到咱们Activity当前的顶级view-- DecorView的头上来了。调用了DecorView的dispatchTouchEvent(), 别忘了DecorView可是ViewGroup的孙子, 他好像也跟就没重写这个方法。于是也就等于调用了ViewGroup的这个方法。。打断,,就是这个dispatchTouchView,是广义上的事件传递的开始!!

其实整个事件传递机制,就是靠着 dispatchTouchevent(),onTouchEvent() 和 onInterceptTouchEvent()这三个方法撑起来的。但是但是但是重要的事情要搞清楚,,就是dispatchTouchEvent()这个方法,View写了一个,View的子类ViewGroup又重写了。而且和View里的那个逻辑有大大滴不同。(其实想想也明白,dispatchTouchEvent()名字太直白了,就是分发事件,但是ViewGroup相比于View的不同就是它可以有子View,而View是不可以有子view的。这个本质的不同直接决定了你怎么分发。毕竟ViewGroup的那一堆子view怎么着也得分发下去吧。但是View就简单直接了,只顾着自己消费就得了呗。)对于另一个方法onTouchEvent(),只有View里面实现了,ViewGroup里面没实现。 还有就是 onInterceptTouchEvent()这个是ViewGroup独有的方法,目的就是搞拦截事件用的。。

好,那么看下关系哈:

(1)View里,有两个回调函数 :

public boolean dispatchTouchEvent(MotionEvent ev);  
public boolean onTouchEvent(MotionEvent ev); 
  • 1
  • 2

(2)ViewGroup里,有三个回调函数 :

public boolean dispatchTouchEvent(MotionEvent ev);  
public boolean onInterceptTouchEvent(MotionEvent ev);  
public boolean onTouchEvent(MotionEvent ev);

补充:

在Android中,事件主要包括点按、长按、拖拽、滑动等,点按又包括单击和双击,另外还包括单指操作和多指操作。所有这些都构成了Android中得事件响应。总的来说,所有的事件都由如下三个部分作为基础: 
按下(ACTION_DOWN) 
移动(ACTION_MOVE) 
抬起(ACTION_UP)

好,,,接着上面的说,我们不是谈到,事件分发,从硬件层传到软件层,再经过一堆逻辑走啊走,就走到DecorView的 diaptchTouchEvent()里面了么,,这个decorView,就是ViewGroup的孙子。最终该是调用的ViewGroup的这个方法。所以呢,先看ViewGroup是怎么分发的。解释全部写在代码里面了。

/**
 * {@inheritDoc}
 */
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
    }

    // If the event targets the accessibility focused view and this is it, start
    // normal event dispatch. Maybe a descendant is what will handle the click.
    if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
        ev.setTargetAccessibilityFocus(false);
    }

    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();
        }

        // Check for interception.
        final boolean intercepted;
        //当前的事件是down事件,并且 已经找到了能够接受touch事件的view
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            //当其他地方调用了,requestDisallowInterceotTouchEvent()时的时候,这句话的结果将是true。即禁止拦截
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            //当可以拦截的时候。
            if (!disallowIntercept) {
                //那就调用onInterceptTouchEvent(),这是android源代码唯一调用这个方法的地方
                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.
            //当当前的时间不是down的时候,或者是mFirstTouchTargetnull的时候。 拦截掉!直接设置为true            intercepted = true;
        }

        // If intercepted, start normal event dispatch. Also if there is already
        // a view that is handling the gesture, do normal event dispatch.
        if (intercepted || mFirstTouchTarget != null) {
            ev.setTargetAccessibilityFocus(false);
        }

        // Check for cancelation.
        //cancle事件进行检查
        final boolean canceled = resetCancelNextUpFlag(this)
                || actionMasked == MotionEvent.ACTION_CANCEL;

        // Update list of touch targets for pointer down, if needed.
        final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
        //这个很有意思,如果不是cancle消息,并且,没有拦截住。
        //咱们上面interceptedfalse的时候,才能过这条。那么除非是程序改变flag,导致强制不拦截。
        //或者是,当前可以拦截,但是调用 onInterceptTouchEvent 返回了false。那这种意思不就是不去拦截呗,相当于没拦截住。
        //好吧,那么 down up 的时候都有可能到这一步。
        //那么if里面究竟是干嘛的呢?
        if (!canceled && !intercepted) {

            // If the event is targeting accessiiblity focus we give it to the
            // view that has accessibility focus and if it does not handle it
            // we clear the flag and dispatch the event to all children as usual.
            // We are looking up the accessibility focused host to avoid keeping
            // state since these events are very rare.
            //找到有焦点的view的顶级父容器
            View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                    ? findChildWithAccessibilityFocus() : null;
            //down 或者 move事件
            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 按下操作总是返回0
                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) {
                    //根据touch坐标的位置,寻找子view来接收touch事件
                    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 = buildOrderedChildList();
                    final boolean customOrder = preorderedList == null
                            && isChildrenDrawingOrderEnabled();
                    final View[] children = mChildren;
                    //遍历子view判断哪个子view能够接收touch事件
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        final int childIndex = customOrder
                                ? getChildDrawingOrder(childrenCount, i) : i;
                        final View child = (preorderedList == null)
                                ? children[childIndex] : preorderedList.get(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;
                        }

                        //判断这个子view是不是能接收touch事件,或者,点击的位置是不是在这个子view里面
                        //不是的话,设置为false,并且continue继续循环
                        if (!canViewReceivePointerEvents(child)
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            ev.setTargetAccessibilityFocus(false);
                            continue;
                        }
                        //那么依照上面的逻辑,这样的走法儿,到这里的话,当前的child肯定是一个可以接收当前touch事件的view
                        newTouchTarget = getTouchTarget(child);
                        if (newTouchTarget != null) {
                            //如果这里,不是null,那么就代表newTouchTarget一定是可以接收touch事件的子View了。
                            //这样的话,就没必要再遍历了,直接break                            // 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;
                        }
                        //但是如果 newTouchTarget null 的话,就会走到这一句。
                        //那代表的究竟是哪种情况呢?就是用户在屏幕上点击了一下,
                        resetCancelNextUpFlag(child);
                        //dispatchTransformedTouchEvent导致其内部会不断的dispatchTouchEvent (),从而形成一个递归式遍历。、
                        //当这个方法吐出true的时候,也就代表该事件被消耗掉了。当然当前这个时间有可能是down,有可能是up                        //但是绝对不可能是cancle事件
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            // Child wants to receive touch within its bounds.
                            //能走到这里的话,说明这个child里面的事件已经被消耗掉了。
                            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();
                            //调用addTouchTarget()child添加到 mFirstTouchTarget的链表头部

                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            alreadyDispatchedToNewTouchTarget = true;
                            //那么到了这里,代表事件已经被消耗完了,并且自己还做了个记录。改了一下相关数据
                            //跳出for循环。因为已经处理完了
                            break;
                        }

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

                    //出了for循环了,
                    if (preorderedList != null) preorderedList.clear();
                }

                //如果到了这里程序还继续走着,那只能说明,整个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;
                }
            }
        } //由此可见这整个大判断里面,基本就是对 down事件,move事件进行处理。

        //1 当当前view是拦截状态(要么不是down事件,再或者回调拦截返回了false)的时候,或者是当前事件是cancle的时候。会跳过前面的一大坨
        //逻辑直接走到这一步
        //2 当前view是不可拦截状态,那么就得走完了前面的一大坨,走到了这一步
        // == null 的时候,代表的是 压根没有消费touch事件的子组件,或者touch事件被拦截了
        // != null 的时候,则代表找到了能够消费touch事件的子组件,那么后续的touch事件便可以传递到子view        // Dispatch to touch targets.
        if (mFirstTouchTarget == null) {
            // No touch targets so treat this as an ordinary view.
            //mFirstTouchTargetnull            //的原因,没有找到消费touch事件的子组件或者touch事件被拦截掉了。
            //稍后会仔细看这个方法。。记得上面有代码也调用到了这个方法,但是不同的是参数之间的
            //不同,此处的第三个参数直接给了一个null
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            //mFirstTouchTarget != null
            //即找到了可以消费的touch事件的子view,并且后续的touch事件可以传到该子view            // 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;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    //如果前面利用ACTION——DOWN事件寻找接收条件的子view同时消耗了该事件。
                    //那么 handled赋值为true                    handled = true;
                } else {
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                    //如果前面没有分发该事件的话,那就分发,很明白了,要么是cancle,要么是move,要么是down
                    //因为前面有一波判断正好把他们过滤掉了,但是依逻辑来看,就是这类事件,可以走到这里。
                    //那么就专门递归,分发这类的事件。
                    //但是要注意欧,,递归下来依然是 被消费了才能走到里面的handles = true                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    if (cancelChild) {
                        if (predecessor == null) {
                            mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                    }
                }
                predecessor = target;
                target = next;
            }
        }
        //对于up  move 事件来说,该处理完的也处理了,那么的话,就更改一下各种状态。
        //主要的是还原状态。
        // Update list of touch targets for pointer up or cancel, if needed.
        if (canceled
                || actionMasked == MotionEvent.ACTION_UP
                || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
            resetTouchState();
        } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
            final int actionIndex = ev.getActionIndex();
            final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
            removePointersFromTouchTargets(idBitsToRemove);
        }
    }

    if (!handled && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
    }
    return handled;
}

这一段方法大致可以分为几个阶段:

1 来的是down吗,是的话,并且不能拦截,那就分发事件,递归遍历,尽量消费。2 走也走完了,那就看看其他的情况处理,是在没有处理,大不了让当前ViewGroup自行消耗,尽力吧。 3 还有一波up,move事件呢,这个没有拦截之说,也没有那么多的限制, 那么就递归分发。。

好,,对于第二条这点主要体现在一个方法上,这个方法很好的解释了事件传递沉下去又浮上来这种现象。

/**
 * Transforms a motion event into the coordinate space of a particular child view,
 * filters out irrelevant pointer ids, and overrides its action if necessary.
 * If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
 */
//dispatchTouchEvent里面总共有散出调用了该方法。这是一个很重要的代码。
//View完成递归分发的关键。
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);
        //这里有个很重要的的判断 child == null ? 这里可以看调用它的地方,三处有一处
        //赤裸裸的给第三个参数设置成了一个true。那接下来的代码寓意何为呢?
        //所以只要看这三处调用,就知道child == null 到底代表的是什么意思了
        //接下来一个个分析三处调用
        //1处, 在处理down事件的时候,遍历子ViewGroup找到自己可接受touch事件子view的时候调用了一次,此时第三个参数就是child
        //2处, 在没有发现自己有可以消耗的view的时候,或者直接被拦截的时候,调用了一次,但是第三个参数就是 null
        //3处, 在处理除了down事件的其他事件,比如up啥的,调用了一次,此时传入的第三个参数是child
        //这一系列的逻辑就直接说明了一个onTouch事件的传递特点,就是子view无法消费的情况下,那么我就调用
        //一下自己父类的diaspatchTouchEvent()尝试着自己消费一下。实在这样还是消费不了的话,那咱就返回false呗。
        //也就是这样,,就形成了一个冒泡式传递的样子,沉下去再浮上来。如  1,2,3,4,3,2,1这个样子。
        if (child == null) {
            // 这一句很明白了,调用viewGroup 父类的 dispatchTouchEvent(),那不就是View
            // 的这个方法么!!
            handled = super.dispatchTouchEvent(event);
        } else {
            // 此处递归的关键
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }

    // Calculate the number of pointers to deliver.
    final int oldPointerIdBits = event.getPointerIdBits();
    final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;

    // If for some reason we ended up in an inconsistent state where it looks like we
    // might produce a motion event with no pointers in it, then drop the event.
    if (newPointerIdBits == 0) {
        return false;
    }

    // If the number of pointers is the same and we don't need to perform any fancy
    // irreversible transformations, then we can reuse the motion event for this
    // dispatch as long as we are careful to revert any changes we make.
    // Otherwise we need to make a copy.
    final MotionEvent transformedEvent;
    if (newPointerIdBits == oldPointerIdBits) {
        if (child == null || child.hasIdentityMatrix()) {
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                final float offsetX = mScrollX - child.mLeft;
                final float offsetY = mScrollY - child.mTop;
                event.offsetLocation(offsetX, offsetY);

                handled = child.dispatchTouchEvent(event);

                event.offsetLocation(-offsetX, -offsetY);
            }
            return handled;
        }
        transformedEvent = MotionEvent.obtain(event);
    } else {
        transformedEvent = event.split(newPointerIdBits);
    }

    // Perform any necessary transformations and dispatch.
    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        final float offsetX = mScrollX - child.mLeft;
        final float offsetY = mScrollY - child.mTop;
        transformedEvent.offsetLocation(offsetX, offsetY);
        if (! child.hasIdentityMatrix()) {
            transformedEvent.transform(child.getInverseMatrix());
        }

        handled = child.dispatchTouchEvent(transformedEvent);
    }

    // Done.
    transformedEvent.recycle();
    return handled;
}


再看看传说中View的dispatchTouchEvent方法的实现,,毕竟这个才是终极调用的。

/**
 * Pass the touch screen motion event down to the target view, or this
 * view if it is the target.
 *
 * @param event The motion event to be dispatched.
 * @return True if the event was handled by the view, false otherwise.
 */
public boolean dispatchTouchEvent(MotionEvent event) {
    // If the event should be handled by accessibility focus first.
    if (event.isTargetAccessibilityFocus()) {
        // We don't have focus or no virtual descendant has it, do not handle the event.
        if (!isAccessibilityFocusedViewOrHost()) {
            return false;
        }
        // We have focus and got the event, then use normal event dispatch.
        event.setTargetAccessibilityFocus(false);
    }

    boolean result = false;

    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onTouchEvent(event, 0);
    }

    final int actionMasked = event.getActionMasked();
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        // Defensive cleanup for new gesture
        stopNestedScroll();
    }

    if (onFilterTouchEventForSecurity(event)) {
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        //如果onTouchListener没有设置的话,这条直接过
        //这里面以及以下的代码,起码可以说明对于 单个的非容器类view而言,如果ontouch()把
        //事件消耗掉了,是不会走到 onTouchEvent()这个方法里的,那么这个方法里相关的
        //onclick()回调当然也就不用走了。
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }

        //如果ontouch的回调没有消费掉这个事件,那么就执行onTouchEvent()方法
        //事实上onTouchEvent()大部分情况下返回为true
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }

    //如果到了现在还是没有消费掉的话
    if (!result && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
    }

    // Clean up after nested scrolls if this is the end of a gesture;
    // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
    // of the gesture.
    if (actionMasked == MotionEvent.ACTION_UP ||
            actionMasked == MotionEvent.ACTION_CANCEL ||
            (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
        stopNestedScroll();
    }

    return result;
}

发现View里面写的倒是很简单。就是先执行自己的onTouch 回调,如果无法消耗,那就走自己的onTouchEvent()方法。注意了啊,这个onTouchEvent里面大部分其实会返回true,并且里面有个十分炸天的逻辑,直接导致了咱们平常写的onClickListerner的 onClick()方法的回调!!

但是仔细看这个代码,你会发现,,如果你的onTouch()回调返回了个true的话,会导致咱们的onTouchEvent()方法无法执行。那么咱们平常写的 什么onCkickListerner 那里面的逻辑自然也不会执行了。

关于事件传递,看起来花里胡哨的,但是沉下心来好好看代码,还是勉强能看出网上帖子上写的那种过程的。

下面是不是好奇 decorView 的 dispatchTouchEvent 前面都经历了啥东东?其实很简单。。我贴几个代码就知道了。首先事件其实是传递给Activity了,因为Acitivity 实现了一个专门的事件接收 reciver,并且还把自己传递给了wms系统里面,所以当来了事件,自然它的相关方法就会被回调到了。

/**
 * Called to process touch screen events.  You can override this to
 * intercept all touch screen events before they are dispatched to the
 * window.  Be sure to call this implementation for touch screen events
 * that should be handled normally.
 *
 * @param ev The touch screen event.
 *
 * @return boolean Return true if this event was consumed.
 */
//activity类中的。。
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    //首先分发一下,,如果该事件没有得到处理的话
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    //那就自己消耗试试
    return onTouchEvent(ev);
}
public boolean superDispatchTouchEvent(MotionEvent event) {//phoneWindow中的方法
    return this.mDecor.superDispatchTouchEvent(event);
}

看出来了吧,,decorView。。如果还要追怎么调用到Activity中的,建议大家找源码看看。。反正最重要的流程讲完了。


















猜你喜欢

转载自blog.csdn.net/weixin_28774815/article/details/80723663
今日推荐