View事件分发机制

点击事件的传递规则

点击事件产生后,传递过程:Activity->Window->顶层View->分发到具体的View。前两个传递比较简单,不用说。顶层View一般为ViewGroup,ViewGroup会首先根据onInterceptTouchEvent判断是否拦截,如果拦截,那么就会调用自己的onTouchEvent方法进行处理,如果不拦截,就会分发给子View,子View再调用自己的dispatchTouchEvent做进一步分发。如果所有的View都不消费掉这个事件,那么会调用Activity的onTouchEvent方法来处理。
对于一个View而言,OnTouchListener的优先级高于自己的onTouchEvent方法,在onTouchEvent方法中,TouchDelegate的优先级最高,其次是OnLongClickListener(如果是长按的话),最后才是OnClickListener。

源码解析

当触摸事件发生时,Activity的dispatchTouchEvent首先被调用。

// Activity$dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    // 首先分发给Window去处理,如果该事件没有被消费掉,那么
    // 会调用Activity的ouTouchEvent方法
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}
// PhoneWindow$superDispatchTouchEvent
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}
// DecorView$superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
}
// ViewGroup$dispatchTouchEvent
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
    // 出于安全考虑,如果设置了窗口被遮挡后丢弃事件的属性,那么窗口被
    // 遮挡时,事件直接被丢弃,而不会分发出去。也就是说,当出现了Toast、
    // 对话框等的时候,触摸事件就不会被响应了。该属性一般不会被设置。
    if (onFilterTouchEventForSecurity(ev)) {
        ...
        // 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);
            // 这个方法中会重置FLAG_DISALLOW_INTERCEPT标识,该标识表示当前
            // ViewGroup不能拦截触摸事件,也就是说,无论如何该标识都不能控制
            // ACTION_DOWN事件的拦截
            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.
            // 也就是说如果ViewGorup拦截了某触摸手势的ACTION_DOWN事件后,
            // 该触摸手势的后续事件都交给这个ViewGroup进行处理,不再需要用
            // onInterceptTouchEvent判断是否拦截
            intercepted = true;
        }
        ...
    }
}
...
// 还是ViewGroup$dispatchTouchEvent
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
    // ViewGroup不拦截时的事件分发
    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;
        }

        if (!canViewReceivePointerEvents(child)
                || !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 (mFirstTouchTarget == null) {
        // ViewGroup进行拦截或者在子View中没有处理事件时,ViewGroup
        // 调用父类View的dispatchTouchEvent进行处理。
        // No touch targets so treat this as an ordinary view.
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
                TouchTarget.ALL_POINTER_IDS);
    }
    ...
}

明显,在正常的事件(通常是ACTION_DOWN)分发时,ViewGroup会依次查询每个子View,如果子View能够接收到触摸事件,就会把事件交给该子View去处理。判断是否能接收到触摸事件有两点:view没有在播放动画,也没有准备播放动画;触摸事件的坐标处于view的范围内。子View收到事件后,会调用自己的dispatchTouchEvent来进一步分发。如果子View处理了该事件,那么mFirstTouchTarget就会在addTouchTarget方法中被赋值,并且终止事件的分发。在之前的代码中,可以看到,如果mFirstTouchTarget为null,也就是没有任何子View处理该事件(通常是ACTION_DOWN),那么接下来的所有事件都会被当前的ViewGroup拦截。

// ViewGroup$dispatchTransformedTouchEvent
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    ...
    // 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);
    }
    ...
}
// View$dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
    ...
    boolean result = false;
    ...
    if (onFilterTouchEventForSecurity(event)) {
        //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;
        }
    }
    ...
    return result;
}

View在处理触摸事件时,会先判断是否设置了OnTouchListener,如果设置了而且onTouch方法处理了该事件,就不再调用onTouchEvent进行处理;否则才会调用onTouchEvent。明显,onTouch优先级高于onTouchEvent。

// View$onTouchEvent
public boolean onTouchEvent(MotionEvent event) {
    ...
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);
        }
        // A disabled view that is clickable still consumes the touch
        // events, it just doesn't respond to them.
        return (((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
    }

    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }

    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
            (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                // 如果在ListView等可以滚动的容器中ACTION_DOWN时,
                // 会有一个延迟之后,才会置位PFLAF_PRESSED,此前都是
                // PFLAG_PREPRESSED被置位。
                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();
                            }
                        }
                    }
                }
                break;
                ...
        }
        return true;
    }

    return false;
}

明显,只要是CLICKABLE或者LONG_CLICKABLE或者CONTEXT_CLICKABLE的,不管View是否是enabled状态,都会消费点击事件。如果View是enabled的,那么会看是否设置了代理,如果有代理,那么先调用代理的onTouchEvent方法看是否被消费掉,如果没有消费,那么才会真正由View来做处理。在ACTION_UP时如果点击事件没有被OnLongClickListener.onLongClick方法消费掉,那么就会调用OnClickListener的onClick方法。

// View$performLongClick
public boolean performLongClick() {
    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);

    boolean handled = false;
    ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnLongClickListener != null) {
        handled = li.mOnLongClickListener.onLongClick(View.this);
    }
    if (!handled) {
        handled = showContextMenu();
    }
    if (handled) {
        performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
    }
    return handled;
}
// View$performClick
public boolean performClick() {
    final boolean result;
    final ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);
        result = true;
    } else {
        result = false;
    }

    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
    return result;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值