Android点击事件分发机制

一,View的事件分发

先概述下 View事件分分流程:View.dispatchEvent->View.setOnTouchListener->View.onTouchEvent,接下来详细分析各个流程:

1. dispatchEvent

    public boolean dispatchTouchEvent(MotionEvent event) {

     //****************省略部分代码******************
        boolean result = false;
     //****************省略部分代码******************
        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // 如果是ACTION_DOWN(一个事件序列的开始),则清除之前的手势
            stopNestedScroll();
        }

        //一些安全策略上的过滤
        if (onFilterTouchEventForSecurity(event)) {
        //View的状态是 enabled 的,并且可以像处理ScrollBar拖曳事件一样处理,则result = true
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //如是mOnTouchListener的onTouch方法能处理(返回true),则result = true
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            //如果以上都没能处理这个事件,则看当前 View的onTouchEvent能否处理
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
          //****************省略部分代码******************

        return result;
    }

其中mOnTouchListener就是我们调用setOnTouchListener方法设置上去的,也就是说如果OnTouchListener的 OnTouch方法返回ture,那么事件的处理就结束了,否则就交给onTouchEvent方法处理了;

2. onTouchEvent(event)

    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的状态是 disable 的
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
        //如果一个状态为 disable 的View,但它却是 clickable 也是可以消费事件的,只是没有什么反映。
            return (((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
        }
        //如果设置了TouchDelegate,并且TouchDelegate消费了该事件,则返回true(即:消费该事件)
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
                //进入了switch,则一定会返回true
            switch (action) {
                case MotionEvent.ACTION_UP:
                    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:
                    mHasPerformedLongPress = false;

                    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:
                    setPressed(false);
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_MOVE:
                    drawableHotspotChanged(x, y);

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

                            setPressed(false);
                        }
                    }
                    break;
            }

            return true;
        }

        return false;
    }

重点在 switch 模块,代码比较长,我们将代码按执行顺序拆解开来:

2.1 MotionEvent.ACTION_DOWN

                case MotionEvent.ACTION_DOWN:
                //设置处理长按事件的标志位为 false
                    mHasPerformedLongPress = false;
                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }

                    //这里是判断当前 View是不是在一个可以滚动的ViewGroup中
                    boolean isInScrollingContainer = isInScrollingContainer();
                    //如果是在一个可以滚动的 ViewGroup 中,则延时处理 press的回调,因为可能是一个滚动
                    if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        //延时一定时间(100ms),CheckForTap是一个 Runnable对象
                        //ViewConfiguration.getTapTimeout()=100ms ,代表着如果在这时间间隔内没有移动,则是一个轻触事件,否则是一个滚动事件
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        setPressed(true, x, y);
                        checkForLongClick(0, x, y);
                    }
                    break;

2.2 CheckForTap

  private final class CheckForTap implements Runnable {
        public float x;
        public float y;

        @Override
        public void run() {
            mPrivateFlags &= ~PFLAG_PREPRESSED;
            setPressed(true, x, y);
            checkForLongClick(ViewConfiguration.getTapTimeout(), x, y);
        }
    }

最后无论 View 是否在一个可以滚动的 ViewGroup 中,ACTION_DOWN 时都会调用setPressed 与 checkForLongClick 方法,setPressed 方法主要是更新些状态的标志位以及刷新背景,而checkForLongClick负责处理长按事件,如果支持长按事件的响应并设置了mOnLongClickListener调用OnLongClickListener的onLongClick方法,接着看 ACTION_MOVE 事件

2.3 MotionEvent.ACTION_MOVE

 case MotionEvent.ACTION_MOVE:
                    //刷新背景
                    drawableHotspotChanged(x, y);
                    // 如果移出当前View
                    if (!pointInView(x, y, mTouchSlop)) {
                    //取消之前ACTION_DOWN中设置的所有回调
                        removeTapCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            removeLongPressCallback();
                            setPressed(false);
                        }
                    }
                    break;

2.4 MotionEvent.ACTION_UP

  case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                    //*****************省略部分代码*******************
                    //如是长按事件没有发生或者发生了但返回 false
                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // 移除 ACTION_DOWN时设置的长按回调
                            removeLongPressCallback();

                            // 如是获取了焦点
                            if (!focusTaken) {
                            //下面是执行我们熟悉的onClick事件
                                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;

至此,Veiw的事件分发机制分析完毕。接下来分析一下 ViewGroup 的事件分发机制。

二. ViewGroup的事件分发机制

ViewGroup 继承自 View ,但它多了一个onInterceptTouchEvent的过程,大致流程如下:dispatchTouchEvent ->onInterceptTouchEvent->子 View, 照例,先看一下dispatchTouchEvent方法

1. dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent ev) {
  //**********************省略部分代码**********************
    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;

        if (actionMasked == MotionEvent.ACTION_DOWN) {
        //当为ACTION_DOWN事件时,重置所有的 Touch 状态,这里可能会影响上一个事件序列的up 或者 cancel事件,从而可能导致 App丢帧或者 ANR
            cancelAndClearTouchTargets(ev);
            resetTouchState();
        }

        final boolean intercepted;
        //注意这里的判断条件ACTION_DOWN,mFirstTouchTarget代表者事件处理者
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
                //disallowIntercept 代表着是否允许拦截,
               //disallowIntercept 可以通过 ViewGroup.requestDisallowInterceptTouchEvent(boolean disallowIntercept )设置
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
            //如果允许拦截,则调用onInterceptTouchEvent(ev)判断是否拦截
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action);
            } else {
                intercepted = false;
            }
        } else {
            //如果不是 ACTION_DOWN事件,并且在 ACTION_DOWN 事件中没找到处理者(mFirstTouchTarget == null)则对事件进行拦截
            intercepted = true;
        }

        //**********************省略部分代码**********************

        final boolean canceled = resetCancelNextUpFlag(this)
                || actionMasked == MotionEvent.ACTION_CANCEL;

        final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
        if (!canceled && !intercepted) {

            View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                    ? findChildWithAccessibilityFocus() : null;
            //注意这里的判断条件,ACTION_DOWN
            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;

                //**********************省略部分代码**********************

                //下面是重点,对每一个子 View进行循环,如果当前点击事件在子 Veiw的区域内,则调用子 View 的dispatchTouchEvent进行事件的处理。
                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能否接收事件以及点击区域是否在当前子 View的范围内
                        if (!canViewReceivePointerEvents(child)
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            ev.setTargetAccessibilityFocus(false);
                            continue;
                        }

                        newTouchTarget = getTouchTarget(child);

                       //**********************省略部分代码**********************

                       //如果子 View可以处理当前事件,即子 View的dispatchTouchEvent方法返回 true
                        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();
                            //这里会给mFirstTouchTarget赋新值
                            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);
        }
    }

  //**********************省略部分代码**********************
    return handled;
}

2.onInterceptTouchEvent

    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                && ev.getAction() == MotionEvent.ACTION_DOWN
                && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                && isOnScrollbarThumb(ev.getX(), ev.getY())) {
            return true;
        }
        return false;
    }

这个函数比较简单,但有以下几点需要说明的:
1. 这个函数会接收 ACTION_DOWN事件;
2. ACTION_DOWN事件要么被 child view 的onTouchEvent处理,或者被当前的 onTouchEvent 处理,所以你实现的onTouchEvent应当返回 true,当返回 true时,事件序列中的其它事件都将由此onTouchEvent处理,onInterceptTouchEvent不会再被调用;
3. 如果onInterceptTouchEvent返回 false,之后所有的事件都将分发到这里来,然后再分发到相应的onTouchEvent中;
4. 如果onInterceptTouchEvent返回 true,你将无法接收到其余事件,所有事件都会直接交给当前的onTouchEvent处理,但是如果 onTouchEvent不处理 ACTION_DOWN事件,那么其余事件都不会交给它处理;

三,总结

  1. 在 View的事件分发中,dispatchTouchEvent负责了所有的分发工件,首先会分发给当前View的OnTouchListener,如果OnTouchListener不能够处理(返回flase)当前事件,那么就交给 OnTouchEvent 处理。在 OnTouchEvent 中,首先在 ACTION_DOWN中会设置一个longClick的延时回调,如果 longClick 回调执行了并且 LongClickListener处理了(返回 true)事件,那么事件就被消费掉了,否则在 ACTION_UP时,事件会交给 OnClickListener 处理;
  2. 在 ViewGroup 的事件分发中,还是 dispatchTouchEvent 负责所有的分发工作,不过多了一个onInterceptTouchEvent,由onInterceptTouchEvent决断是否拦截当前事件,如果不拦截的话,交给能处理的子 View处理。子 View的处理方式同 ViewGroup或者,层层向下,如果在 ACTION_DOWN 时拦截了事件,onInterceptTouchEvent就不会再被调用了,所有事件都会直接交给当前的onTouchEvent处理,但是如果 onTouchEvnet不处理 ACTION_DOWN事件,那么其余事件都不会交给它处理;
  3. 事件的分发是层层向下的,只要有一层拦截了,那么下面的就接收不到事件,而处理是层层向上的,只要有一层处理了事件,那么事件就结束了,否则向上分发;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值