源码探索系列12---关于事件分发机制
关于View的事件分发,实质就是关于MotionEvent时间的分发
再简单点说就是通过一堆判断,最后决定这个MotionEvent给谁用的问题。
-
三巨头
分发过程中有主要涉及到三个人:
dispatchTouchEvent()
,onInterceptTouchEvent()
,onTouchEvent()
这三者的关系如下public boolean dispatchTouchEvent(MotionEvent ev) { boolean belongToMe=false; if(onInterceptTouchEvent(ev)){ belongToMe=onTouchEvent(ev); }else{ belongToMe=child.dispatchTouchEvent(ev); } return belongToMe; }
但有点击事件产生的时候, dispatchTouchEvent被调用,然后给onInterceptTouchEvent看下要不要拦截,拦截下来的就调用onTouchEvent处理下。如果不拦截,就传递给子view去做,重复这个流程。
不过需要说的是,这个onInterceptTouchEvent()是ViewGroup的,View里面没有这个。
另外这个事件还受OnTouchListener
这个的影响,如果我们设置了监听,且他的onTouch()
事件返回真,那么事件是不会发到onTouchEvent
里面去的。即前者有更高的优先级。
-
传递顺序
事件的传递顺序是从Activity传起,最后到我们的各种View里面去的,即使父传给子的关系。
如果传到底部的onTouchEvent
也没有人出来处理这个MotionEvent的话,最终这个事件会像递归一样,跑回来Activity,然后他的onTouchEvent
函数被调用.
图1
即:MotionEvent---->Activity->widnow->DecorView->ViewGroup->View->ViewGroup->DecorView->window->Activity;
就像下面这样的:
Note left of Activity: 我要去给Activity \n 送点击事件咯
Note over Activity: 啊哈,我收到了MotionEvent
Activity->Widnow:dispatchTouchEvent
Widnow->DecorView:dispatchTouchEvent
DecorView->ViewGroup:dispatchTouchEvent
ViewGroup->View:dispatchTouchEvent
View->ViewGroup:搞不定啊
ViewGroup->DecorView:搞不定啊
DecorView->Widnow:搞不定啊
Widnow->Activity:搞不定啊
Activity->Activity:onTouchEvent()
起航
API:23
我们看下我们的Activity的处理
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
他会调用window的时间分发,把时间分发下去,如果返回的是false,再调用回自己的onTouchEvent()
。
这里的getWindow()返回的是Windows类,一个抽象类,他的具体实现是PhoneWidnow
看下我们的PhoneWindow里面写的内容:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
跑去了mDecor即DecorView里面去了
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker
这DecorView是PhoneWindow里面的一个内部类,继承FrameLayout。
我们在Activity里面通过setContentView(R.layout.activity_main);
来设置我们的界面,而这个函数生成的View,即我们的界面是他的子View。
所以他的分发事件我们看下,是直接调用super的。这样再去看顶部的那张图1
,ViewGroup下面一堆的View。就可以知道事件最后会分发到我们的View去。
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
好了,我们继续看下那个ViewGroup里面的内容
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
//1. 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();
}
//2. 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;
}
//3.分发事件
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = customOrder
? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(childIndex);
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
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;
}
}
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
...
}
...
return handled;
}
这个过程真的挺长的,一百多行,不过在看多了AMS里面的内容,这个也就一般般的感觉了。
我们慢慢说起,
-
首先第一步。
我们看下第二个函数里面的内容private void resetTouchState() { clearTouchTargets(); resetCancelNextUpFlag(this); mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; mNestedScrollAxes = SCROLL_AXIS_NONE; }
这里他会去设置一个 FLAG_DISALLOW_INTERCEPT
的标记,关于他,真的是看得好累啊。
下次补充。可以看下这篇文章
-
看下拦截里面的内容
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; }
他判断这个事件是否为Action_Down
或者mFirstTouchTarget != null
来进一步选择是否要拦截。
前面的判断条件好理解,后面这个mFirstTouchTarget
表示的意思是, 当事件由子View成功处理后,mFirstTouchTarget会被赋值并指向childView,就是说,如果这时间被childView处理了,这标记就不是空,因此ViewGroup不再做拦截,并且事件将继续默认都交给这个ChildView。因此这个onInterceptTouchEvent()
并不是每次都回被调用,虽然我开头那样写,看起来像每次都要拦截的样子。
-
事件分发
在事件分发部分的内容,他先看下这个Child是在播动画,或者这个child的区域在不再这个Event的范围内的,不在范围就不发给这个child。if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { continue; }
如果没播而且在这个范围内,就发送事件给她
dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)
具体的内容是调用他的dispatchTouchEvent()
,就像下面代码一样。
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);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
...
}
如果这个处理返回的handled是 true,那么我们看到下面的内容:
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
他标记新的touchTarGet,然后退出循环,另外在addTouchTarget()函数里面
private TouchTarget addTouchTarget(View child, int pointerIdBits) {
TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
我们看到了mFirstTouchTarget = target
这句话,前面我们在拦截的时候,有用到这个作为一个判断条件!判断是否要对事件拦截。
对于循环一圈分发完后,如果都没人处理的话,即没有一个ChildView或者ChildView返回了false的情况。
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
这时候我们的ViewGroup就自己处理了。这个dispatchTransformedTouchEvent()
我们前面有提到,因child参数被设成null,我们知道他会调用 handled = super.dispatchTouchEvent(event);
这句。
这句跑去调用的就是View的dispatchTouchEvent()去了。
小结:
这里我们可以做个简单的总结,当我们ViewGroup在分发事件的过程中,如果自己的childView没一个处理好了事件,那么这事件会从ViewGroup转到View去分发。
前进 ------ View的事件处理
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
...
if (onFilterTouchEventForSecurity(event)) {
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;
}
}
...
return result;
}
我们截取重要部分。
他看下我们有没设置onTouchListener
,如果有调用,并且如果返回的是true,那么就结束了,不会再去调用onTouchEvent
了,没有的话才去调用onTouchEvent。
这个onTouchevent还是挺长的,基本都是对event的Action()做处理,为何不分割成几个小函数呢,不就容易看多了。
哎,这里弄个大概的样子,方便掌握整体,清楚顺序逻辑。
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
...
break;
case MotionEvent.ACTION_DOWN:
...
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
break;
case MotionEvent.ACTION_MOVE:
...
break;
}
return true;
}
return false;
}
我们来看下开头的
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
这里说的内容是,当我们设置我们的View是Disabled的状态,不过还ClickAble的话,就消耗掉事件。
这里补充一点:
我们平常的LONG_CLICKABLE默认是false,而CLICKABLE就分情况了,例如那个Textview就默认是false。Button默认是true。有时我习惯用Textview来替代Button做一些事,所以老要加这个熟悉的设置....
接下来就到了一句有趣的了,如果我们给View设置了代理,就调用我们的代理 onTouchEvent()
去干活。
这么久都没有设置过view.setTouchDelegate()
有点意思,查了下,可以用来扩大触摸点击区域
接着看下面的内容
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
...
}
return true;
}
return false;
我们的View有一个特效,只要是可点击的状态,不管你是不是Enable,都能消耗掉MotionEvent!
我们看下其中的一个case情况
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
boolean focusTaken = false;
...
if (!mHasPerformedLongPress) {
// 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)) {
performClick();
}
}
}
...
removeTapCallback();
}
break;
这里面说了一件重要的事,当我们申起手的时候,会触发点击事件。
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;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}
好啦,到这里,我们的事件基本就处理完了,从Activity到最后我们的View的过程。
不过还是有一些内容没说,下次有空记得再补充吧!
后记
在这个过程看到了关于Touch事件的委托。
Window类的具体实现的PhoneWindow,和里面的DecorView.
重要的是其中我们熟悉的每次设置界面都调用的函数,看来下次的目标就是PhoeWindow咯。
参考资料:
FLAG_DISALLOW_INTERCEPT: 探究requestDisallowInterceptTouchEvent失效的原因