美文网首页
谈一谈安卓的事件分发

谈一谈安卓的事件分发

作者: 零星瓢虫 | 来源:发表于2020-04-28 00:58 被阅读0次

    事件分发的规则

    安卓中的手势动作,都会产生MotionEvent对象;所谓点击事件的分发,其实就是对MotionEvent事件的分发过程;当一个MotionEvent产生了以后,系统则需要把这个事件传递给到一个具体的View,而这个传递的过程就是事件的分发过程。

    了解事件分发之前,需要了解到事件在分发传递过程中三个重要的方法:

    public boolean dispatchTouchEvent(MotionEvent ev)

    用来事件的分发,其实就是事件传递到了当前的View,则此方法一定会被调用。此方法返回boolean类型的数据。返回结果受到当前View的onTouchEvent和当前View下一级别View的dispatchTouchEvent方法的影响,表示是否消耗此事件。

    public boolean onInterTouchEvent(MotionEvent ev)

    用来判断是否拦截某个事件,如果当前View拦截了某个事件;在同一个事件序列中(按下,移动,抬起);此方法不会被再次调回,返回结果表示是否拦截该事件。

    public boolean onTouchEvent(MotionEvent ev)

    此方法在dispatchTouchEvent方法内部中调用,用来处理点击事件,返回结果表示是否处理此事件。

    我们知道这三个事件分别在Acvitivy,Window,ViewGroup,View类中存在我们先来看看他们各个类中的分布与区别;

    事件分发_001.jpg

    对于他们之间的联系 ,我们通常也可以用一段伪代码来表示:

    public boolean dispatchTouchEvennt(MotionEvent event){
            boolean consume = false;
            if(onInterTouchEvent(ev)){
                consume = onTouchEvent(ev);
            }else {
                consume = child.dispathTouchEvent(ev);
            }
            return consume;
        }
    

    可以看到父类dispatchTouchEvennt方法内部会调用onInterTouchEvent的方法,如果返回True,表示拦截了事件,则会调用自己的onTouchEvent方法,如果没有拦截,则会继续往下调用子类View的dispathTouchEvent方法;

    同时可以通过一张图看下整体由父类传递到子类的调用顺序:


    事件分发_002.png

    接下来我们就按照这个传递的顺序来探究分析整个事件分析从上到下的具体过程:

    首先我们来到Activity类中;查看dispatchTouchEvent方法:

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

    这里看到 Activity 的 dispatchTouchEvent 会把事件分发交给 WIndow 的 superDispatchTouchEvent 方法;同时如果返回了True,此方法也会返回True;否则会调用自己的 onTouchEvent(这里其实就是分发完成发现没有控件消耗了我们的事件);

    接下来我们先去看看 Window 的 superDispatchTouchEvent 方法:

      public abstract boolean superDispatchTouchEvent(MotionEvent event);
    

    Window 是一个抽象类,而真正实现 superDispatchTouchEvent 的方法则应该在 Window 的实现类 PhoneWindow 类中:

     // This is the top-level view of the window, containing the window decor.
        private DecorView mDecor;
    
      @Override
        public boolean superDispatchTouchEvent(MotionEvent event) {
            return mDecor.superDispatchTouchEvent(event);
        }
    

    继续调用 mDecor.superDispatchTouchEvent(event);而 mDecor又是什么呢?

       /**
         * Constructor for main window of an activity.
         */
        public PhoneWindow(Context context, Window preservedWindow,
                ActivityConfigCallback activityConfigCallback) {
            this(context);
            // Only main activity windows use decor context, all the other windows depend on whatever
            // context that was given to them.
            mUseDecorContext = true;
            if (preservedWindow != null) {
                mDecor = (DecorView) preservedWindow.getDecorView();
                mElevation = preservedWindow.getElevation();
                mLoadElevation = false;
                mForceDecorInstall = true;
                // If we're preserving window, carry over the app token from the preserved
                // window, as we'll be skipping the addView in handleResumeActivity(), and
                // the token will not be updated as for a new window.
                getAttributes().token = preservedWindow.getAttributes().token;
            }
            // Even though the device doesn't support picture-in-picture mode,
            // an user can force using it through developer options.
            boolean forceResizable = Settings.Global.getInt(context.getContentResolver(),
                    DEVELOPMENT_FORCE_RESIZABLE_ACTIVITIES, 0) != 0;
            mSupportsPictureInPicture = forceResizable || context.getPackageManager().hasSystemFeature(
                    PackageManager.FEATURE_PICTURE_IN_PICTURE);
            mActivityConfigCallback = activityConfigCallback;
        }
    
    
        @Override
        public final @NonNull View getDecorView() {
            if (mDecor == null || mForceDecorInstall) {
                installDecor();
            }
            return mDecor;
        }
    

    mDecor 和 PhoneWindow紧密相连,就是附在 Window上面的一个View;看下 DecorView 类的基础结构:

    public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {}
    

    可以看到 DecorView 其实就是一个FrameLayout ,这里我们可以知道 DectorView 其实就是我们 Activity 所设置的 View 的父类,包含 setContentView 中的一个子类 view,具体我们可以看到 AppCompatDelegateImpl 类中的下面的代码片段:

    class AppCompatDelegateImpl extends AppCompatDelegate
            implements MenuBuilder.Callback, LayoutInflater.Factory2 {
            
    
            private ViewGroup createSubDecor() {
                   .....
                   final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
                  ....
            }
    }
    

    这里AppCompatDelegateImpl其实是Activity委托类去初始化了相关信息,而在Window类中;

        @Nullable
        public <T extends View> T findViewById(@IdRes int id) {
            return getDecorView().findViewById(id);
        }
    

    就通过 DecorView 去获取设置 Activity 的 View了,android.R.id.content 也就是 Dector 的一个子 View;

    那么我们这里就可以发现事件已经从Activity传到了Window,然后又从 Window 给传到了 DecorView,而这个 DecorView 实则就是一个 ViewGroup ,那么接下来我们就应该继续看这个 ViewGroup 的superDispatchTouchEvent 是去做了什么呢?如何去处理这个事件传递的呢?superDispatchTouchEvent 最终会调用到 ViewGroup 类中的 dispatchTouchEvent 方法(即又开始进行事件分发了):

        @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;
                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;
                }
    
                // 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.
                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;
                if (!canceled && !intercepted) {
    
                    // If the event is targeting accessibility 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 childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                            ? findChildWithAccessibilityFocus() : null;
    
                    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;
                            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;
                                }
    
                                if (!child.canReceivePointerEvents()
                                        || !isTransformedTouchPointInView(x, y, child, null)) {
                                    ev.setTargetAccessibilityFocus(false);
                                    continue;
                                }
    
                                newTouchTarget = getTouchTarget(child);
                                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();
                                    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();
                        }
    
                        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) {
                    // 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;
                        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;
                    }
                }
    
                // 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;
        }
    

    这里 ViewGroup 的 dispatchTouchEvent 方法步骤很长,看着比较繁琐。下面我们会把主要的部分分段进行剖析,首先看下面:

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

    这里代表是个安全的手势动作,intercepted 表示是否要拦截事件,而先看第一个判断,我们这个事件为DOWN 事件,即手势按下事件的时刻,或者 mFirstTouchTarget != null 的时候,mFirstTouchTarget 后面会讲到即为子元素处理成功了,我们会进行赋值操作。也就是说我们 ViewGroup 一旦拦截了事件,那么mFirstTouchTarget 就是空的。除了ACTION_DOWN 以外的事件调用的时候,onInterceptTouchEvent 方法将不再会被调用了,同一次手势引起的触发的事件默认给 ViewGroup 进行处理。

    当然这里会有一种比较特殊的情况,那就是 FLAG_DISALLOW_INTERCEPT 这个标记位,这个标记位是通过 requestDisallowTouchEvent 方法设置的,一般用在子 view 当中,一旦设置后,ViewGroup 则无法拦截除ACTION_DOWN 以外的事件;为什么这样说呢?在上述代码里面我们可以看到在事件为 ACTION_DOWN 的时候会调用 cancelAndClearTouchTargets 和r esetTouchState 方法,此时就会清除我们的标记位;

    通过上面的情况,我们可以看到,如果父 ViewGroup 拦截了 ACTION_DOWN 的事件,不管你子 View 如何要求不拦截,ViewGroup 终会把这整个事件消耗完成,包括后续的 ACTION_MOVE ,ACTION_UP 等事件。而如果 ViewGroup 没有拦截 ACTION_DOWN 事件,此时子类想要处理 ACTION_MOVE,ACTION_UP 这些事件,只需要设置requestDisallowTouchEvent方法,让父类不要拦截即可;

    这里继续往下看:

                 if (!canceled && !intercepted) {
    
                    // If the event is targeting accessibility 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 childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                            ? findChildWithAccessibilityFocus() : null;
    
                    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;
                            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;
                                }
    
                                if (!child.canReceivePointerEvents()
                                        || !isTransformedTouchPointInView(x, y, child, null)) {
                                    ev.setTargetAccessibilityFocus(false);
                                    continue;
                                }
    
                                newTouchTarget = getTouchTarget(child);
                                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();
                                    newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                    alreadyDispatchedToNewTouchTarget = true;
                                    break;
                                }
    
                              ......
                        }
    
                        ......
                    }
                }
    

    可以看到这里 if (!canceled && !intercepted) 的判断,ViewGroup 不取消以及没有拦截事件的情况下,会去遍历ViewGroup的所有的子类,通过:

                               if (!child.canReceivePointerEvents()
                                        || !isTransformedTouchPointInView(x, y, child, null)) {
                                    ev.setTargetAccessibilityFocus(false);
                                    continue;
                                }
    
    

    isTransformedTouchPointInView 判断出手势的落点是否在某个 child 中,如果不是继续遍历下一个;如果判断到了手势落点位置在某个子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();
                                    newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                    alreadyDispatchedToNewTouchTarget = true;
                                    break;
                                }
    

    这里 dispatchTransformedTouchEvent 方法实际上会调用子元素 child 的 dispatchTouchEvent 方法,这样事件又交给子元素进行处理,从而完成了对子元素分发(落点在子元素的控件范围内);

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

    在对子元素进行分发的过程中,看到如果 child 的不为空,则会调用到handled = child.dispatchTouchEvent(event);方法:

    
    /**
         * 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)) {
                if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                    result = true;
                }
                //noinspection SimplifiableIfStatement
                ListenerInfo li = mListenerInfo;
                if (li != null && li.mOnTouchListener != null
                        && (mViewFlags & ENABLED_MASK) == ENABLED
                        && li.mOnTouchListener.onTouch(this, event)) {
                    result = 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;
        }
    
    

    在子元素的 dispatchTouchEvent 方法中,我们可以看到最终不仅会通过 mOnTouchListener 调用相关监听的方法,同时也会调用 onTouchEvent 方法来处理事件,而在实际开发操作做我们一般会重写 dispatTouchEvent 方法和 onTouchEvent 方法分别表示消耗掉事件以及处理相关事件;

    这里可以看到事件已经被onTouchEvent进行了处理并返回了结果;

    我们继续回到ViewGroup类中查看后续过程

    如果某个 child 的 dispatchTransformedTouchEvent 返回了True,我们可以看到,最终会执行下面的方法代码;

    
                                 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                    ......
                                    mLastTouchDownX = ev.getX();
                                    mLastTouchDownY = ev.getY();
                                    newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                    alreadyDispatchedToNewTouchTarget = true;
                                    break;
                                }
    

    newTouchTarget 和 alreadyDispatchedToNewTouchTarget 进行了赋值;终止对子类的循环遍历;我们看下addTouchTarget 这个方法;

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

    这里我们看到了 mFirstTouchTarget 的赋值操作,这在之前我们 ViewGroup 的 dispatchTouchEvent 的事件中提到过;

    继续往下看 ViewGroup 的 dispatchTouchEvent 方法:

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

    这里看到mFirstTouchTarget 为空的情况,其实就是通过分发发现没有子元素去处理这个事件;即 onTouchEvent 都返回了false;此时则又会调用到 dispatchTransformedTouchEvent 方法,这里注意第三个参数传的为null:

              //对子类进行分发事件
                if (child == null) {
                    handled = super.dispatchTouchEvent(event);
                } else {
                    handled = child.dispatchTouchEvent(event);
                }
    
    

    又到了这里的判断,但是此时child是null了,所以我们要调用super.dispatchTouchEvent(event)方法,而上面我们针对此方法最终会调用的 View 的 onTouchEvent 方法,实际中我们可能会重写 onTouchEvent 方法。此时就相当于子元素没有处理掉事件,此时又会一级级往上抛事件进行处理;

    这里我们可以发现一些分发机制并进行一点总结:
    1 ViewGroup 拦截了事件进行处理;则会通过自己 onTouchEvent 方法进行处理事件
    2 ViewGroup不断分发事件,子元素中有 dispatchTouchEven t事件返回为 True 对事件进行了处理(onTouchEvent返回true);
    3 子元素未能处理掉,则会向上抛事件

    最后若事件都没处理呢?

    我们再继续回到Activity里面去看看:

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

    最后一句 return onTouchEvent(ev);大家都搞不定的事情,看样子还是得自己出马去搞定呀!

    这样整个事件的分发就形成了一个环路,既然产生了事件,就一定会有对象去处理。

    这里顺便也提几个平时我们可能不太注意的地方,在View类中:

              if (li != null && li.mOnTouchListener != null
                        && (mViewFlags & ENABLED_MASK) == ENABLED
                        && li.mOnTouchListener.onTouch(this, event)) {
                    result = true;
                }
    
                if (!result && onTouchEvent(event)) {
                    result = true;
                }
    

    我们可以看到,这里我们先判断了mOnTouchListener,然后去调用onTouchEvent方法,而在View的onTouchEvent方法中:

    
             final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
    
            if ((viewFlags & ENABLED_MASK) == DISABLED) {
                if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                    setPressed(false);
                }
                mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                // A disabled view that is clickable still consumes the touch
                // events, it just doesn't respond to them.
                return clickable;
            }
    
          if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
         ......
                                  if (mPerformClick == null) {
                                        mPerformClick = new PerformClick();
                                    }
                                    if (!post(mPerformClick)) {
                                        performClickInternal();
                                    }
         }
    
    

    首先我们可以看到OnTouchListener的优先级会比onClickListner的优先级高(PerformClick内部会调用onClickListner方法),所以如果两个同时设置了,则会调用OnTouchListener的实现方法;同时一个控件只要设置了onClickListner或者onLongClick它一定是可以触发onClick实现方法,不管是不是Enable状态;

    最后我们用UML图来看下事件分发的整体过程:


    窝藏大白菜_003.png

    相关文章

      网友评论

          本文标题:谈一谈安卓的事件分发

          本文链接:https://www.haomeiwen.com/subject/gukybhtx.html