一、事件分发
有一段时间没更博客了,想象我写博客的初衷主要有2个原因:
- 如果有人看到了,从中找到一点能帮助到他的东西,这样最好不过了。
- 写给自己看。我相信整理一遍写一遍对自己的理解和认知有很大的帮助。
最近闭关修炼,哈哈哈,说的有点魔幻。主要最近对安卓的看法有了一点新的看法,看了一个公众号的推送,有个人将安卓的学习和进阶分为了5个部分:
- UI
- 性能
- NDk
- 架构
- 其他
这样分是基于项目而言的,不同类型不同定位的项目这5个部分所占的比重也是不一样的。比如有些项目就要UI好看,用户看你UI好看就会极大的提升兴趣。
当然我最近学习这些东西不是因为看了这个公众号然后就去学UI相关的东西,而是我在学的过程中发现了这个公众号,我就觉得这样分也比较合理,就顺带提了一下。
回归主题:今天我要说的是我学习的第一个东西:
事件分发
之前一直学习这块,但是也是含含糊糊,这次静下心来好好的梳理了一遍,算是掌握了7-8分吧,但还有几个问题没弄明白,百度了也没百到,问了大神也没得到我想要的答案,然后就不了了之了。这些问题我记在心中,我相信在不远的将来肯定会遇到我的师傅,他肯定会为我传道解惑。
好了下面开始我的表演:
我之前只知道事件分发是从父view传递到子view,但是我不知道起点是那里。最近看了一篇博客,链接忘记了,我感到很羞耻,其实以我的水平我应该能想到是Activity,但是之前我没想到,但我现在知道了,哈哈哈。
为什么说是Activity呢,因为Activity是负责与用户交互的啊,那么我的手势操作不应该被Activity所捕捉然后进行下一步吗?
那么Activity是怎么捕捉到我们的手势的呢?这个我不知道,跟硬件或底层实现有关了,我只知道最终会从来到Activity的dispatchTouchEvent()方法:
1. public boolean dispatchTouchEvent(MotionEvent ev) {
2.
3. //如果是按下状态就调用onUserInteraction()方法,onUserInteraction()方法
4. //是个空的方法, 我们直接跳过这里看下面的实现
5. if (ev.getAction() == MotionEvent.ACTION_DOWN) {
6. onUserInteraction();
7. }
8.
9. if (getWindow().superDispatchTouchEvent(ev)) {
10. return true;
11. }
12.
13. //getWindow().superDispatchTouchEvent(ev)返回false,这个事件就交给Activity
14. //来处理, Activity的onTouchEvent()方法直接返回了false
15. return onTouchEvent(ev);
16. }
根据注释我们可以看到,第9行如果返回true就说明有view能处理我们的事件,那么就不会调用Activity的onTouchEvent()方法;如果第9行返回false,那么就说明没有view能响应我们的事件,那么事件就交给Activity处理。
我们进入第9行的getWindow().superDispatchTouchEvent(ev)方法内部:
它是调用Window类的superDispatchTouchEvent(ev)方法,但是我们进入Window类发现这个方法是个抽象方法,而Window也是一个抽象类。Window就是我们的窗口,就是最顶层的一个承载窗体view的东西。他只有一个子类PhoneWindow,我们直接进入PhoneWindow类的superDispatchTouchEvent(ev)方法:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
mDecor是什么呢?他是DecorView的实例,这个DecorView是一个继承FrameLayout的的ViewGroup,在之前的版本中,它是作为PhoneWindow的内部类的形式出现,在之后的版本中它被独立出来作为一个单独的类。他的作用就是存放我们在Activity中设置的布局View,也就是说我们在Activity中通过setContentView(int resid)方法设置的布局,最终会绑定到DecorView上。看一张图:
图中DecorView中还有一个LinearLayout,因为要存放2个FrameLayout:一个是顶部的ActionBar,一个是我们给activity设置的布局。
好了,我们继续看DecorView类中的superDispatchTouchEvent(event)方法:
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
调用的是父类的dispatchTouchEvent(event),我们知道他是继承FrameLayout的,所以我们去ViewGroup中看dispatchTouchEvent(event)方法:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
// If the event targets the accessibility focused view and this is it, start
// normal event dispatch. Maybe a descendant is what will handle the click.
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// 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();
}
// 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;
}
// If intercepted, start normal event dispatch. Also if there is already
// a view that is handling the gesture, do normal event dispatch.
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
// Check for cancelation.
//cancle检查
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed.
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
// 1、这个if是在没有拦截也没有取消事件时执行的
if (!canceled && !intercepted) {
// If the event is targeting accessiiblity focus we give it to the
// view that has accessibility focus and if it does not handle it
// we clear the flag and dispatch the event to all children as usual.
// We are looking up the accessibility focused host to avoid keeping
// state since these events are very rare.
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
//从down开始
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
//移除之前点击的这个点的位置的一些操作
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
//拿到child
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
//这句话的意思就是根据用户的触摸位置找到相应的子view,添加进集合中,然后根据子view的count数反向遍历,为什么要反向遍历,因为view是一层一层覆盖的啊(比如Relativelayout)。然后我们找到最上层的子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);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
//这个newTouchTarget就是在链表里找到的接收事件的view对应的newTouchTarget ,这个newTouchTarget 应该包含了一些信息吧,如果不=null,说明这个view已经接收到事件了
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方法里面就是去调用该子view的dispatchtouchevent方法,下面会贴源码继续分析
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
//如果这个子view的dispatchtouchevent返回true ,应该这一套下来有一个不管是view还是ViewGroup能处理事件才会返回true
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();
}
//没找到接收事件的child
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
//2、接下来的if else逻辑是当拦截了或者是cancle事件时执行。
// 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 {
//这里应该是cancle事件吧
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
// Update list of touch targets for pointer up or cancel, if needed.
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
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;
}
// Calculate the number of pointers to deliver.
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
// If for some reason we ended up in an inconsistent state where it looks like we
// might produce a motion event with no pointers in it, then drop the event.
if (newPointerIdBits == 0) {
return false;
}
// If the number of pointers is the same and we don't need to perform any fancy
// irreversible transformations, then we can reuse the motion event for this
// dispatch as long as we are careful to revert any changes we make.
// Otherwise we need to make a copy.
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
transformedEvent = MotionEvent.obtain(event);
} else {
transformedEvent = event.split(newPointerIdBits);
}
// Perform any necessary transformations and dispatch.
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);
}
// Done.
transformedEvent.recycle();
return handled;
}
新版API(我这是API26)做了一些改动,有些东西提出去做了,有些东西整合进来了。分析起来有点吃力,之前看的是老版本的API,简单一些,但主体都差不多。
总结一下:对于ViewGroup的dispatchTouchEvent方法:
首先看down是否拦截,如果没拦截,根据我们点击的位置找到子view,然后调用它的dispatchTouchEvent方法,如果这个子view的dispatchTouchEvent方法返回true,那么ViewGroup也返回true。如果子viewdispatchTouchEvent返回false,代表不能处理事件,那么对于ViewGroup而言就要自己处理了,他会先判断是是cancle还是up事件,如果是cancle或者up得话,就将拦截事件的标志置为默认的false(可以理解当我们设置了拦截事件后,我们抬起手指或取消touch的时候会将这个标志置为false。所以说disallowIntercept在我们每次down的时候都是false。)。然后因为ViewGroup自己处理,就要调用ViewGroup的super.dispatchtouchevent方法,如果是up或者cancle,那么就传入up或cancle事件,并返回。下面还有,下面的逻辑down事件不会走,move和up可能会走到这里,如果move或up拦截了事件的话,那么就将cancle事件交给子view,ViewGroup返回true,表示消费了事件。
如果调用,那么最终会调用到我们想要点击的子view,执行他的dispatchtouchevent方法。然后根据返回情况向上返回,就是ViewGroup的dispatchtouchevent返回值是根据其子view的dispatchtouchevent返回值而定的。
有一张图:
view的dispatchtouchevent方法:
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
如果view设置了touchlistener,enable,而且ontouch方法返回了true的话,就不会执行ontouchevent了,那么点击事件就无法执行了。否则就执行ontouchevent方法。
我们看onTouchEvent方法:
1. public boolean onTouchEvent(MotionEvent event) {
2. final int viewFlags = mViewFlags;
3.
4. if ((viewFlags & ENABLED_MASK) == DISABLED) {
5. return (((viewFlags & CLICKABLE) == CLICKABLE ||
6. (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
7. }
8.
9. //如果设置了Touch代理,就交给代理来处理,mTouchDelegate默认是null
10. if (mTouchDelegate != null) {
11. if (mTouchDelegate.onTouchEvent(event)) {
12. return true;
13. }
14. }
15.
16. //如果View是clickable或者longClickable的onTouchEvent就返回true, 否则返回false
17. if (((viewFlags & CLICKABLE) == CLICKABLE ||
18. (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
19. switch (event.getAction()) {
20. case MotionEvent.ACTION_UP:
21. boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
22. if ((mPrivateFlags & PRESSED) != 0 || prepressed) {
23. boolean focusTaken = false;
24. if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
25. focusTaken = requestFocus();
26. }
27.
28. if (!mHasPerformedLongPress) {
29. removeLongPressCallback();
30.
31. if (!focusTaken) {
32. if (mPerformClick == null) {
33. mPerformClick = new PerformClick();
34. }
35. if (!post(mPerformClick)) {
36. performClick();
37. }
38. }
39. }
40.
41. if (mUnsetPressedState == null) {
42. mUnsetPressedState = new UnsetPressedState();
43. }
44.
45. if (prepressed) {
46. mPrivateFlags |= PRESSED;
47. refreshDrawableState();
48. postDelayed(mUnsetPressedState,
49. ViewConfiguration.getPressedStateDuration());
50. } else if (!post(mUnsetPressedState)) {
51. mUnsetPressedState.run();
52. }
53. removeTapCallback();
54. }
55. break;
56.
57. case MotionEvent.ACTION_DOWN:
58. if (mPendingCheckForTap == null) {
59. mPendingCheckForTap = new CheckForTap();
60. }
61. mPrivateFlags |= PREPRESSED;
62. mHasPerformedLongPress = false;
63. postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
64. break;
65.
66. case MotionEvent.ACTION_CANCEL:
67. mPrivateFlags &= ~PRESSED;
68. refreshDrawableState();
69. removeTapCallback();
70. break;
71.
72. case MotionEvent.ACTION_MOVE:
73. final int x = (int) event.getX();
74. final int y = (int) event.getY();
75.
76. //当手指在View上面滑动超过View的边界,
77. int slop = mTouchSlop;
78. if ((x < 0 - slop) || (x >= getWidth() + slop) ||
79. (y < 0 - slop) || (y >= getHeight() + slop)) {
80. // Outside button
81. removeTapCallback();
82. if ((mPrivateFlags & PRESSED) != 0) {
83. removeLongPressCallback();
84.
85. mPrivateFlags &= ~PRESSED;
86. refreshDrawableState();
87. }
88. }
89. break;
90. }
91. return true;
92. }
93.
94. return false;
95. }
这个就是处理点击事件了,长点击在down里面执行,短点击在up里执行。
down里面会发送一个post延迟消息,用来判断是否是长点击事件即是否达到系统设置的长点击事件时间,而且要设置了onLongClickListener的话,达到事件后就去执行长点击事件,如果onLongClick返回了true,那么会有一个标志mHasPerformedLongPress被置为true,相当于表示事件被消费了。在up事件中if会对这个标志取反,如果被消费了,那么就不会进入if里面,那么点击事件不会得到执行了。但是up这个时间还是会执行的,只不过不执行点击事件了。
事件分发应该就是这样大致的流程,有几点注意:
- 我们重写拦截事件的方法去拦截事件,默认拦截的是down事件。如果想拦截其他事件,那么就需要在方法里面对ev进行判断然后进行拦截操作。
- 如果在ViewGroup中对down拦截了,那么就会调用其的super.dispatchtouchevent方法,也就是view的,但是ViewGroup是没有设置ontouchlistener的,而且默认不能点击,所以根据情况自己去做相应的处理,默认不做处理的话就直接返回false了。
- 如果对move或者up事件进行拦截的话,子view得不到这些事件,点击事件无法执行,但是长点击事件是可以执行的,因为长点击是在down里面做的,这种情况ViewGroup就会将cancle交给子view,并直接返回true。
就到这里为止吧,以后项目中遇到什么问题再来补充,学习的过程也是一个不断纠正错误的过程。
下一篇我要写从启动Activity到用户可以点击view的这个过程发生了什么。
上一篇: 2019华为机试