安卓事件分发机制
原文:https://juejin.im/post/5eb3e0d6f265da7c002028cd
这次我来说说Android中的事件分发机制。
从点击屏幕开始,一系列事件将从“活动”到“去视图”传输到最里面的视图。视图或视图组的每个层将首先调用其dispatchTouchEvent方法,然后确定是否在当前层使用该事件。
view的事件分发
首先伪代码最后一段是在一本书上读到的,我觉得是最好的总结。
公共布尔dispatchttouchevent(MotionEvent事件){ 0
布尔值isConsume=false
if(IsView GrouP){ 0
if(on interceptouchevent(event)){ 0
isConsume=onTouchEvent(事件);
} else {
is consume=child . dispatchttouchevent(事件);
}
} else {
//isView
isConsume=onTouchEvent(事件);
}
return isConsume
}
如果复制代码当前处于视图组级别,它将确定onInterceptTouchEvent是否为真。如果是真的,则意味着事件将在此级别被消耗,不会被传递下去。然后,执行当前视图组的onTouchEvent方法。如果onInterceptTouchEvent为false,则意味着事件继续传递给下一级的dispatchTouchEvent方法,然后相同的代码逻辑继续,直到视图的最内层。
好吧,还没结束。在最内层,将直接执行onTouchEvent方法。在这个时候,观点是否有权拒绝消费事件?根据原则,视图作为底层,应该没有发言权。但是,根据公平正义的原则,观点也可以被拒绝。它可以在onTouchEvent方法中返回false,以指示他不想使用此事件。那么这个事件会如何处理呢?请参见以下伪代码:
public void handleTouchEvent(MotionEvent事件){ 0
if(!ontoucheevent(event)){ 0
getParent.onTouchEvent(事件);
}
}
复制代码如果视图的onTouchEvent方法返回false,那么它的父容器的onTouchEvent将被再次调用,如果父容器的onTouchEvent返回false,它将被移交给下一个更高的级别。直到顶层,也就是活动的本体事件被调用。
至此,消费过程完成。
然而,onTouch、onTouchEvent和onClick的调用关系是什么?
那我们再来看看另一段伪代码:
public void consumeEvent(motion event事件){ 0
if(SetTouchListener){ 0
onTouch();
if(!onTouch()){ 0
事件;
}
} else {
事件;
}
if(setnclicklistener){ 0
onClick();
}
}
复制代码当调用某个视图级别的onInterceptTouchEvent时,这意味着当前级别想要使用该事件。如果它的onTouchListener,将调用onTouch如果OnTouch的返回值返回true,则不会调用onTouch。如果返回false或未设置onTouchListener,将继续调用onTouchEvent。另一方面,如果设置了onClickListener,则通常会调用onClick方法。
下面是一个流程图来总结:
fig caption style=' margin : 10px 0px 0px;padding: 0pxoutline: 0px最大宽度:100%;盒子大小的:边框盒子!重要;溢出-wrap:断字!重要;l
ine-height: inherit; text-align: center; color: rgb(153, 153, 153); font-size: 0.7em;"在这里插入图片描述/figcaption
源码分析
一个触摸事件,首先是传到Activity层级,然后传到根view,通过一层层的viewgroup最终到底最里面一层的view,我们来一层层解析
Activity(dispatchTouchEvent)
直接上代码
//Activity.java
public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
public void onUserInteraction() {
}
复制代码这里可以看到,onUserInteraction方法是空的,主要是调用了getWindow().superDispatchTouchEvent(ev)方法,返回true,就代表事件消费了。返回false,就代表下层没人处理,那就直接到了activity的onTouchEvent方法,这点跟之前的消费传递也是吻合的。
继续看看superDispatchTouchEvent方法,然后就走到了PhoneWindow的superDispatchTouchEvent方法,以及DecorView的superDispatchTouchEvent,看看代码:
//PhoneWindow.java
private DecorView mDecor;
@Override
public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event);
}
//DecorView.java public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
复制代码这里可以看到,依次经过了PhoneWindow到达了DecorView,DecorView是activity的根view,也是setcontentView所设置的view的父view,它是继承自FrameLayout。所以这里super.dispatchTouchEvent(event)方法,其实就是走到了viewgroup的dispatchTouchEvent 方法。
ViewGroup(dispatchTouchEvent)
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (onFilterTouchEventForSecurity(ev)) {
// Check for interception,表示是否拦截的字段
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//FLAG_DISALLOW_INTERCEPT标志是通过requestDisallowInterceptTouchEvent设置 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;
}
//mFirstTouchTarget赋值
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget target == newTouchTarget) {
} else {
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
continue;
}
}
}
}
复制代码这里截取了部分关键的代码,首先是两个条件
-
actionMasked == MotionEvent.ACTION_DOWN
-
mFirstTouchTarget != null
如果满足了其中一个条件才会继续走下去,执行onInterceptTouchEvent方法等,否则就直接intercepted = true,表示拦截。
第一个条件很明显,就是表示当前事件位按下事件(ACTION_DOWN)
第二个条件是个字段,根据下面的代码可以得知,当后面有view消费掉事件的时候,这个mFirstTouchTarget字段就会赋值,否则就为空。
所以什么意思呢,当ACTION_DOWN事件时候,一定会执行到后面代码。当其他事件来的时候,要看当前viewgroup是否消费了事件,如果当前viewgroup已经消费了事件,没传到子view,那么mFirstTouchTarget字段就为空,所以就不会执行到后面的代码,就直接消费掉所有事件了。
这就符合了之前的所说的一种机制:
某个view一旦开始拦截,那么后续事件就全部就给它处理了,也不会执行onInterceptTouchEvent方法了
但是,两个条件满足了一个,就能执行到onInterceptTouchEvent了吗不一定,这里看到还有一个判断条件:disallowIntercept。这个字段是由requestDisallowInterceptTouchEvent方法设置的,后面我们会讲到,主要用于滑动冲突,意思就是子view告诉你不想让你拦截,那么你就不拦截了,直接返回false。
ok,继续看源码,之前的内容我们了解到,如果viewgroup不拦截事件,应该会传递给子view,那在哪里传的呢继续看看dispatchTouchEvent的代码:
if (!canceled !intercepted) {
final int childrenCount = mChildrenCount; if (newTouchTarget == null childrenCount != 0) { 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);
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
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;
}
resetCancelNextUpFlag(child);
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;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
}
}
复制代码这里可以看到,进行了一个子view的便利,其中,如果满足两个条件中的一个,就跳出。否则就执行dispatchTransformedTouchEvent方法。先看看这两个条件:
-
!child.canReceivePointerEvents()
-
!isTransformedTouchPointInView(x, y, child, null)
看名字是看不出啥了,直接看代码吧:
protected boolean canReceivePointerEvents() { return (mViewFlags VISIBILITY_MASK) == VISIBLE || getAnimation() != null;
}
protected boolean isTransformedTouchPointInView(float x, float y, View child, PointF outLocalPoint) {
final float[] point = getTempPoint();
point[0] = x;
point[1] = y;
transformPointToViewLocal(point, child);
final boolean isInView = child.pointInView(point[0], point[1]);
if (isInView outLocalPoint != null) {
outLocalPoint.set(point[0], point[1]);
}
return isInView;
}
复制代码哦,原来是这个意思。canReceivePointerEvents方法就代表view是不是可以接受点击事件,比如是不是在播放动画。而isTransformedTouchPointInView方法代表点击事件的坐标是不是在这个view的区域上面。
ok,如果条件都满足,就执行到dispatchTransformedTouchEvent方法了:
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;
}
}
复制代码这个方法大家应该都猜到了,其实就是执行了child.dispatchTouchEvent(event)。也就是下一层view的dispatchTouchEvent方法呗,开始事件的层级传递。
View(dispatchTouchEvent)
到view 层级的时候,自然就执行的view的dispatchTouchEvent,上代码
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
final int actionMasked = event.getActionMasked(); if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
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;
}
}
if (!result mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
// Clean up after nested scrolls if this is the end of a gesture;
// also cancel it if we tried an ACTION_DOWN but we didn't want the rest
// of the gesture.
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN !result)) {
stopNestedScroll();
}
return result;
}
复制代码这里可以看到,首先会判断li.mOnTouchListener != null,如果不为空,就会执行onTouch方法。
根据onTouch方法返回的结果,如果为false,result就为false,那么onTouchEvent才会执行。这个逻辑也是符合我们之前说的传递方式。
最后我们再看看view的onTouchEvent都做了什么事:
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) {
// 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 (!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)) {
performClickInternal();
}
}
}
}
mIgnoreNextUpEvent = false;
break;
}
return true;
}
复制代码从代码可以得知,如果设置了CLICKABLE或者LONG_CLICKABLE,那么这个view就会消费事件,并且执行performClickInternal方法,然后执行到performClick方法。这个performClick方法大家应该都很熟悉,就是触发点击的方法,其实内部就是执行了onClick方法。
private boolean performClickInternal() {
notifyAutofillManagerOnClick();
return performClick();
}
public boolean performClick() {
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;
}
return result;
}
复制代码至此,源代码也看的差不多了,内部其实有很多细节,这里也就不一一说明了,大家有空可以去研究下。
事件分发的应用(requestDisallowInterceptTouchEvent)
那既然学会了事件分发机制,我们实际工作中会怎么应用呢其实最常见的就是解决滑动冲突的问题。一般有两种解决办法:
-
一种是外部拦截:从父view端处理,根据情况决定事件是否分发到子view
-
一种是内部拦截:从子view端处理,根据情况决定是否阻止父view进行拦截,其中的关键就是requestDisallowInterceptTouchEvent方法。
第一种方法,其实就是在onInterceptTouchEvnet方法里面进行判断返回true还是返回false。
第二种方法,就是用到了requestDisallowInterceptTouchEvent方法,这个方法的意思就是让父view不要去拦截事件了,在dispatchTouchEvent方法里面就有这个标志位:FLAG_DISALLOW_INTERCEPT,如果disallowIntercept字段为true,就不会去执行onInterceptTouchEvent方法,而是返回false,不拦截事件。
上代码:
//外部拦截法:父view.java
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
//父view拦截条件
boolean parentCanIntercept;
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
if (parentCanIntercept) {
intercepted = true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
}
return intercepted;
}
复制代码外部拦截很简单,就是判断条件,然后决定是否进行拦截。
//父view.java
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) { if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}
//子view.java
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
//父view拦截条件
boolean parentCanIntercept;
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if (parentCanIntercept) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
}
return super.dispatchTouchEvent(event);
}
复制代码感觉内部拦截有点复杂呀,还要重写父view的方法,这里分析下,为什么要去这么写:
-
父view ACTION_DOWN的时候,不能拦截,因为如果拦截,那么后续事件也就跟子view无关了
-
父view 其他事件的时候,要返回true,表示拦截。因为onInterceptTouchEvent方法的调用是被FLAG_DISALLOW_INTERCEPT标志位所控制,所以子view需要父view拦截的时候,才会走到这个onInterceptTouchEvent方法中来,那么这时候要保证方法中一定是要拦截的。
至此,事件的分发机制也就说的差不多了。
文末
您的点赞收藏就是对我最大的鼓励!
欢迎关注我,分享Android干货,交流Android技术。
对文章有何见解,或者有何技术问题,欢迎在评论区一起留言讨论!
内容来源网络,如有侵权,联系删除,本文地址:https://www.230890.com/zhan/136275.html