重学Android——View的事件分发

Android View 事件分发机制,大型职场 PUA 现场 前言 很高兴见到你 ???? 我不是大佬,但我相信我在通往成为大佬的路上 中我结合自身经历阐述了自己对学习方法的思考与实践。简单来说,我认为学习需要建立一套系统的知识体系(知识树),在此基础上可将学习分为 通用学习 与 需求学习。本文将简单讨论掌握知识的过程,并基于这一过程的各阶段进行内容创作实践。 掌握知识的三个阶段 个人认为,对知识的掌握,需要经历以下过程: 感性认识阶段 理性认识阶段 实践阶段 感性认识 可以看作是一个 从 0 到 1 的过程,它是认识的初级阶段。在这一过程学习者通常会寻找已有的素 阅读详情

本文源码Api28

View的事件分发

我们都知道,点击事件最重要的三个方法

//用来进行事件分发
public boolean dispatchTouchEvent(MotionEvent ev);
//判断是否拦截某个事件
public boolean onInterceptTouchEvent(MotionEvent ev);
//处理touch事件
public boolean onTouchEvent(MotionEvent event);

他们三者的关系用伪代码表示如下:

public boolean dispatchTouchEvent(MotionEvent ev){
    if (onInterceptTouchEvent(ev)){
        return onTouchEvent(ev);
    }else{
        return child.dispatchTouchEvent(ev);
    }
}

Activity中事件分发处理

当我们点击界面的时候,最先调用到Activity的dispatchTouchEvent(),

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

因为我们点击事件的开始,一般是由ACTION_DOWN开始的,所以我们第一个if是要进入的,查看源码

    /**
     * <p>All calls to your activity's {@link #onUserLeaveHint} callback will
     * be accompanied by calls to {@link #onUserInteraction}.  This
     * ensures that your activity will be told of relevant user activity such
     * as pulling down the notification pane and touching an item there.
     *
     * <p>Note that this callback will be invoked for the touch down action
     * that begins a touch gesture, but may not be invoked for the touch-moved
     * and touch-up actions that follow.
     *
     * @see #onUserLeaveHint()
     */
    public void onUserInteraction() {
    }

这个方法是个空方法,但是,我们可以在注释中看到,这个方法主要的作用就是实现屏保功能,并且当这个Activity在栈樟树布的时候,点击Home、Back等键时都会触发这个方法。

再来看第二个if语句,调用getWindow().superDispatchTouchEvent(ev)方法

    public Window getWindow() {
        return mWindow;
    }

而mWindow就是

    final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window, ActivityConfigCallback activityConfigCallback) {
        attachBaseContext(context);

        mFragments.attachHost(null /*parent*/);

        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        ...
    }

在attach这个方法中,mWindow被赋值成了PhoneWindow,所以就相当于事件由Activity传递到了PhoneWindow中了,如果返回true,整个事件循环就结束了,返回false就意味着没人处理,所有的view的onTouchEvent都返回了false,那么Activity自己的onTouchEvent就会被调用。

PhoneWindow事件分发
    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

可以看到phoneWindow的处理非常简单,直接把事件继续传递给了mDecor,也就是DecorView。

DecorView

在PhoneWindow的构造函数中

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

可以看到调用getDecorView方法获取到了此DecorView

public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
...
}

这个类是一个FrameLayout,也就是一个ViewGroup,这样,事件已经由Activity传递到了Window然后再传递到了顶层的View——DecorView了。

我们可以反过来看Activity中的setContentView

//AppCompatActivity    
public void setContentView(@LayoutRes int layoutResID) {
        this.getDelegate().setContentView(layoutResID);
}
//AppcompatDelegateImpl
@Override
public void setContentView(View v) {
    ensureSubDecor();
    ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
    contentParent.removeAllViews();
    contentParent.addView(v);
    mOriginalWindowCallback.onContentChanged();
}

可以看到,我们设置进去的view,其实都是add在这个顶级的view之上。

顶级ViewGroup对点击事件的分发

在DecorView中

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

其实就是调用的super.dispatchTouchEvent,继续跟踪源码,到了我们可爱的ViewGroup中了。

这个方法体代码比较多,可以分开一段一段的看,其实很多地方都有英文注释

@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.
            //处理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 (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null)在这两种情况下会进行事件拦截,ACTION_DOWN就是按下的事件,mFirstTouchTarget在下面的代码中马上要分析到,当事件由ViewGroup的子元素成功处理后,会把子元素赋值给mFirstTouchTarget。

我们可以这么理解:当ViewGroup不拦截事件,把事件传递给子元素后,子元素处理事件,那么,mFirstTouchTarget!=null,反过来,事件由ViewGroup拦截处理了的话,mFirstTouchTarget就是null,那么当ACTION_MOVW以及ACTION_UP到来时,这个if语句就不成立了,ViewGroup就不能进入到onInterceptTouchEvent方法,并且同一序列中的其他事件都会默认交给它处理。

我们注意到,在判断是否拦截下还有FLAG_DISALLOW_INTERCEPT这个标识位,

    @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            // We're already in this state, assume our ancestors are too
            //已经是这个状态了,直接return
            return;
        }

        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }

        // Pass it up to our parent
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }

可以看到,ViewGroup提供给外界用的这个方法,就是为了让final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;的boolean值进行改变的,当设置requestDisallowInterceptTouchEvent(true)时,那么dispatchTouchEvent方法中这个if语句直接进else分支,intercepted=false;ViewGroup这时不能拦截所有的除ACTION_DOWN以外的其他点击事件。

为什么是除了ACTION_DOWN以外呢,其实在上面的源码分析中,我们漏了一句,resetTouchState();,当ACTION_DOWN到来之时,VIewGroup都会调用resetTouchState,看方法名我们也能猜到是重置touch的状态。

    private void resetTouchState() {
        clearTouchTargets();
        resetCancelNextUpFlag(this);
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        mNestedScrollAxes = SCROLL_AXIS_NONE;
    }

在源码中,果然又把mGroupFlage的标识设为初始值。

因此,子View调用requestDisallowInterceptTouchEvent方法,并不能影响到ViewGroup对down事件的处理。

所以我们在处理事件冲突的时候,有两种处理方法

  • 外部拦截法——父容器对事件做拦截处理,重写父容器的onInterceptTouchEvent方法
    • 需要注意的是,ACTION_DOWN事件,父容器必须返回false,因为DOWN事件是谁消费的,那么后面的MOVE和UP也只能由它来消费,事件已经不能传递给子view了
    • ACTION_MOVE就看事件由谁处理,父处理就返回true,子处理就返回false
    • ACTION_UP必须返回false,因为如果事件是交给子元素处理的,一旦父容器返回了true,那么子元素就不能点击事件了,而父容器如果是要处理的话,它的DOWN事件由自己消费,那么,以后的UP事件也必定传给了自己,在这里设了false也不影响的
  • 内部拦截法——配合requestDisallowInterceptTouchEvent这个方法来使用
    • 同样的,父容器也必须是默认只拦截了除DOWN事件以外的别的事件,如果拦截了DOWN事件,我们设置requestDisallowInterceptTouchEvent是对它没有影响的

接下来我们继续看这个方法体下面当ViewGroup不拦截事件时,事件会交给子view处理的源码

        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;
            }
            //如果这个元素无法接收Pointer EVent或这个事件点压根就没有落到这个元素上
            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);
            //投递事件执行触摸事件
            //如果子元素还是一个子ViewGroup,则递归调用重复过程
            //如果子元素是一个view,那么会调用view的dispatchTouchEvent,并最终由onTouchEvent来处理
            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;
            }

上面这段代码我已经在关键地方做了注释,首先遍历ViewGroup的所有子元素,然后判断这个元素能够接收到事件:

    private static boolean canViewReceivePointerEvents(@NonNull View child) {
        return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                || child.getAnimation() != null;
    }

是否在播动画还是点击事件的坐标不能落到子元素的区域内。如果某个子元素满足这两条件,那么这个事件就交由它来处理。

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

调用dispatchTransformedTouchEvent时,源码主要就是这两句

if (child == null){
    handled = super.dispatchTouchEvent(transformedEvent);
}else{
    handled = child.dispatchTouchEvent(transformedEvent);
}

从源码角度也证实了,当事件落到这个view的区域上,且它不是正在执行动画时,事件已经由子元素处理,当子元素的dispatchTouchEvent返回true,这时暂时不去看子元素是怎么处理的,继续把上面的代码往下读,在这个if语句中,如果子元素返回了true,会执行到

    newTouchTarget = addTouchTarget(child, idBitsToAssign);
    alreadyDispatchedToNewTouchTarget = true;
    break;

设置addTouchTarget,直接跳出循环。

    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

可以看到,其实在addTouchTarget的内部,就把mFirstTouchTarget设置为了当前子元素。

继续往下阅读源码

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

如果在此时mFirstTouchTarget还没有被赋值,那么只能说明,要么没有子元素处理这个事件,要么处理了而并没有返回true,直接调用了dispatchTransformedTouchEvent,

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

这个方法在上面有介绍,我们可以看到,此时,传入的child为null,所以调用的其实就是super.dispatchTouchEvent(event);事件交由viewGroup自己来处理。

ViewGroup的流程图

ViewGroup 的事件分发流程图

View对事件的处理

当上面ViewGroup找到事件处理者,而且不是ViewGroup时,那么,就会调用到View的dispatchTouchEvent方法了

    //返回结果定义在方法内部变量result中,当返回true时,表示事件已被消费
	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);
        }

        //当actionMasked为ACTION_DOWN,停止滑动事件
        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }

        //判断窗口window是否被遮挡,方法返回true,事件可以继续被分发,false不再往下分发
        if (onFilterTouchEventForSecurity(event)) {
            //view当前是否被激活,并且有滚动事件
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            //ListenerInfo是一个内部类,定义了一些监听事件
            ListenerInfo li = mListenerInfo;
            //注意:li.mOnTouchListener就是我们通过setOnTouchListener设置的
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            //如果我们自己设置的setOnTouchListener,并返回了true,那么不再处理onTouchEvent事件了!!!
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

        if (!result && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }

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

        return result;
    }

view的这个方法没有ViewGroup那么吓人,量级要少得多,我也对主要代码逐行做了注释。特别要注意的地方就是**当我们在外面设置了setOnTouchListener时,这的优先级是比onTouchListener是要高的,如果在setOnTouchListener里返回了true,那么onTouchListener是不会处理了的。**这样的好处是方便外界来处理。

继续看事件分发到了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();

        //当前视图是否可被执行点击、长按
        //可通过java代码或者xml设置enable或clickable
        //当这些状态为false时,此clickable变量才为false,否则都是true
        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;
        }
        //如果View设置有代理,还会执行TouchDelegate的onTouchEvent方法
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

        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();
                                }
                                //最终ACTION_UP要执行的方法,post到UI线程中的一个runnable
                                if (!post(mPerformClick)) {
                                    performClickInternal();
                                }
                            }
                        }

                        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.
                    //判断当前view是否在滚动容器中
                    boolean isInScrollingContainer = isInScrollingContainer();

                    // For views inside a scrolling container, delay the pressed feedback for
                    // a short period in case this is a scroll.
                    //如果在滚动容器中,延迟返回事件,延迟时间为ViewConfiguration.getTapTimeout()
                    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
                    //判断当前滑动事件是否还在当前的view中
                    if (!pointInView(x, y, mTouchSlop)) {
                        // Outside button
                        // Remove any future long press/tap checks
                        //如果出view了,取消事件
                        removeTapCallback();
                        removeLongPressCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            setPressed(false);
                        }
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    }
                    break;
            }

            return true;
        }

        return false;
    }

代码有点长,做了一些必要的注释。主要总结有几点:

  1. 当view在不可用的状态下的时候照样会消耗点击事件,只是不响应

  2. 只要view的clickable或者long_clickable有一个为true,它就会消费这个事件,返回true。

    自定义view的clickable默认返回false,但button,textview默认是true。设置setOnClickListener时也会执行clickable=true。

  3. 第一个事件一定会是DOWN事件,在滚动窗口中会有延迟响应,不在则立即响应事件

  4. ACTION_MOVE只做了处理响应事件

  5. 在ACTION_UP中,会调用performClickInternal()方法,这就是我们熟悉的点击事件

   private boolean performClickInternal() {
        // Must notify autofill manager before performing the click actions to avoid scenarios where
        // the app has a click listener that changes the state of views the autofill service might
        // be interested on.
        notifyAutofillManagerOnClick();

        return performClick();
    }

    public boolean performClick() {
        // We still need to call this method to handle the cases where performClick() was called
        // externally, instead of through performClickInternal()
        notifyAutofillManagerOnClick();

        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            
            //这里其实就是我们熟悉的setOnClickListener.onClick()方法
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

        notifyEnterOrExitForAutoFillIfNeeded(true);

        return result;
    }
View的事件分发流程图

View 的事件分发流程图

参考:

《Android开发艺术探索》

通过流程图来分析Android事件分发


我的掘金
下面是我的公众号,欢迎大家关注我

推翻自己和过往,重学自定义View 关于自定义View以前看了很多资料看,从博客园到优快云,从stackoverflow到EOE论坛,从百草园到三味书屋,搜了一大筐,沮丧的发现这些文章大同小异:只举个简单的例子,很少研究为什么;人云亦云,文章里的内容根本没有去验证和深究就发出来了;或者避重就轻展示简单的Demo直接把难点给绕过去了;文章零零散散不成体系……每次看完这些文章觉得还是晕头转向,稀里糊涂,原本就不多的学习动力和勇气也消失殆尽了。 阅读详情

相关推荐

编译原理(东南大学)一:【编译原理期末冲刺救命笔记】速存!看完再上考场稳了!

本文详细介绍了编译原理的核心概念和流程,包括程序设计语言的分类、编译程序的各个阶段(如词法分析、语法分析、语义分析、中间代码生成、优化和目标代码生成)以及编译程序生成和构造的基础知识。文章还探讨了编译中的表格管理、出错处理和遍的概念,以及编译基础知识如高级语言、语法规则、字母表和符号串的运算。此外,文中还涉及文法与语言的关系,包括文法的概念、非终结符、终结符、产生式、推导和规约等。整体内容旨在为学习编译原理的学生提供全面的复习资料和考试准备指南。

一位搞嵌入式和前端的genius的博客 1027

Android智能事件触发器---ApkViewT…

ApkViewTracer是一个工具,主要是通过与Android模拟器中View Server通信,可以跟踪并分析模拟器当前Views的状态,可以向指定的某个View发送事件,触发其行为。目前,这个工具主要提供两个功能: 1.      Intelligent event trigger,在apk运行过程中更拟人地更智能地触发其行为,用于辅助Android Application的动态分析。

Viktor的专栏 1604

SoC 验证老兵手册

《SoC验证老兵手册》摘要: 针对大型SoC系统级验证,手册提出Cache+DMA场景的高效验证方案。核心方法采用C测试用例(模拟Linux/RTOS真实场景)触发隐蔽bug,配合SV Checker精准捕捉硬件问题(如DMA读取脏缓存行)。特别强调验证方法论:C用例负责复杂行为模拟,SV专注RTL正确性检查;利用Linux天然并发性暴露硬件缺陷。手册还包含模拟面试攻防技巧,指出验证架构师需具备定义Stimulus/Checker边界的能力。终极判断标准:未通过Linux下Cache+DMA+SMP+中断测

元直的博客 1370

Android进阶系列7—重说View的工作流程三部曲

从DecorView开始的View绘制流程,不是直接告诉你ViewGroup和View应该如何三部曲,而是以DecorView从上往下,从代码的逻辑分析了绘制流程,不客气区分View 、ViewGroup,更加易懂

Kerwin的学习历程 2852

当OTN网络遭遇LOS告警:5个运维老手才知道的故障定位技巧(含CA动作对照表)

本文深入解析OTN网络中LOS告警的实战定位技巧,涵盖光功率深度诊断、模块差异分析及核心的CA动作机制。通过逆向追踪告警流与对照速查表,帮助运维人员快速锁定故障段落,区分硬软故障,并掌握系统健康度评估方法,从而提升排障效率与网络稳定性。

weixin_29266007的博客 551

ICCompiler II实战:从NDM生成到APR流程的完整避坑指南

本文是一份针对ICCompiler II(ICC II)后端物理实现流程的实战避坑指南。文章详细解析了从NDM库生成、设计初始化、布局规划到时钟树综合与布线的完整APR流程,重点分享了各环节的常见陷阱、精细化配置命令与调试经验,旨在帮助工程师高效达成PPA目标,确保设计一次成功。

p4q5r6s7t的博客 531

Android开发:RecyclerView的点击事件

RecyclerView是Android中对ListView的升级,用来在有限的手机屏幕上展示大量的数据。除了用来展示数据,我们还可以为每个item,甚至每个item内的子控件设置监听器,以实现点击事件。 点击事件的实现方式有如下两种。 在onCreateViewHolder中注册点击事件 RecyclerView并非Android的原生空间,因此需要在build.gradle中添加依赖。 其次,在布局文件中添加RecyclerView。 <?xml version="1.0" encoding="

weixin_42708161的博客 1万+

Python+pytest+requests 自动化测试框架

环境准备 本次选用的是Python+Pytest+requests来搭建自动化框架,需要有 Python 环境(3.x 版本),安装pytest和requests,不会安装的自行去网上搜教程。 关于 Pytest 的基本使用,这里不作讲解,不清楚的可以看我之前的文章。 简单 Demo 我们先创建一个项目,取名api-test,然后在下面创建一个test_api.py的文件 # test_api.py import pytest import requests def test_01():

weixin_42686892的博客 1万+

带着问题重学Android事件分发

如果 ViewGroup 决定拦截事件,那么事件是不是应该交给 ViewGroup 来消费,这个是我们之前经常背的八股文,具体怎么做的呢,前面我们已经分析到如果 ViewGroup 决定拦截事件那么事件最终会交给 View 的函数处理,在代码1处定义了一个变量 result 用于标记这个事件是否已经被消费处理,代码2处判断了是否定义了以及onTouch。

2401_89820538的博客 1045

PCB数据集

整理了一部分PCB数据集

m0_57996537的博客 3867

基于嵌入式Linux设计的智能车库(GEC6818)

随着城市化进程的加快,传统停车管理方式存在效率低下、人工成本高、车主体验差等问题。为解决这些问题,本项目基于嵌入式Linux技术,设计并实现了一套智能车库管理系统。该系统采用GEC6818开发板作为核心控制器,运行Qt应用程序,并结合USB免驱摄像头进行车牌识别,RFID模块完成刷卡支付,SQLite数据库存储车主信息和停车记录。系统提供车主注册、用户管理、出入库管理、停车费结算、车辆查询等功能,并具备语音播报提示,提高用户体验。通过本系统,停车场管理更加高效、智能,有助于提升停车服务质量,减少人工干预,适

445

学习 View 事件分发,就像外地人上了黑车!

那我们该怎么做才能做到年薪60万+呢,对于程序员来说,只有不断学习,不断提升自己的实力。我之前有篇文章提到过,感兴趣的可以看看,到底要学习哪些知识才能达到年薪60万+。通过职友集数据可以查看,以北京 Android 相关岗位为例,其中 【20k-30k】 薪酬的 Android 工程师,占到了整体从业者的 30.8%!北京 Android 工程师「工资收入水平 」今天重点内容是怎么去学,怎么提高自己的技术。1.合理安排时间2.找对好的系统的学习资料3.有老师带,可以随时解决问题。

2401_84149845的博客 1008

爬虫瑞数5.5案例:欧冶钢材交易官网,带后缀K5nOZLud(面向对象补环境)

瑞数动态安全 Botgate(机器人防火墙)以“动态安全”技术为核心,通过动态封装、动态验证、动态混淆、动态令牌等技术对服务器网页底层代码持续动态变换,增加服务器行为的“不可预测性”,实现了从用户端到服务器端的全方位“主动防护”,为各类 Web、HTML5 提供强大的安全保护。由于之前某通信的瑞数已经调整,这里再出一篇文章,瑞数特点以及请求执行的流程已在之前的文章中分析过,这里不再特别讲解;

老衲爱洗头的博客 3043

重学Android之事件分发机制

通过本文了解Android中触摸事件在视图层次中的分发流程以及多点触控时系统的处理方式。通过分析源码的方式初步建立UI系统中处理事件的基本思路,为以后自己设计UI系统时做参考。同时也为开发Android自定义控件时处理复杂事件提供理论基础。 前置条件: 有一定的Android开发经验 源码版本: android 10 在开始之前先思考几个问题: 事件在视图层级中分派流程是怎样的 什么时机确认事件被某一View处理 如何记录事件应该分派到哪一个View 多点触控时如何处理事件分派 什么情况下View会收到A

Tom的博客 449

最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版

Part-E1-SDIO-Specification-Ver4.00-Final-120220.pdf:最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版,包括SDIO 接口的外设,SDIO 接口官方协议等。

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

view 判断显示 微信_资深安卓程序员带你用另一种角度学习 View 事件分发

我无法忘却 3 年前备受折磨的那个夜晚 —— 在我第一次学习 View 事件分发,却被网文折磨的那个夜晚。是网上介绍 View 事件分发的文章不够多吗?不是的,恰恰相反,网上的爆款文章不计其数,待你仔细阅读,却 颇有一种“外地人上了黑车”的感觉 —— 一言不合先上 30 张图表,带你在城市外围饶个上百圈,就是不直奔主题 解释一个现象为什么会存在、造成它存在的缘由为何、它如此设计是为了解决什么问题 ...

weixin_30431137的博客 91

廖雪峰Python教程[完整版]

廖雪峰的python教程整理的,已经排好版了,而且还是完整的,适合小白

带着问题重学Android事件分发,android移动应用开发试题

//排序所有的子控件 final ArrayList preorderedList = buildTouchDispatchChildList(); for (int i = childrenCount - 1; i >= 0; i–) { //获取子控件的index下标 final int childIndex = getAndVerifyPreorderedIndex( childrenCount, i, customOrder); //获取子控件对象 final View child = get

m0_66265001的博客 799

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

带着问题重学Android事件分发(1)

算法知识点繁多,企业考察的题目千变万化,面对越来越近的“金九银十”,我给大家准备好了一套比较完善的学习方法,希望能帮助大家在有限的时间里尽可能系统快速的恶补算法,通过高效的学习来提高大家面试中算法模块的通过率。这一套学习资料既有文字档也有视频,里面不仅仅有关键知识点的整理,还有案例的算法相关部分的讲解,可以帮助大家更好更全面的进行学习,二者搭配起来学习效果会更好。部分资料展示:有了这套学习资料,坚持刷题一周,你就会发现自己的算法知识体系有明显的完善,离大厂Offer的距离更加近。

2401_84003733的博客 299

最全学习 View 事件分发,就像外地人上了黑车!(2),面试必问公司的问题

很高兴见到你!今天我要和大家分享的是 View 事件分发。原本这是作为深度思考练习的随笔,没打算发表公开的。可没想到的是,我无法忘却 3 年前备受折磨的那个夜晚 —— 在我第一次学习 View 事件分发,却被网文折磨的那个夜晚 …… 🤯是网上介绍 View 事件分发的文章不够多吗?不是的,恰恰相反,网上的爆款文章不计其数,待你仔细阅读,却颇有一种“外地人上了黑车”的感觉。

2401_84538544的博客 949

Android最全学习 View 事件分发,就像外地人上了黑车!(1),2024年最新移动客户端开发岗面试题

最后这里放上我这段时间复习的资料,这个资料也是偶然一位朋友分享给我的,里面包含了腾讯、字节跳动、阿里、百度2020-2021面试真题解析,并且把每个技术点整理成了视频和PDF(知识脉络 + 诸多细节)。还有高级架构技术进阶脑图、高级进阶架构资料帮助大家学习提升进阶,这里我也免费分享给大家也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。一起互勉~网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

2401_84520182的博客 467

重学 Android 面试题

目录 Android Java 网络 Kotlin 项目经验 算法 设计模式 性能优化 高频问题: Android 1,Activity生命周期 2,Activity启动模式及功能和使用场景 3,Android动画有哪些?之间有什么区别? 4,Android事件分发机制 5,View绘制流程 6,滑动冲突 7,AyscTask 异步消息处理 8,Handler 9,Handler防止内存泄漏的方法 10,自定义View 11,AIDL 12,Android有几种进

ezconn的博客 1657

面试:事件拦截相关问题

1、view在执行dispatchTouchEvent的时候先要去判断该view是否有touchListener和执行TouchListener的onTouch方法的返回值。2、TouchListener的onTouch方法返回false,事件继续传递在dispatch方法中调用了同级的onTouchEvent方法3、然后在onTouchEvent方法中的UP事件中判断是否注册有onClickListener,有就执行onclick方法。android中的事件传递和处理机制 - fuly - 博客园。

王温暖的博客 884

自定义View系列教程07--详解ViewGroup分发Touch事件

深入探讨Android异步精髓Handler 站在源码的肩膀上全解Scroller工作机制 Android多分辨率适配框架(1)— 核心基础 Android多分辨率适配框架(2)— 原理剖析 Android多分辨率适配框架(3)— 使用指南 自定义View系列教程00–推翻自己和过往,重学自定义View 自定义View系列教程01–常用工具介绍...

weixin_30852367的博客 272

自定义View系列教程06--详解View的Touch事件处理

探索Android软键盘的疑难杂症 深入探讨Android异步精髓Handler 详解Android主流框架不可或缺的基石 站在源码的肩膀上全解Scroller工作机制 Android多分辨率适配框架(1)— 核心基础 Android多分辨率...

weixin_33675507的博客 246
上一篇: 重学Android——消息机制
下一篇: 重学Java——ThreadLocal源码解读
ApkCore
博客等级 码龄12年 8粉丝 24原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值