Android事件分发流程梳理
Android事件分发流程梳理
View点击事件
事件流程:DOWN --> MOVE(许多个) --> UP or Cancel(非人为)
事件分发的本质:将点击事件MotionEvent传递到具体某个View进行处理的流程
传递事件的对象:Activity——ViewGroup——View
涉及方法:
方法 | 作用 |
---|---|
dispatchTouchEvent | 传递点击事件 |
onTouchEvent | 处理点击事件 |
onInterceptTouchEvent | 判断是否拦截某个事件 |
事件分发流程
Activity事件分发流程
// Activity
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
// mWindow is PhoneWindow
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
// 如果没人处理, 那就Activity就自己处理
return onTouchEvent(ev);
}
// PhoneWindow
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
// DecorView : ViewGroup
@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);
}
// ViewGroup.dispatchTouchEvent 做了很多事,这里先不说
// 如果上述步骤中有一步返回false,则进入Activity.onTouchEvent中
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
总结一下就是Activity负责将事件经由Window与DecorView传递给根布局ViewGroup
需要注意的是Activity与DecorView和Window的关系是: Activity包含Window Window包含DecorView。Activity在Create时创建Window,在setContentView方法中创建DecorView并添加自定义布局到DecorView中。DecorView比较熟悉了,每次打开AS Layout Inspector时查看Component Tree顶层就是DecorView
ViewGroup事件分发流程
省略以及内联了部分代码
// ViewGroup
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// ViewGroup每次事件分发时,都要调用onInterceptTouchEvent询问是否拦截事件
// disallowIntercept:是否禁用事件拦截的功能(默认是false 不禁用),可通过调用requestDisallowInterceptTouchEvent()修改
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
// 允许Intercept
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
// ...
// 仅当事件未被取消且事件未被拦截时进行子View的事件传递
if (!canceled && !intercepted) {
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// 只有View可接受事件并且点击事件坐标落在View的区域内才可以
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
// 将具体事件交给每个Child View去处理,返回true代表view消费了该事件
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// 没有可处理的child,则只能自己处理
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
}
}
}
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;
}
// 只有VISIBLE的View才能接受事件,INVISIBLE不能处理事件
protected boolean canReceivePointerEvents() {
return (mViewFlags & VISIBILITY_MASK) == VISIBLE || getAnimation() != null;
}
// 真正进行child view事件分发流程的方法
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
// ...
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
transformedEvent.recycle();
return handled;
}
理解onInterceptTouchEvent: 个人理解是在ViewGroup中对事件进行一些预先处理和判断,判断是否要传递给Child
核心过程是:ViewGroup判断是否拦截以及寻找可传递事件的子View。并且需要注意,事件分发只会分发给一个Child,如果同一层ViewGroup下有两个有重叠区域的View,则点击该重叠区域,则只会按照child中排序对第一个View进行点击事件处理!
![ViewGroup事件传递](https://upload-images.jianshu.io/upload_images/944365-74008ffc11f4f3dd.png?imageMogr2/auto-orient/strip|imageView2/2/w/1000/format/webp)
![方法调用说明图](https://upload-images.jianshu.io/upload_images/944365-0ada946099db0d78.png?imageMogr2/auto-orient/strip|imageView2/2/w/1000/format/webp)
View事件分发流程
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
ListenerInfo li = mListenerInfo;
// 是否ENABLE且实现OnTouchListener, 调用onTouch方法
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
// 执行onTouchEvent方法
if (!result && onTouchEvent(event)) {
result = true;
}
}
View中onTouch与onTouchEvent方法执行顺序:
View中如果实现了OnTouchListener接口且返回为true时,就不会再调用onTouchEvent方法,但如果返回为false,则会继续执行onTouchEvent方法
![img](https://upload-images.jianshu.io/upload_images/944365-ba4e3c87a3563122.png?imageMogr2/auto-orient/strip|imageView2/2/w/1000/format/webp)
Recyclerview滑动嵌套冲突
根源:在Dragging时,父级RecyclerView onInterceptTouchEvent中返回true,导致父级RecyclerView不分发该事件给子View并自行处理了该事件
解决办法:
- 自定义父RecyclerView并重写onInterceptTouchEvent()方法
- 重写子布局的dispatchTouchEvent()方法,调用getParent().requestDisallowInterceptTouchEvent(true),通知通知父层ViewGroup不要拦截点击事件
- 实现子布局OnTouchListener接口,在ACTION_DOWN时请求父布局不拦截事件:getParent().requestDisallowInterceptTouchEvent(true)
2和3的做法是可行的,因为Recyclerview中仅当state为dragging时才进行事件拦截,ACTION_DOWN与UP均不会被拦截,因此事件可以传递给子View,从而调用requestDisallowInterceptTouchEvent方法