Android事件分发全面解析(源码篇)-夯实基础

有了前一篇的概念及Demo的亲自体验,那么接下来我们从源码出发,知根知底,看一下究竟。

在这里插入图片描述

Android中事件分发顺序:Activity(Window) -> ViewGroup -> View

其中:

  • super: 调用父类方法;
  • true:消费事件,即事件不继续向下传递;
  • false:不消费事件

所以,我们的重心也就是 Activity的分发机制,ViewGroup 的分发机制,View的分发机制

Activity的事件分发解析:

当一个点击事件发生时,事件最先传到Activity 的 dispatchTouchEvent() 进行事件分发。

我们来看一下具体的源码:

public boolean dispatchTouchEvent(MotionEvent ev) {
   	//关注点1
    //一般事件列开始都是 DOWN ,所以这里基本都是 true
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        //关注点2
        onUserInteraction();
    }
    //关注点3
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

关注点1 :一般这里开始都是 DOWN,所以这里返回 true,执行onUserInteraction();

所以我们直接跳到 关注点2

/**
 * Called whenever a key, touch, or trackball event is dispatched to the
 * activity.  Implement this method if you wish to know that the user has
 * interacted with the device in some way while your activity is running.
 * This callback and {@link #onUserLeaveHint} are intended to help
 * activities manage status bar notifications intelligently; specifically,
 * for helping activities determine the proper time to cancel a notfication.
 *
 * <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() {
}

可以看出:

  • 该方法为null方法
  • 当次Activity 在栈顶时,触屏点击按 home,back,menu键等都会触发此方法
  • 所以onUserInteraction 主要用于屏幕保护。

关注点3:

  • Window类是抽象类,而且PhoneWindow 类是 Window 类唯一的实现类。

  • superDispatchTouchEvent(ev) 是抽象方法

  • 我们通过PhoneWindow来看一下 superDispatchTouchEvent的作用:

  • @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
        //mDecor是DecorView的实例
    //DecorView是视图的顶层view,继承自FrameLayout,是所有界面的父类
    }
    
  • 接着我们再看 mDecor.superDispatchTouchEvent(event)

  • public boolean superDispatchTouchEvent(MotionEvent event) {
        //DecorView 继承自 FragmentLayout
        //所以它的父类也是ViewGroup
        //所以super.dispatchTouchEvent(event)调用的其实也就是 ViewGroup的 dispatchTouchEvent
        return super.dispatchTouchEvent(event);
    }
    

所以: 执行getWindow().superDispatchTouchEvent(ev)实际上是执行了 ViewGroup.dispatchEvent(event)

再回头看开始的源码,

public boolean dispatchTouchEvent(MotionEvent ev) {
   	//关注点1
    //一般事件列开始都是 DOWN ,所以这里基本都是 true
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        //关注点2
        onUserInteraction();
    }
    //关注点3
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

由于一般事件列都是 DOWN,所以这里返回true,基本上都会进 getWindow().superDispatchTouchEvent(ev)判断,所以 执行 Activity.dispatchTouchEvent 其实也就是执行 ViewGroup.dispatchTouchEvent(event),这样事件就从Activity 传递到 ViewGroup.

总结

当一个点击事件发生时,调用顺序如下:

  • 最先传入Activity的 dispatchTouchEvent()进行事件分发
  • 调用 window 类实现类 phoneWindow的 superDispatchTouchEvent()
  • 调用 Decoorvier 的 superDispatchTouchEvent()
  • 最终调用 Decoorvier 父类的 dispatchTouchEvent,也就是ViewGroup的 dispatchTouchEvent();
简单来说,也就是

当一个点击发生后,事件最先传到 Activity的 dispatchTouchevent进行事件分发,最终调用了ViewGroup的 dispathchTouchevent。这样事件就从Activity 传递到了ViewGroup.

ViewGroup 的事件分发解析:

dsispatchTouchEven() 源码分析:
//发生Action_DOWN事件或者已经发生过 ACTION_DOWN,并且将 mFirstTouchTarget赋值,才进入此区域,主要目的是拦截器。
//mFirstTouchTarget!=null也就是,当事件由ViewGroup的子元素成功处理时,mFirstTouchTarget会被复制并指向子元素。也就是说,当ViewGroup不拦截此事件并将此事件交给子元素处理时,mFirstTouchTarget!=null
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) {
        //disallowIntercept 是否禁用事件拦截器的功能(默认false)
        //可以在子view通过调用 requestDisallowInterceptTouchEvent 方法对这个值进行修改,不让该 view 拦截事件
    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.
    //当没有触摸targets,且不是down事件时,开始持续拦截触摸
    intercepted = true;
}

上面这段内容主要就是为判断是否拦截。如果当前事件为 MotionEcent.ACTION_DOWN或 mFirstTouchTarget!=null,则进入判断,判断是否拦截。如果不是以上两种情况,即已经是 MOVE或者 别的后续事件,并且之前的事件没有对象进行处理,则设置成 true,开始拦截所有事件。这也就解释了 如果子view 的onTouchEvent() 方法返回false,那么接下来的一系列事件都不会交给他处理。如果 ViewGroup 的 onInterceptTouchEvent()第一次执行为true,则 mFirstTouchTarget!=null,则也会使得接下来不会调用 onInterceptTouchEvent(),直接将拦截设置为 true

当然,这里有一种特殊情况,那就是 FLAG_DISALLOW_INTERCEPT标记位,这个标记位是通过 requestDisallowInterceptTouchEvent 方法来设置的,一般用于子 View中。FLAG_DISALLOW_INTERCEPT一旦设置后,ViewGroup 将无法拦截除了 ACTION_DOWN 以外的其他点击事件。为什么说是除了 ACTION_DOWN 以外的其他事件呢?这是因为 ViewGroup 在分发事件时,如果是 ACTION_DOWN 就会重置 FLAG_DISALLOW_INTERCEPT 这个标记位,将导致子 View 中设置的这个 标记位 无效。因此,当面对 ACTION_DOWN 事件时,ViewGroup 总会调用自己的 onInterceptTouchEvent 方法来询问自己是否要拦截事件。

当ViewGroup不拦截事件的时候,事件会向下分发交由它的子View或ViewGroup进行处理。

//从最底层的父视图开始遍历
//找寻newTouchTarget,即上面的 mFirstTouchTarget
//如果已经存在找寻newTouchTarget,说明正在接受触摸事件,则跳出循环
for (int i = childrenCount - 1; i >= 0; i--) {
    final int childIndex = getAndVerifyPreorderedIndex(
            childrenCount, i, customOrder);
    final View child = getAndVerifyPreorderedView(
            preorderedList, children, childIndex);

  	//如果当前视图无法获取用户焦点,则跳出本次循环
    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);
    
    //已经开始接受触摸事件,并退出整个循环
    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;
    }

    //重置取消或抬起标志位
    //如果触摸位置再 child 的区域内,则把时间分发给 View 或 ViewGroup
    resetCancelNextUpFlag(child);
    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
        // 获取TouchDown时间点
        mLastTouchDownTime = ev.getDownTime();
        if (preorderedList != null) {
            // 获取TouchDown的Index
            for (int j = 0; j < childrenCount; j++) {
                if (children[childIndex] == mChildren[j]) {
                    mLastTouchDownIndex = j;
                    break;
                }
            }
        } else {
            mLastTouchDownIndex = childIndex;
        }
        //获取TouchDown 的x,y坐标
        mLastTouchDownX = ev.getX();
        mLastTouchDownY = ev.getY();
        //添加TouchTarget,则mFirstTouchTarget != null。
        newTouchTarget = addTouchTarget(child, idBitsToAssign);
        //表示以及分发给NewTouchTarget
        alreadyDispatchedToNewTouchTarget = true;
        break;
    }

上面这段代码逻辑也很清晰,首先遍历 ViewGroup 的所有子元素,然后判断子元素是否能够接收到点击事件。是否能够接受点击事件主要由两点来衡量;子元素是否在播动画和点击事件的坐标是否落在子元素的区域内。如果某个子元素满足这两个条件,那么事件就会传递给它来处理。可以看到, dispatchTransformedTouchEvent 实际上调用的就是子元素的 dispatchTouchEvent 方法,在它的内部有如下一段内容,而在上面的代码中,child 传递的 不是 null,因此他会直接调用子元素的dispatchTouchEvent 方法,这样事件就交由子元素处理了。从而完成了一轮事件分发。

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

如果子元素的 dispatchTouchEvent 返回 true,这时我们暂时不用考虑 事件在子元素内部是怎么分发的,那么 mFritsTouchTarget 就会被赋值同时跳出 for循环,如下所示:

//添加TouchTarget,则 mFirstTouchTarget!=null
newTouchTarget = addTouchTarget(child, idBitsToAssign);
//表示以及分发给 NewTouchTarget
alreadyDispatchedToNewTouchTarget = true;
break;

其中 addTouchTarget(child, idBitsToAssign) 内部完成 mFirstTouchTarget 被赋值。如果 mFirstTouchTarget为空,将会让ViewGroup 默认拦截所有操作。如果遍历所有子 View或ViewGroup,都没有消费事件,这包含两种情况:第一种是ViewGroup没有子元素;第二种是子元素处理了点击事件,但是在dispatchTouchEvent中返回了false,就像我们最上面写的那个demo一样,是因为在onTOuchEvent中返回了false,这个时候,ViewGroup会自己处理点击事件。

总结:
  • Android事件分发是先传递到ViewGroup,再传递到View

  • 在ViewGroup 中通过 onInterceptTouchEvent() 对事件传递进行拦截

    1. onInterceptTouchEvent 返回值true代表拦截
    2. false代表继续传递(默认false)
    3. 子view中如果将传递的时间消费掉,ViewGroup 中将无法接收到任何事件。

View事件分发解析:

同样,我们还是先分析下 dsispatchTouchEven()方法:

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

从源码中可以发现:只有4个条件都为真,dispatchTouchEvent() 才返回true;否则执行 onTouchEvent(event)方法:

  1. li != null
  2. li.mOnTouchListener != null
  3. (mViewFlags & ENABLED_MASK) == ENABLED
  4. li.mOnTouchListener.onTouch(this, event))

**li != null **

ListenerInfo是View的一个静态类,包括了几个 Listener,比如TouchListener,FocusChangeListener等,一般情况下它均不为null,它不是我们今天讨论的核心,所以不用多关注它。

li.mOnTouchListener != null

mOnTouchListener 是由View设置的,比如 mButton.setTouchListener().所以如果View 设置了 Touch 监听,那么 mOnTouchListener 不为null.

//mOnTouchListener是在View类下setOnTouchListener方法里赋值的
public void setOnTouchListener(OnTouchListener l) { 

//即只要我们给控件注册了Touch事件,mOnTouchListener就一定被赋值(不为空)
    mOnTouchListener = l;  
}

(mViewFlags & ENABLED_MASK) == ENABLED

该条件是判断当前点击的控件是否 enable,由于很多view默认是enable ,所以该条件默认为true

li.mOnTouchListener.onTouch(this, event))

判断TouchListener 的onTouch() 方法是否消耗了Touch 事件。返回值为 true表示消费该事件,false表示未消费。

如果上面全部满足,那么result 为true,否则为 false,调用 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();

    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 (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }
	//如果该控件是可以点击就会进入到下两行的switch判断中
    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;
                //经过种种判断,会执行到关注点1的performeCLick() 方法
                //请往下看关注点1
                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)) {
                                //关注点1
                                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.
                boolean isInScrollingContainer = isInScrollingContainer();

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

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

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

                // Be lenient about moving outside of buttons
                if (!pointInView(x, y, mTouchSlop)) {
                    // Outside button
                    // Remove any future long press/tap checks
                    removeTapCallback();
                    removeLongPressCallback();
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                        setPressed(false);
                    }
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                }
                break;
        }
	//如果该控件是可以点击的,就一定返回true
        return true;
    }
//如果该控件不可以点击,就一定返回false
    return false;
}

关注点1:

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);
        li.mOnClickListener.onClick(this);
        result = true;
    } else {
        result = false;
    }

    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

    notifyEnterOrExitForAutoFillIfNeeded(true);

    return result;
}
  • 只要li.mOnClickListener!=null与li!=null,就回去调用 onClick方法。

  • 那么,mOnclickListener 又是在哪里赋值的呢?

  • public void setOnClickListener(OnClickListener l) {  
        if (!isClickable()) {  
            setClickable(true);  
        }  
        mOnClickListener = l;  
    }
    

当我们通过调用 setOnclickListener 方法来给控件注册一个点击事件时,就会给 mOnclickListener赋值,即回调onClick();

总结:

  1. onTouch()的执行高于onClick(),这一点从最上面的Demo都可以看出。

  2. 如果在回调onTouch() 里返回false,就会让 dispatchTouchEvent 方法返回false,那么就会执行 onTouchEvent();如果回调了 setOnclickListener() 来给控件注册点击事件的话,最后会在 performClick() 方法里回调 onClick()。

    onTouch() 返回false,未消费该事件->执行onTouchEvent()->执行 onClick

    如果在回调 onTOuch()里返回 true,就会让 dispatchTouchEvent 方法返回 true,那么将不会执行 onTouchEvent(),即onClick(),也不会执行;

    ​ onTouch()返回true(该事件被onTouch()消费掉)=dispatchTouchEvent()返回true,即不会再向下传递,也就不会执行onTouchEvent(),不会执行 Onclick()

onTouch()onTouchEvent() 的区别:

  • 这两个方法都是在View 的dispatchTouchEvent 中调用得,但onTouch 优先于 onTouchEvent 执行。

  • 如果在onTouch 方法中返回 true 将事件消费掉,onTouchEvent() 将不会在执行。

      if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
    
            if (!result && onTouchEvent(event)) {
                result = true;
            }
    
  • 因此如果有一个控件是 非 enable 的,那么给它注册 onTouch 事件将永远得不到执行。对于这一类控件,如果我们想要监听它的 touch 事件,就必须通过在该控件中重写 onTOuchEvent 方法来实现。

到了这里,我们的事件分发源码解析也就差不多结束了,如果有什么问题,也欢迎与我讨论。

更多Android开发知识请访问—— Android开发日常笔记,欢迎Star,你的小小点赞,是对我的莫大鼓励。

参阅:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

petterp

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值