之前在面试的过程中遇到了Android事件分发的相关面试题,其实之前也看了一些源码性的东西,但是感觉脑子里对这个东西还是比较乱,但是一些面试题还是答的不太好,故此自己来总结一下。
Android事件分发机制
当我们点击屏幕之后,就产生了点击事件,这个事件被封装成了一个类:MotionEvent,之后系统将MotionEvent传递给View层级,在View层级中的传递过程就是点击事件的分发。
当我们点击事件产生之后,事件首先会传递给当前的Activity,调用Activity的dispatchTouchEvent,之后交给Activity中的PhoneWindow来处理,PhoneWindow再将事件交给DecorView,DecorView又交给根ViewGroup来处理,所以我们先来看ViewGroup中的dispatchToucheEvent.
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (actionMasked == MotionEvent.ACTION_DOWN) {
//如果是按下事件,则放弃先前所有的状态
//由于程序切换,ANR或者一些其他的状态改变,可能放弃上一个手势的up或者cancel事件
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// 检查拦截
final boolean intercepted;
//如果是按下事件或者没有拦截(如果拦截mFirstTouchTarget为null)
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//禁止ViewGroup拦截除了DOWN之外的事件
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;、
//如果拦截了事件
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); //如果已更改请还原操作
} else {
intercepted = false;
}
} else {
//没有触摸目标,并且此动作不是最初的,因此继续拦截触摸
intercepted = true;
}
//除按下事件的其他事件
...
return handled;
}
在ViewGroup中,首先判断是否是按下事件DOWN,如果是则清除之前的所有状态重新开始。接着检查是否要拦截。接着判断如果是按下事件或者mFirstTouchTarget是否为null(如果拦截了则会将mFirstTouchTarget=null,如果没有拦截则则交给子View处理,则mFirstTouchTarget!=null),之后将标志位设为之只拦截DOWN事件,之后执行onInterceptTouchEvent,如果触发的是MOVE、UP等事件则不会执行该方法。
接下来看看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;
}
//默认返回false不拦截
return false;
}
onInterceptTouchEvent默认返沪false不进行拦截,如果你想要拦截则应该在自定义的ViewGroup中重写onInterceptTouchEvent方法。
我们来看看dispatchToiuchEvent()其他源码:
//获取到所有子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);
//触摸点没有在子View范围内
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
//下一个child
i = childrenCount - 1;
}
//是否在子View的范围内或者是否子View是否在播放动画
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
//获取到符合条件的View
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
//关键代码
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
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;
}
ev.setTargetAccessibilityFocus(false);
}
通过倒序循环(从最外层View到内层View遍历)来找到符合条件的子View,首先判断子View是否能获取焦点,如果能接收到则交由子View来处理,接着判断子View是否在范围内或者是否在播放动画,如果均不符合跳过当前继续寻找下一个,一直找到合适的子View。
我们看一下注释标注的关键代码dispatchTransformedTouchEvent:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
//如果没有子View则调用父类的dispatchTouchEvent,有子View则会调用自己的dispatchTouchEvent
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
}
官方注释:将运动的事件转换为特定子视图的坐标空间,过滤掉无关的point ids,并在必要时覆盖其动作,如果child为null,则将事件发给父类。
在该方法中就是判断是否有子View,有则调用子View自身的dispatchTouchEvent,没有则会调用父类的dispatchTouchEvent。
那我们就来看一看View的dispatchTouchEvent:
public boolean dispatchTouchEvent(MotionEvent event) {
// 如果改事件有可访问性
if (event.isTargetAccessibilityFocus()) {
//没有焦点或者没有虚拟后代,则不处理
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
// 我们获取到事件并开始分发
event.setTargetAccessibilityFocus(false);
}
...
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
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;
}
}
...
}
View中的dispatchTouchEvent最关键就是注释代码。
如果onTouchListener不为null并且onTouch方法返回true,则表示事件被消费,就不会执行onTouchEvent(event),否则会执行onTouchEvent。可以看出onTouchListener中的onTouch()优先级高于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 (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
...
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
...
boolean focusTaken = false;
...
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
removeLongPressCallback();
if (!focusTaken) {
if (mPerformClick == null) {
//关键代码
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
...
return true;
}
return false;
}
我们在这里主要分析switch中的ACTION_UP,只要View中的CLICKABLE和LONG_CLICKABLE有一个为true,那么onTouchEvent就会就会消耗掉这个事件。通过View的setClickable和setLongClickable来设置也可以通过View的setOnClickLisntenr和setOnLongClickListener来设置,他们会自动将View设置为CLICKABLE和LONG_CLICKABLE.
我们看一下关键代码PerformClick:
public boolean performClick() {
notifyAutofillManagerOnClick();
final boolean result;
final ListenerInfo li = mListenerInfo;
//如果View设置了点击事件就执行onClick
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;
}
在performClick中如果View设置了onClickListener则会执行onClick方法。
总结
首先我们按下手机屏幕,产生点击事件,Activity最先收到点击事件,之后发给PhoneWindow中的DecorView中的dispatchTouchEvent来处理,也就是发送到了ViewGroup的dispatchTouchEvent,发送到ViewGroup中首先判断是否是按下事件,按下之后判断是否拦截事件,默认不拦截,拦截的话需要我们重写onInterceptor方法。之后不拦截之后继续向下执行ViewGroup中的dispatchTouchEvent,倒序循环找到合适的子View,找到之后判断如果有子View则调用子View的dispatchTouchEvent,如果ViewGroup没有子View则会调用父类的dispatchTouchEvent,之后进入View的dispatchTouchEvent,之后判断onTouchListener是否为null以及onTouchListener.ouTouch方法返回true(在这里可以看出onTouch优先级高于onTouchEvent),如果不为null且onTouch返回true标识事件被消费,否则会调用onTouchEvent方法,在onTouchEvent中只要View的CLICKABLE或者LONG_CLICKABLE有一个为true,就是点击事件以及长按事件有一个为true就会消耗掉事件,一般我们通过设置点击事件setOnClickListenere以及长按事件setOnLongClickListener。之后再ACTION_UP中调用performClick,在该方法内部中如果设置了点击事件onClick则会被执行。
点击事件分发的传递规则
首先点击事件是由上而下的传递规则,当点击事件产生之后会由Activity来处理,传递给PhoneWindow,在传递给DecorView,最后传递给顶层的ViewGroup。传递给ViewGroup一般我们都考虑他是否拦截,如果拦截方法onInterceptTonchEvent返回true则不会向下传递,就交给ViewGroup的onTouchEvent来处理;如果onInterceptorTouchEvent返回false则不拦截,事件继续向下传递,传递给子View的dispatchTouchEvent(),如果View没有子View就会调用View的dispatchTouchEvent,一般情况下最终会调用View的onTouchEvent来处理事件。
如果子View没有处理掉事件则会由下而上传递,点击事件传递到最底层的View时,如果该View的onTouchEvent返回true则表示处理并消费掉事件;如果返回false则表示该View不处理,继续向上传递,交给父View的onTouchEvent来处理,如果夫View的onTouchEvent返回true表示事件被消耗;如果返回false继续向上传递,如此反复。