View的事件分发简单说明
前言
本文是阅读ViewGroup事件分发后的总结,仅供自己复习查阅使用,如果能够帮到您,那我很荣幸。PS:本文只分析单点触摸的情况
事件的类型
ACTION_DOWN:手指初次接触到屏幕时触发
ACTION_MOVE:手指在屏幕上滑动时触发,会多次触发
ACTION_UP:手指在屏幕上抬起时触发
ACTION_CANCEL:事件被上层拦截时触发
关于View的一点说明
View:例如ImageView,TextView等,不会有子View的存在,只能处理触摸事件而不能分发事件。
ViewGroup:例如LinearLayout、ConstraintLayout,可能存在子View,本身即可以分发事件也可以处理事件,在分发事件时如果所有的子View都不能处理,则需要自己进行事件处理,如果自身也无法处理的话,则通知父控件自身无法处理该触摸事件。
ViewGroup的事件分发说明
一个点击事件最先由activity进行分发,在传到ViewGroup,最后传到View。
事件分发顺序.png首先看一下Activity的dispatchTouchEvent方法吧
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
//设置屏保,空实现
onUserInteraction();
}
//调用window的superDispatchTouchEvent方法。
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
代码很清晰,如果window的superDispatchTouchEvent返回true则,则该方法返回true,否则调用Activity的onTouchEvent方法的结果作为返回。
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
Activity的onTouchEvent中会判断点击区域是否在window的边界上,如果是返回true,否则返回false。现在在回过头来getWindow().superDispatchTouchEvent(ev),由于window是一个抽象类且只有一个实现类PhoneWindow,所以我们直接看PhoneWindow.superDispatchTouchEvent()即可。
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
PhoneWindow直接调用了DecorView的superDispatchTouchEvent方法。
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
DecorView会调用ViewGroup的dispatchTouchEvent方法,这也是我们需要关注的重点之一。
现在总结一下Activity到ViewGroup的事件分发顺序,流程如下图
流程总结.png
现在看一下ViewGroup的dispatchTouchEvent吧
public boolean dispatchTouchEvent(MotionEvent ev) {
......
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
......
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//获取子View是否允许父View拦截事件,子控件调用requestDisallowInterceptTouchEvent来通知父控件是否可以拦截事件,默认为false
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
//子View不阻拦父View
if (!disallowIntercept) {
//调用onInterceptTouchEvent方法判断是否拦截此事件
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
intercepted = true;
}
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
final boolean isMouseEvent = ev.getSource() == InputDevice.SOURCE_MOUSE;
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0
&& !isMouseEvent;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
//不是取消事件且父View不进行拦截
if (!canceled && !intercepted) {
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
......
//获取子View数量
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
.....
//将子View按照一定顺序排列成集合
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
//遍历集合
for (int i = childrenCount - 1; i >= 0; i--) {
//获取指定位置
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
//获取指定位置的View
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
.....
//分发指定事件到该view,如果该View能够消费事件则跳出循环,否则继续寻找下一个子View进行处理
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();
}
.......
}
}
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
//没有子View处理该事件,则由该ViewGroup尝试处理
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
//不分析多点触摸
.......
}
.....
}
.....
return handled;
}
总结一下:首先询问父View是否拦截该事件,如果父View拦截事件的话则由父View调用dispatchTransformedTouchEvent方法进行处理,子View可以通过调用requestDisallowInterceptTouchEvent方法阻拦父View拦截该事件,如果父View不拦截则询问子View是否能够消费该事件(调用dispatchTransformedTouchEvent方法),如果没有子View能够消费的话,则仍交由父View尝试消费该事件(调用dispatchTransformedTouchEvent方法)。有代码可以看出询问子View是否消耗该事件和父View尝试消耗该事件都会调用ViewGroup.dispatchTransformedTouchEvent这个方法。再看一下ViewGroup.dispatchTransformedTouchEvent这个方法
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
//这里需要注意如果是父View尝试自己处理则child, 为null,是子View则不为null
final int oldAction = event.getAction();
//不是取消事件
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
//父View(ViewGroup),调用View的dispatchTouchEvent方法
handled = super.dispatchTouchEvent(event);
} else {
//子View
//为ViewGroup 则会调用ViewGroup.dispatchTouchEvent,形成嵌套调用
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
.....
return handled;
}
通过这个方法我们能够看出ViewGroup的diapatchTouchEvent方法存在嵌套调用,这是因为父控件的子View可能是View也有可能是ViewGroup,如果是ViewGroup则和父View一样进行事件的分发。如此则形成了嵌套调用。至此ViewGroup的事件分发分析完毕。
View的事件处理
public boolean dispatchTouchEvent(MotionEvent event) {
.......
boolean result = false;
.....
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
//如果该控件是enable状态且设置了触摸回调则会调用触摸回调,如果触摸回调返回了true,则消费了该事件
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//调用onTouchEvent为true表示子View消费该事件,否则不消费该事件
if (!result && onTouchEvent(event)) {
result = true;
}
}
......
return result;
}
总结一下,如果该View设置了onTouchEventListener且该View处于Enable状态则执行回调的onTouchEvent方法,如果返回true则消费该事件,返回false则不消费该事件。如果该View不处于Enable状态,没有设置回调或者回调返回false,则调用onToucheEvent尝试进行事件的消费。最后看一下onToucheEvent方法,这也是View事件分发的一个重点需要分析的方法。
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) {
switch (action) {
//处理抬起事件,主要看PerformClick
case MotionEvent.ACTION_UP:
....
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) {
//返回一个runnable,一会重点分析
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
......
break;
//处理按下事件
case MotionEvent.ACTION_DOWN:
.....
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
......
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:
......
break;
}
// 若该控件可点击,就一定返回true,消费该事件
return true;
}
// 若该控件不可点击,就一定返回false,不消费该事件
return false;
}
再来看一下performClick
private final class PerformClick implements Runnable {
@Override
public void run() {
recordGestureClassification(TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SINGLE_TAP);
//调用performClickInternal
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();
//调用performClick方法
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;
//如果设置了OnClickListener则执行回调,方法返回true
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;
}
通过这段源码分析大家应该了解setOnClickListener方法的回调时机了。特别需要注意的是如果设置了setOnTouchListener并回调返回了true则不会执行setOnClickListener的回调的,View 的onTouchEvent 方法默认都会消费掉事件(返回true),除非它是不可点击的(clickable和longClickable同时为false),View的longClickable默认为false,clickable需要区分情况,如Button的clickable默认为true,而TextView的clickable默认为false,如果该View不消费该事件,则由其父组件分发给其他的子View或者自己尝试处理。。View的diapatchEvent调用顺序如下:
总结
以上就是View事件分发的相关内容。掌握了相关内容才能比较容易解决事件冲突打下良好的基础