Android 事件分发机制分析

Android 事件分发机制分析

一、引子

先来看一段代码。

findViewById<TextView>(R.id.tvHellWorld).setOnClickListener {
    Log.d("TAG", "onCreate: 3333333")
}

很熟悉的一个点击事件,对吧,我们在对TextView点击之后,就会打印出日志。

那大家有没有想过,我们的点击操作是怎么一层层到我们这个点击回调呢?这就是我们接下来要聊的东西了。搬好小板凳,一起来看看吧。

二、缘起-事件

说到这个,就必须得说一说Android中事件的到底是什么。

我们日常操作手机都是点击,滑动。那么对应到到事件就是ACTION_DOWN,ACTION_MOVE,ACTION_UP。down代表事件开始,up代表事件结束,中间会穿插move事件,总结来说就是down->move->move->···->move->up。当然事件对应的情况不仅仅只是有这几种,还有其他的一些情况,比如长按事件等。这些情况都被包含在一个叫MotionEvent的类里面的,体现在代码里面就是这样的。

我们再来谈一谈事件的分发流程,一个触摸事件会从Activity->ViewGroup->View完成整个的流程。事件会先到Activity再又Activity去进行分发处理。具体细节我们一看源码就知道了。话不多说,我们直接开始源码的解析。

三、缘生-Activity分发事件

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

Activity的分发事件方法很少呀。正好我们一行一行的分析。先看第一个if判断里面的逻辑。

这里判断了按下事件,也就是一个触摸事件的开始。那我们看看onUserInteraction()方法里都做了啥。

public void onUserInteraction() {
}

我真的把所有的源码粘贴下来了,没错,它就是个空方法。幸好还有注释可以看看。

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 notification.

可以阅读就阅读下。阅读不了请看下一段。

每当向活动分派按键、触摸或轨迹球事件时调用。如果您希望知道用户在您的 Activity 运行时以某种方式与设备进行了交互,请实现此方法。此回调和 {@link onUserLeaveHint} 旨在帮助 Activity 智能管理状态栏通知;具体来说,用于帮助活动确定取消通知的适当时间。

是的,这个方法是需要你自己重写实现的,做一些界面元素改变的操作。这个方法也不影响后续的事件分发。为什么不影响呢,上面的代码写着呢,哈哈哈哈哈。

我们再看第二个if判断,getWindow()是获取到了什么呢。

public Window getWindow() {
    return mWindow;
}

获取到了window实例,那我们去看看这个Window类里面都是啥呢。

/* <p>The only existing implementation of this abstract class is
 * android.view.PhoneWindow, which you should instantiate when needing a
 * Window.
 */
public abstract class Window {
		//.....省略代码
		public abstract boolean superDispatchTouchEvent(MotionEvent event);
		//.....省略代码
}

发现这个window类只是个抽象类,我们想看事件分发的实现怎么看呢,既然他是抽象类,那么肯定有人实现了它呀。可以看到在类上面的注释,只有唯一的实现类PhoneWindow。再去这个PhoneWidow瞧瞧。

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}

它又调用了mDecor的方法,我们看看这个mDecor是啥呢。

// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;

DecorView,window的顶层View。

public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
}

我们可以看到DecorView里面调用了父View的dispatchTouchEvent()方法,DecorView的父View是啥呢。

public class DecorView extends FrameLayout{
  //...
}

它的父View是FrameLayout,那不用想了FrameLayout的父View肯定是ViewGroup,那肯定会调用到ViewGroup的dispatchTouchEvent()方法。

是不是很神奇,从Activity的分发方法里调用到了ViewGroup的分发方法,就通过简单的继承关系,完成事件的转移。妙啊。

看下事件流转的过程吧。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f7R2xvwk-1624952972218)(/Users/tzl/Downloads/未命名文件 (1)].png)
在这里插入图片描述

看上面一张图,就应该很清晰了。这还不去和面试官对线嘛。

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

再次熟悉Activity的dispatchTouchEvent(),我们聊着聊着也不能忘了Activity的onTouchEvent方法吧。

我们看看这个方法吧。

public boolean onTouchEvent(MotionEvent event) {
    if (mWindow.shouldCloseOnTouch(this, event)) {
        finish();
        return true;
    }

    return false;
}

它还调用了Window的shouldCloseOnTouch()方法。如果这个方法返回为true就代表,就会调用finish()方法,关闭activity。

public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
    final boolean isOutside =
            event.getAction() == MotionEvent.ACTION_UP && isOutOfBounds(context, event)
            || event.getAction() == MotionEvent.ACTION_OUTSIDE;
    if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
        return true;
    }
    return false;
}

我们再看这个方法。首先会判断这时候的操作是不是抬起操作和点击了Activity之外的或者是操作发生在界面之外。mCloseOnTouchOutside是一个boolean变量,它是由Window的android:windowCloseOnTouchOutside属性值决定。peekDecorWindow()返回的是DecorWindow的实例。通过这几个属性来控制是否该关闭这个activity。

四、缘生-ViewGroup的事件分发

先来看看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);
    }
		//....省略代码
    return handled;
}

先来看看这个mInputEventConsistencyVerifier东西具体是啥。

/**
 * Consistency verifier for debugging purposes.
 * @hide
 */
protected final InputEventConsistencyVerifier mInputEventConsistencyVerifier =
        InputEventConsistencyVerifier.isInstrumentationEnabled() ?
                new InputEventConsistencyVerifier(this, 0) : null;

这个是在ViewGroup的父View中找到的,就是View,不会还有人不知道ViewGroup的父View是View吧,不会吧,不会吧。看这个逻辑,具体得看isInstrumentationEnabled()方法。走吧,具体去看看吧。

public static boolean isInstrumentationEnabled() {
    return IS_ENG_BUILD;
}

一个变量,我们去看看这个到底是啥吧。

/** {@hide} */
public static final boolean IS_ENG = "eng".equals(TYPE);

最后会调用到Build类的这个属性,经过我的实验,这个TYPE其实在手机上一直返回user,所以其实isInstrumentationEnabled()这个方法会一直返回false,所以这个mInputEventConsistencyVerifier其实一直都为null。

@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);
    }
		//....省略代码
    return handled;
}

所以第一个if判断在大部分情况是并不会走到判断里面的。再来看第二个if里面的东西判断是不是当前视图是不是可以聚焦的,如果是的话,就清除该标志,并且进行正常的事件分发。我们再接着看下面的代码。

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

  	//.....
    if (!canceled && !intercepted) {
     			//........省略代码
        if (actionMasked == MotionEvent.ACTION_DOWN
                || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
            			//......省略代码
                    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 (preorderedList != null) preorderedList.clear();
            }
        }
    }
	//......省略代码
}

代码有点多啊,不要慌,我们慢慢看,首先我们会发现所有的逻辑都是在onFilterTouchEventForSecurity()返回为true之后才会进行。

我们先去看看这个方法里具体处理什么逻辑。不过一般都会返回true,不然我们的事件分发就直接在这就结束了。

public boolean onFilterTouchEventForSecurity(MotionEvent event) {
    //noinspection RedundantIfStatement
    if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
            && (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
        // Window is obscured, drop this touch.
        return false;
    }
    return true;

看着几个标志的意思都是如果界面被遮挡了,就会赋值为true。界面被遮挡了,就不会处理触摸事件。逻辑很简单,不复杂。按照正常的思维来说,界面被遮挡了,确实不应该处理触摸事件的。再往下走,代码很多,我们只看关键的处理。

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

看到一个关键的方法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;
}

没有特别复杂的逻辑,判断了各种情况都满足的话就返回true表示需要拦截这个事件。否则,就返回false。这些情况总共在一起都是判断是不是鼠标操作了。好了,我们接着往下走。

 if (!canceled && !intercepted) {
   //...省略代码
 }

这个判断是需要执行看是不是被拦截的还是取消了。没有被拦截或者取消的话,就会执行事件分发的操作。我们看看if里面的代码。

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()方法里面应该就会去调用子View的dispatchTouchEvent()方法。

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

看来我们没有猜错,这个方法里果然调用了子View的dispatchTouchEvent()方法。我们看下逻辑,如果子View为空,会调用父View的dispatchTouchEvent(),而ViewGroup的父View是View也就是说,事件一定会调用到子View里面的dispatchTouchEvent()方法的。就这样事件就分发到了View的dispatchTouchEvent()方法里面了。

ViewGroup分发事件到子View里面,没有太多的逻辑。唯一一点就是需要判断onInterceptEvent()方法是不是返回true。好对事件去做拦截,不需要分发到子View中。

五、缘生-View的事件分发

我们看一下dispatchTouchEvent的方法。

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

onFilterTouchEventForSecurity(event)这个方法也不用说,肯定是判断界面是不是被遮挡了,遮挡了就不出处理遮挡事件。这个方法里的逻辑在ViewGroup的事件分发解析里已经分析过了。这里就不再解释了。

我们再看看这个 ListenerInfo类,这个类是一个内部类里面都封装了各种的触摸事件和点击事件。也就不再详谈了。

这个onTouch()方法熟悉嘛,没错就是我们写的触摸事件监听回调。如果我们在这个回调里面返回了true,那么就代表了事件在这个回调方法里处理了

如果这个回调不返回true,那么就会调用onTouchEvent()方法了。我们再去看看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;
        }
    }
  
  	if (clickable || (viewFlags & TOOLTIP) == TOOLTIP){
      //....省略代码
       return true;
    }
  return false;
}

不知道大家发现盲点没,只要代码进入到if (clickable || (viewFlags & TOOLTIP) == TOOLTIP)都会返回true。这是不是一个很奇怪的点。

我们再看看判断里面的代码,代码很多具体就不给出来了,经过很多逻辑判断会调用到performClickInternal()这个方法。

private boolean performClickInternal() {
    notifyAutofillManagerOnClick();
    return performClick();
}

我们再看看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;
}

我们最后的点击事件终于在这里被处理了。看到这里是否解释了你最开始的疑问呢。事件是怎么一步步到onClick方法的呢。

六、缘灭-事件流向

看过上面的最基础的事件流向,大家应该就知道了从Activity->ViewGroup->View。那从头到尾整个事件没有消费的话呢。最终就会走到Activity的onTouchEvent()方法,然后就被处理掉了。如果其中有的被消费掉了就不会再往下传递。
再看看一张网图。以下图片来自百度,如有侵权必删。
在这里插入图片描述
这样应该就很清晰了。

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值