1.前言
事件分发这个东西嘛,大家一直都在讲,但总有人觉得吃不透。为什么呢?因为事件分发是多维的,有好多条思维分岔路口,而文章基本上只能用一维的方式从左到右,从上到下进行表达,所以基本不可能让普通智力的人从入门到精通。我们所要做的,就是踏踏实实打开源码,自己多琢磨,多整理。才能彻底理解这些多维的知识点。
下面内容请配合源码食用!不然基本上索然无味!
2.Touch与Click的前生今世
首先,我们先来做点前戏,搞清楚setOnTouchListener
、setOnClickListener
以及onTouchEvent
之间的关系。
2.1 setOnTouchListener
因为这一系列操作都是针对View的,所以我们直接看其源码,精准定位到dispatchTouchEvent()
方法。
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
...
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;
}
这段代码非常简单,直接将一切都暴露了出来。
result
变量十分关键,它是用来控制Touch与Click执行流程的。最开始result
为false,如果我们通过setOnTouchListener()
为某个View设置了touch监听,并且在监听的onTouch()
方法中返回true,那么result
变量就会被赋值为true,此时dispatchTouchEvent()
执行完毕,就不会执行接下来View本身的onTouchEvent()
方法。
2.2 onTouchEvent
相反,如果我们没有为View设置touch监听,或者设置了touch监听但是在监听的onTouch()
方法中返回false,那么result
依旧为false,就会执行View本身的onTouchEvent()
方法。我们来看看onTouchEvent()
做了什么,由于只是热身运动,所以只贴出了与其有关的部分代码。
public boolean onTouchEvent(MotionEvent event) {
...
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
...
if (!post(mPerformClick)) {
performClick();
}
}
}
...
}
可以看到,在View本身的onTouchEvent()
方法中,先去判断了该View是否可以被点击,接着判断触摸事件的类型,如果是ACTION_UP
类型,则执行performClick()
方法。
2.3 setOnClickListener
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;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}
显而易见,如果通过setOnClickListener
为当前View设置了Click监听,此时就会去执行监听中onClick()
方法。
2.4 小结
到此为止,前戏就算结束了。我们总结下,setOnTouchListener
与setOnClickListener
是程序员可以设置的,而onTouchEvent
是View本身的方法,在onTouchEvent
中会去执行setOnClickListener
中设置的OnClick
方法。而在View的dispatchTouchEvent()
中,首先会去判断是否设置了OnTouchListener
并且其OnTouch
方法返回为true,如果是,则不会执行View本身的onTouchEvent
方法,如果不是,则会执行onTouchEvent
进而执行OnClick
方法。
我个人是这样记住他们的关系的:Touch是触摸,Click是点击,从逻辑上来说,触摸包含了点击。所以如果设置了触摸的监听,那么其必定包含点击,于是点击的监听也就没什么必要了。
3.事件分发
3.1 事件分发的开始
下面进入正题,在使用安卓手机时,我们用手指触摸了屏幕,物理设备就会一层层将触摸事件传递出来,这是底层的活儿,我们暂且不去了解。属于Android开发的故事,从Activity的dispatchTouchEvent()
方法开始。请注意,这是Activity的dispatchTouchEvent()
,不要和View的dispatchTouchEvent()
混淆起来。
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
分析一波,首先判断触摸事件,如果是ACTION_DOWN
,则调用onUserInteraction()
,这是一个空方法,专门用来让用户重写的,可以用于在事件发生前做一些操作。
接着getWindow().superDispatchTouchEvent(ev)
就比较重要了。一路跟来的同学肯定知道Activity中的Window就是PhoneWindw,不知道的传送门在这里。我们直接看其superDispatchTouchEvent
方法。
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
显而易见,这里调用了DecorView中的superDispatchTouchEvent
方法
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
DecorView是PhoneWindow的内部类,我们去看看他的父类是谁。
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker
可见,FrameLayout 是DecorView的父类,所以就会调用FrameLayout的dispatchTouchEvent
方法,遗憾的是,FrameLayout并没有这个方法,所以还要去找FrameLayout 的父类ViewGroup。ViewGroup中的dispatchTouchEvent
是本篇最大的高潮,我们下一节专门来讲。在此,我们回到Activity的dispatchTouchEvent
方法中
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
看最后一行代码——年轻的程序员啊,请记住,只要外层ViewGroup的dispatchTouchEvent
返回为true,那么就代表事件被消耗了,此时连Activity中的onTouchEvent
都不会被执行!
3.2 ViewGroup.dispatchTouchEvent
3.2.1 事件重置
我们从上往下,慢慢分析ViewGroup的dispatchTouchEvent
方法。
Android是支持残障人士使用的,AccessibilityService能够模拟触摸事件,而现在我们通常用它来抢红包,其实现原理就和下面这段代码息息相关,先挖个坑。
// 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);
}
下一行代码对handled赋值为false,这里单独拿出来就说明这个参数很重要,从名字可以看出这个值代表了事件是否被处理,后面还会多次遇到。
boolean handled = false;
接着判断事件是否是安全的,如果OJBK,则通过事件掩码获取actionMasked,这里提一嘴,MotionEvent.ACTION_MASK
可以翻译成事件掩码,主要作用是在多点触摸时分辨触摸事件。
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();
}
...
继续分析,如果事件是ACTION_DOWN
,则调用cancelAndClearTouchTargets(ev)
和resetTouchState()
两兄弟。先看前面一个方法。
/**
* Cancels and clears all touch targets.
*/
private void cancelAndClearTouchTargets(MotionEvent event) {
if (mFirstTouchTarget != null) {
boolean syntheticEvent = false;
if (event == null) {
final long now = SystemClock.uptimeMillis();
event = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
syntheticEvent = true;
}
for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
resetCancelNextUpFlag(target.child);
dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
}
clearTouchTargets();
if (syntheticEvent) {
event.recycle();
}
}
}
顾名思义,cancelAndClearTouchTargets
是用来重新初始化TouchTarget的,毕竟Down事件是一次用户触摸的开始,所以在开始之前都要把之前的TouchTarget都清除掉。那么TouchTarget又是个啥玩意儿呢?
/* Describes a touched view and the ids of the pointers that it has captured.
*
* This code assumes that pointer ids are always in the range 0..31 such that
* it can use a bitfield to track which pointer ids are present.
* As it happens, the lower layers of the input dispatch pipeline also use the
* same trick so the assumption should be safe here...
*/
private static final class TouchTarget {
private static final int MAX_RECYCLED = 32;
...
// The next target in the target list.
public TouchTarget next;
从注释上可以看出,TouchTarget形容了触摸的点。它是一个单向链表,最大长度为32,也就是说,Android最多允许32个触摸点同时进行操作,算一下,起码2个人把手脚都放在同一个屏幕上才能(先不考虑能不能放得下)把设备弄成傻逼。
resetTouchState()
的作用是清除标志位,就不仔细看了。我们稍微总结一下这部分功能,ViewGroup的dispatchTouchEvent
会判断触摸事件类型,如果当前为DOWN事件,则会将所有状态都初始化,开始新的一轮事件处理。
3.2.2 事件拦截
下面继续分析dispatchTouchEvent
。结束了事件重置之后,这里定义了一个intercepted变量,显而易见,这是用来做事件拦截的。
// 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判断中,只要当前触摸事件为DOWN或者存在TouchTarget就会继续执行intercepted判断。这里有一个与运算mGroupFlags & FLAG_DISALLOW_INTERCEPT
(两位同时为“1”,结果才为“1”,否则为0)。来想想我们是如何请求父控件不要拦截触摸事件的?没错,就是getParent().requestDisallowInterceptTouchEvent(true)
:
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
当参数disallowIntercept为true时,会执行mGroupFlags |= FLAG_DISALLOW_INTERCEPT
运算(参加运算的两个对象只要有一个为1,其值为1),由于是getParent
,所以此时的mGroupFlags
就是ViewGroup中的mGroupFlags
,下面的运算属于计算机基础,X|A&A=A,所以最终mGroupFlags
的结果就是FLAG_DISALLOW_INTERCEPT
。我们看源码发现FLAG_DISALLOW_INTERCEPT
的值为0x80000不等于0,因此如果子View执行了getParent().requestDisallowInterceptTouchEvent(true)
这个方法,父View中的intercepted
参数就会被赋值为false。
知道了这样一个流程后,我们再把思维分叉开来,回到之前的代码中
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
默认情况下,disallowIntercept结果是false,此时就会执行onInterceptTouchEvent(ev)
并将其返回值赋值给intercepted,而onInterceptTouchEvent
一般是会由程序员来重写的。
好了,现在你已经搞清楚intercepted这个标志位是怎么被赋值为true或者false的,不过这只是个标志位,并没有执行什么拦截的操作,接下来我们回到ViewGroup的dispatchTouchEvent
方法中,去看看具体的拦截操作是怎么执行的。
// Check for cancelation.
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;
if (!canceled && !intercepted) {
...这里有很多很多代码...
此时又获取了canceled 标志位,顾名思义用来判断事件是否被取消,这位兄弟一般都为true,并不是什么重点。重点在于if判断,这里先讨论intercepted
为true的情况,此时就不会执行if中的一大段代码,直接跳到下面的流程中:
// 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);
}
由于之前的重置操作会将mFirstTouchTarget 设置为Null,所以此时会执行dispatchTransformedTouchEvent()
方法,这是事件分发中最重要的方法,请注意第三个参数为null:
// 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);
}
我们截取了方法中最关键的部分,child就是调用方法时传入的第三个参数,当child==null
时,会执行super.dispatchTouchEvent(transformedEvent)
并将返回结果赋值给handled,我们此时是在ViewGroup中,其父类是View,所以我们要去考察View的dispatchTouchEvent方法:
什么?
你居然还在等着看View的dispatchTouchEvent源码?
文章的第二部分是白看的吗?
前戏是白做的吗?
快回去重新读一遍!
请注意!虽然最后代码跑到了View中,但这个View是ViewGroup的父类!也就是说最终执行的Touch或Click方法依然是外层ViewGroup中重写的Touch或Click方法!请区分ViewGroup、View、父View与子View的区别~
我知道有人还是懵逼的,我们总结下。导致intercepted
为true的原因有两个,一是父View重写了onInterceptTouchEvent
方法并返回true,二是子View没有请求getParent().requestDisallowInterceptTouchEvent(true)
方法。而当intercepted
为true时,父View就会执行拦截操作,在源码中的表现就是dispatchTransformedTouchEvent()
的第三个参数为null,从而执行super.dispatchTouchEvent(transformedEvent)
方法,这个方法最终会调用在父View中重写的Touch或Click方法。
别放松,还没完呢。看View中的这段代码:
if (!result && onTouchEvent(event)) {
result = true;
}
如果父View中onTouchEvent
返回false,那么result的结果就是false(结合文章第二段看更加清晰哟)。此时再回到Activity的dispatchTouchEvent
方法中:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
由于result为false,所以getWindow().superDispatchTouchEvent(ev)
也为false,此时就仍然会执行Activity的onTouchEvent(ev)
方法!因此,纵使父View拦截了事件,只要他的onTouchEvent
返回false,Activity中的onTouchEvent(ev)
方法依旧会得到执行!
OK,到此为止事件拦截就算讲完了。道友们且好好消化,下面继续发车!
3.2.3 事件分发
在前面事件拦截的分析中,我们假设intercepted
为true,所以就会跳过下面代码中的if判断
// Check for cancelation.
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;
if (!canceled && !intercepted) {
...这里有很多很多代码...
而跳过的一大段代码恰恰是实现事件分发的代码。默认情况下,intercepted
都为false,因此if判断中的代码基本都会执行,现在让我们一起来看看事件分发是如何实现的。
// 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;
在事件分发的开始,又出现了Accessibility
相关的字段,这是android可以实现自动化测试的原因之一,这里先加深一波印象。
下面还是条件判断,由于此时仍然是DOWN事件,自然而然就进去了。
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
...省略几行代码...
final int childrenCount = mChildrenCount;
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.
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
...省略下面代码...
接着判断当前控件是否含有子控件,如果包含子View,则通过buildTouchDispatchChildList()
对所有子View进行重排序,这个方法也是挺有意思的,它会回调buildOrderedChildList()
:
ArrayList<View> buildOrderedChildList() {
...省略...
for (int i = 0; i < childrenCount; i++) {
// add next child (in child order) to end of list
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View nextChild = mChildren[childIndex];
final float currentZ = nextChild.getZ();
// insert ahead of any Views with greater Z
int insertIndex = i;
while (insertIndex > 0 && mPreSortedChildren.get(insertIndex - 1).getZ() > currentZ) {
insertIndex--;
}
mPreSortedChildren.add(insertIndex, nextChild);
}
return mPreSortedChildren;
}
为什么要重排序呢?因为View是一层层添加到Window上的,在事件分发的时候,如果某一触摸点下面有多层子View,自然应该是最外层的子View先接收到事件。遗憾的是,在View的添加过程中,并不是先添加到父View中的子View就一定在最外层,因此我们就有必要通过每个子View的Z轴数值对他们进行重排序。
重排序之后,就按照排好的顺序一个个拿到子View,并通过if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null))
这句代码来判断子View是否可以接收当前的触摸事件。
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
...省略Accessibility相关代码...
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
先看前一个方法,显而易见接收触摸事件有两个条件,一是可见,二是不在执行动画。
private static boolean canViewReceivePointerEvents(@NonNull View child) {
return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null;
}
接着看下一个方法,关在在于transformPointToViewLocal
会加上偏移值,而child.pointInView(point[0], point[1])
会判断该child是否可以接收到(x,y)点的触摸事件
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;
}
综合上述两处,我们总结View能够接收触摸事件的条件一共有四个:
1.可见
2.不在执行动画
3.可点击
4.触摸点在View内
在之前的分析中,如果遍历到的子View不能接收事件,就直接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;
}
getTouchTarget()
也是重点方法:
private TouchTarget getTouchTarget(@NonNull View child) {
for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
if (target.child == child) {
return target;
}
}
return null;
}
当触摸事件为DOWN时,mFirstTouchTarget会被重置为null,因此此时getTouchTarget
返回null,什么都没有发生,代码继续向下执行。那么为什么又说这是重点方法呢,因为当触摸事件为MOVE时,mFirstTouchTarget不为null,此时就会直接break出当前循环。我们知道MOVE是十分频繁的调用,所以这里相当于是做了一层性能优化。具体是怎么优化的,我们在下个篇章还会介绍。
拓展完毕回到主线上,代码再次进入条件判断
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
...
dispatchTransformedTouchEvent()
在之前出现过,当父View拦截事件时,该方法被调用,第三个参数为null,并最终调用了父View的Touch或Click方法。而此时,第三个参数不再为null,取而代之的是可以接收触摸事件的子View,我们重新来看dispatchTransformedTouchEvent()
中最关键的代码:
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;
当child不为空时,先进行触摸点的偏移计算,接着执行handled = child.dispatchTouchEvent(event)
。如果child是ViewGroup,就相当于重新执行上面的一大波步骤;如果child是View,则类似于父View拦截事件的过程,会执行View本身的Touch或Click方法。
3.2.4 事件回调
好了好了,事件分发下去的过程终于梳理完了,我们接着看分发结果的回调。
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;
}
如果子View的dispatchTouchEvent()
返回true,就会进入判断体并执行最重要的一行代码newTouchTarget = addTouchTarget(child, idBitsToAssign)
:
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
之前说过TouchTarget表示触摸目标,其本质是一个单向链表。显而易见,addTouchTarget()
的作用就是为mFirstTouchTarget 赋值,初始化这个链表。
现在mFirstTouchTarget 不为Null了,我们来看最后一段源码:
// 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 {
// 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 {
...
}
}
当子View返回true时,子View就消耗了这个事件,执行else中的代码,handled被赋值为true;而当子View不消耗事件返回false时,执行if中的代码,dispatchTransformedTouchEvent
一共出现了3次,大家应该很熟悉了,当第三个参数为null时,会调用父View的Touch或Click方法,就这样一层一层的回调上去,整个过程是一个很完美的递归。
3.3 MOVE事件
在前面的文章中,我们基本是以DOWN事件为例进行分析的,如果你熟练理解了上面所讲的内容,那么请换上这辆快车,继续来看看MOVE事件是怎样的玩法。
3.3.1 MOVE事件拦截
MOVE事件也能够被拦截的原因就在于这个if判断。
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;
}
}
由于DOWN事件会为mFirstTouchTarget赋值,因此MOVE时mFirstTouchTarget!=null,该拦截的继续拦截。
3.3.2 MOVE事件分发
现在回忆一下DOWN事件分发的那一大堆步骤,什么子View重排序啊、遍历啊、判断能否接收触摸事件啊等等等等,其过程十分复杂,因为DOWN是点一下就完事了,所以可以这么整,而MOVE的调用非常频繁,要也这样操作,用户界面绝对会被卡死。
所以我们才会用到TouchTarget,触发Down事件后,TouchTarget被赋值,其目标就是可以接收触摸事件的子View,因此在MOVE事件中,我们可以直接跳过前面的一大段代码,直接从mFirstTouchTarget获取需要的子View:
// 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 {
// 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;
}
}
这段代码不久前出现过,只是我省略了else中的部分代码,因为之前在说DOWN事件,与MOVE无关。现在我们来看完整的流程,当mFirstTouchTarget不为Null时,最关键的是这段:
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
MOVE事件就是在这里进行分发与回调的!
4.总结
如果读到最后,你有一种什么都联系起来了,豁然开朗的感觉,那就点个赞呗!如果读完心想这文章写的什么[哔]东西,请务必摆上一份源码再读一次!
而如果你真的什么都懂,底下留言!我赔钱!
网友评论