Android View的事件分发

View的事件分发所针对的是MotionEvent事件,在Touch过程中会产生大量的MotionEvent,记录了与Touch相关的事件。一次ACTION_DOWN、中间可能多次ACTION_MOVE、一次ACTION_UP,这便是一次完整的MotionEvent事件。在我们点击屏幕的那一刻,会先经过硬件的一系列处理,然后在当前应用的主(UI)线程中接收到来自底层传输过来的input事件,将事件交付于ViewRootImpl的enqueueInputEvent()方法,通过ViewRootImpl的内部类InputStage转换处理,最终交给View的dispatchTouchEvent()方法,事件分发的开始。 一个应用程序的根视图(顶级View)是DecorView,也就是说,View的事件分发实际是由DecorView中的dispatchTouchEvent()方法开始的。

在处理View的事件分发时,View和ViewGroup(继承自View)稍有差异。

  • View的相关处理方法: dispatchTouchEvent()、onTouchEvent()
  • ViewGroup的相关处理方法: dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent()

1、DecorView # dispatchTouchEvent()

public boolean dispatchTouchEvent(MotionEvent ev) {
        final Window.Callback cb = mWindow.getCallback();
        return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
                ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
    }
复制代码

上面代码中,mWindow是一个PhoneWindow类型的变量,在Activity的attach()方法中对mWindow进行赋值【可以参考这篇文章 — Activity的启动过程】。且Activity实现了Window.Callback接口**(实现Window.Callback的不只有Activity,比如Dialog,这里以Activity为例)**,也就是说:cb.dispatchTouchEvent()回调的是Activity中的dispatchTouchEvent(),执行这一步首先要满足前提条cb是否存在,PhoneWindow没有销毁,mFeatureId < 0表示DecorView存在。否则会执行ViewGroup的dispatchTouchEvent()方法(DecorView继承FrameLayout)。

2、Activity # dispatchTouchEvent()

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            //内部是一个空实现,用于点击时与用户交互
            onUserInteraction();
        }
        //getWindow()返回PhoneWindow对象
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        //若前面没有消费事件,最后交给Activity的onTouchEvent
        return onTouchEvent(ev);
    }
复制代码

这里的实现很简洁,先交给PhoneWindow分发,如果没有消费事件,则在Activity的onTouchEvent()方法中消费。

3、PhoneWindow # superDispatchTouchEvent()

 public boolean superDispatchTouchEvent(MotionEvent event) {
        //mDecor指的是DecorView
        return mDecor.superDispatchTouchEvent(event);
    }
复制代码

进入DecorView中的superDispatchTouchEvent()

public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }
复制代码

前面已注明了DecorView是继承自FrameLayout(继承ViewGroup),那么这里也就自然而然的把分发任务交给了ViewGroup的dispatchTouchEvent()。

4、ViewGroup # dispatchTouchEvent(),由于代码量太长,这里选择关键性的代码

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

        // 如果是点击事件,先对之前的状态进行清除
        //1、把mFirstTouchTarget(TouchTarget对象)链表清空,同时mFirstTouchTarget置空
        //mFirstTouchTarget链表内部存放的是接受了触摸事件的view
        //2、重置FLAG_DISALLOW_INTERCEPT标识位(若设置了这个标识,表示禁止ViewGroup的拦截)
        //可以通过requestDisallowInterceptTouchEvent()进行设置,
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            cancelAndClearTouchTargets(ev);
            resetTouchState();
        }

        // 检查是否进行拦截
        final boolean intercepted;

        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {

            // 检查是否已设置FLAG_DISALLOW_INTERCEPT标识
            // 若设置,则不进行拦截,intercepted为false
            // 否则拦截,由onInterceptTouchEvent()决定intercepted的状态
            // onInterceptTouchEvent()默认返回false
            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;
        }

    ... ...

        // 检查当前触摸事件是否被取消
        final boolean canceled = resetCancelNextUpFlag(this)
                || actionMasked == MotionEvent.ACTION_CANCEL;

        //把当前ViewGrop的触摸事件进行分发给子View和子ViewGroup
        //在这之前需要满足两个条件:1、触摸事件没有取消  2、没有被拦截
        //如果当前ViewGroup的子View接收到触摸事件,则把该子View添加到mFirstTouchTarget链表
        final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
        if (!canceled && !intercepted) {

            View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                    ? findChildWithAccessibilityFocus() : null;

            if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {

                //记录触摸事件的序列号,事件Id,点击事件的Id总是为0,
                //在多指触控下,会产生多个Id,比如第一根手指,记录0,第二根手机记录1 ~ ~ ~
                final int actionIndex = ev.getActionIndex();
                final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                        : TouchTarget.ALL_POINTER_IDS;

                // 清除早期的触摸目标                 
                removePointersFromTouchTargets(idBitsToAssign);

                // 获取当前ViewGroup包含的子元素,进行遍历,然后对触摸事件进行分发
                // 如果子元素也是ViewGroup,那么就对其里面的子元素遍历,如此递归下去
                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;
                        }

                        //判断子View是否能接收触摸事件
                        if (!canViewReceivePointerEvents(child)
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            ev.setTargetAccessibilityFocus(false);
                            continue;
                        }

                        // 先查找mFirstTouchTarget链表,是否存在该View
                        // 若查找到,则返回当前View在链表中的节点赋值给newTouchTarget
                        // 若没有,则返回null
                        newTouchTarget = getTouchTarget(child);
                        if (newTouchTarget != null) {
                            newTouchTarget.pointerIdBits |= idBitsToAssign;
                            break;
                        }

                        //重置子View的mPrivateFlags中的PFLAG_CANCEL_NEXT_UP_EVENT
                        resetCancelNextUpFlag(child);

                        //将触摸事件分发给子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;
                            }

                            //若子元素(包含子View和子ViewGroup)可以接收到触摸事件,
                            // 通过addTouchTarget()把已接收触摸事件的子元素添加到
                            //mFirstTouchTarget链表,并把当前子元素作为头结点返回,赋值给newTouchTarget
                            mLastTouchDownX = ev.getX();
                            mLastTouchDownY = ev.getY();
                            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();
                }

                //在newTouchTarget为null,mFirstTouchTarget不为null时,
                //把mFirstTouchTarget赋值给newTouchTarget,作为链表第一个节点
                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;
                }
            }
        }

... ...

        // 进一步对触摸事件的分发进行处理,mFirstTouchTarget==null表示未有
        // 子View接收到触摸事件,此时,会交由ViewGroup的父类View的dispatchTouchEvent()进行分发,
        //然后再交给onTouch()或onTouchEvent()进行处理
        //onTouch()优先于onTouchEvent(),但是要setOnTouchListener()后才生效
        // 如果mFirstTouchTarget != null,表示存在子View,则分发到子View
        if (mFirstTouchTarget == null) {        
            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;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                    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;
            }
        }

        // 完成后重置触摸状态
        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;
 }
复制代码

关于ViewGroup的事件分发,由于代码太长,所以直接在里面注释了,这样跟着代码会比较容易理解。上面阐述了关于ViewGroup的整个分发过程,在第二个省略号的下面一段代码,从其 if 条件可以知道,这段代码只有在ViewGroup发生ACTION_DOWN事件时才会执行(分发事件),而后续的事件(ACTION_MOVE、ACTION_UP)将由第三个省略号下面一段代码中执行分发,这时会遍历mFirstTouchTarget链表,找到具体的子View进行分配事件。(实际就是:假设当前ViewGroup中包含三个子View,分别是A B C,如果ACTION_DOWN事件分发到了A,那么后续的事件一定不会分发到B或C) 下面进一步去分析dispatchTransformedTouchEvent()

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

        //这里检测是否需要发送ACTION_CANCEL事件。
        final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {    
                //分发给当前ViewGroup
                handled = super.dispatchTouchEvent(event);
            } else {   
               //分发给子View
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }

        // 计算触摸事件Id
        final int oldPointerIdBits = event.getPointerIdBits();
        final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;

        if (newPointerIdBits == 0) {
            return false;
        }
      
        final MotionEvent transformedEvent;
        //若触摸事件Id相同,不需要重新计算MotionEvent,直接进行分发
        if (newPointerIdBits == oldPointerIdBits) {
            if (child == null || child.hasIdentityMatrix()) {
                //child为null,交由父类分发,否则交由child分发
                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;
    }
复制代码

在dispatchTransformedTouchEvent()中主要是对子View进行判断,如果子View为null,则分发交由super.dispatchTouchEvent(),也就是由View的dispatchTouchEvent()分发,然后交给onTouch()(如果设置了OnTouchListener)或onTouchEvent(),如果这时候onTouchEvent()依旧返回false,则交由当前ViewGroup的上一级去处理。

5、View # dispatchTouchEvent(),省略了部分代码

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

        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // 接收到ACTION_DOWN,停止嵌套滑动
            stopNestedScroll();
        }
        
         //判断View是否被屏蔽,是否能被点击,
        //然后再确定是否执行onTouch或onTouchEvent
        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }

            //在这里注意下li.mOnTouchListener.onTouch(),这里说明了
            //onTouch会先于onTouchEvent执行,前提:当前View设置了OnTouchListener
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            
            //若没有设置OnTouchListener,交给onTouchEvent
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

    ... ...

        return result;
    }
复制代码

关键的步骤已在代码中注释,就不再另外赘述了。 接着看下View的onTouchEvent()方法。

public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();
        
        //计算View是否可点击
        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
        
        //这里View被禁用,调用了setEnabled(false)或android:enabled=false
        //返回其点击状态,View默认是不可点击的
        //可以通过setClickable()或者android:clickable设置点击状态
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;        
            return clickable;
        }

        //委托事件给其它View,mTouchDelegate默认为null
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

        //判断View的点击状态,设置焦点。后续执行onClick()、onLongClick()
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    if ((viewFlags & TOOLTIP) == TOOLTIP) {
                        handleTooltipUp();
                    }
                    if (!clickable) {
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        break;
                    }
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed.  Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            setPressed(true, x, y);
                        }

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

                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }

                        if (prepressed) {
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            // If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }

                        removeTapCallback();
                    }
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_DOWN:
                    if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                        mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                    }
                    mHasPerformedLongPress = false;

                    if (!clickable) {
                        checkForLongClick(0, x, y);
                        break;
                    }

                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }

                    // Walk up the hierarchy to determine if we're inside a scrolling container.
                    boolean isInScrollingContainer = isInScrollingContainer();

                    // For views inside a scrolling container, delay the pressed feedback for
                    // a short period in case this is a scroll.
                    if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        // Not inside a scrolling container, so show the feedback right away
                        setPressed(true, x, y);
                        checkForLongClick(0, x, y);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    if (clickable) {
                        setPressed(false);
                    }
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    break;

                case MotionEvent.ACTION_MOVE:
                    if (clickable) {
                        drawableHotspotChanged(x, y);
                    }

                    // Be lenient about moving outside of buttons
                    if (!pointInView(x, y, mTouchSlop)) {
                        // Outside button
                        // Remove any future long press/tap checks
                        removeTapCallback();
                        removeLongPressCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            setPressed(false);
                        }
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    }
                    break;
            }

            return true;
        }

        return false;
    }
复制代码

关于View的事件分发大概流程到这里就结束了。 Activity > ViewGroup(可包含多个ViewGroup) > View 。 最后通过一张图的形式整理下上面的思路:

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值