由于目前还是处于比较闲置的状态,所以我依然在github上面乱看别人的优秀项目。最近看到一个仿今日头条详情页的效果,觉得不错,本来想学习一遍之后分享给大家,没想到最后怂了,手势事件分发流程有点不清不楚了。这不,再来一遍源码分析吧
网上的源码分析很多还是不错的,但是有的源代码版本比较老,虽然说万变不离其宗,但是总不能让别人看2.3的上古源码吧。还有个别只讲结论的,那些就不说了,很多说的还是错的。今天我试图用最通俗的讲解,让大家觉得源码理解也不那么复杂,更重要的是学习到最准确的知识
怎么样阅读源码
我不是什么高手、大神,所以我仅仅说一下自己阅读源码的经验:抓大放小。每个开源项目的规模都是不同的,像系统源码这种玩意,一般情况下我们都是以验证功能的目的来阅读。对于本篇文章所说的手势事件分发,基本流程你肯定或多或少知道一些,所以我们就找那些重要节点。我们不需要知道每一行代码的意思,不然你就会陷入茫茫大海不可自拔。
本文使用的是最新的Android8.1的源码,请提前下载好相应的SDK以便于查阅
基础知识
- 当用户触摸屏幕时(View或ViewGroup派生的控件),将产生点击事件(Touch事件),Touch事件的相关细节(发生触摸的位置、时间等)被封装成MotionEvent对象。
- 事件类型有4种:MotionEvent.ACTION_DOWN、MotionEvent.ACTION_UP、MotionEvent.ACTION_MOVE和MotionEvent.ACTION_CANCEL
- 事件传递的顺序一般为:Activity -> ViewGroup -> View
- 事件分发过程由dispatchTouchEvent() 、onInterceptTouchEvent()和onTouchEvent()这三个方法来协作完成。这里要注意一点,Activity和View是没有onInterceptTouchEvent()方法的
View的事件处理流程
为了方便我们阅读源码,我先把View的事件处理流程用图来进行说明。
View的事件处理流程简单的对图中的流程进行说明:View的事件处理流程从dispatchTouchEvent
开始,随后对其中的OnTouchListener
对象进行判断,如果不为null且View为enable,就去检查该对象的onTouch
方法返回值。如果这个返回值为true,那么说明事件被消费,后续也不会调用onClick
与onLongClick
方法。如果这个返回值为false,事件流向onTouchEvent
方法,默认情况下onTouchEvent
的返回值就是dispatchTouchEvent
的返回值,这个值会影响ViewGroup的事件分发,我会稍后进行说明。
所以你应该很清楚,View其实不应该用“分发”两字来描述它的流程,而是单纯的在自己跟自己玩。
有了大概的印象之后,我们开始来阅读View的源码。既然源头在dispatchTouchEvent
,那我们就从这个方法开始看,找到dispatchTouchEvent
方法,来看11714行
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;
}
这里就是事件流程进行判断的地方,整个View事件流程的核心就是在这里。我们开始分解代码
ListenerInfo
是什么?它是一系列事件Listener的总类
static class ListenerInfo {
public OnClickListener mOnClickListener;
protected OnLongClickListener mOnLongClickListener;
private OnTouchListener mOnTouchListener;
.......................................
}
也就是说如果我们定义了OnClickListener
或者OnLongClickListener
、OnTouchListener
,那ListenerInfo
对象就不会是null。
这样我们通过第一个if语句就可以明白,如果你的View是enable并且View的OnTouchListener
中onTouch
方法返回true,那么这个View就消费了事件。这是因为result值已经变成true,对照剩余部分的源码可以知道其他判断方法都走不进去了,这里实际上已经完成了dispatchTouchEvent
的流程。既然这一步骤已经将result改为true了,那么后续的onTouchEvent
自然就进不去了。
解决完onTouchListener出现与否的问题后,我们开始真正进入onTouchEvent研究了
注意看第二个if,当你绕过onTouchListener
之后,当前View的onTouchEvent
的返回值与super.dispatchTouchEvent
的值是相同的了。
进入onTouchEvent
方法,来到12923行
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
只要你设置了CLICKABLE
或者LONG_CLICKABLE
或者CONTEXT_CLICKABLE
,那么这里的clickable就是true。这几个属性都可以在布局中设置。
在向下来看12942行
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP)
clickable为true,才能进入各种MotionEvent的action的处理逻辑。因为View本身clickable的值是false,如果我们刚才在布局中没有将其设置为true,那么后面的一系列判断跟我们就都没关系了。
action的处理逻辑里有你最关心的onClick与onLongClick事件的处理,先来看MotionEvent.ACTION_DOWN
boolean isInScrollingContainer = isInScrollingContainer();
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
setPressed(true, x, y);
checkForLongClick(0, x, y);
}
首先判断事件是否在滚动的容器内,如果是就延迟100ms再去执行长按操作的检查,否则就立刻开始。这里先将pressed状态变为true,可以联想到selector变色,然后执行checkForLongClick
private void checkForLongClick(int delayOffset, float x, float y) {
if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {
........................
postDelayed(mPendingCheckForLongPress,
ViewConfiguration.getLongPressTimeout() - delayOffset);
}
}
如果在500ms内这个长按操作message没有被取消,那他就会被执行。执行的就是我们熟悉的performLongClick
。来看看这个message
private final class CheckForLongPress implements Runnable {
@Override
public void run() {
if ((mOriginalPressedState == isPressed()) && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
if (performLongClick(mX, mY)) {
mHasPerformedLongPress = true;
}
}
}
··········································
}
既然说到了取消长按操作message,那这个取消会出现在哪里呢?我们来MotionEvent.ACTION_UP
看看,注意这里
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
removeLongPressCallback();
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
长按事件没有执行,被取消了。removeLongPressCallback
将长按事件取消,随后执行了performClick
到此为止,你就很清楚的明白什么情况下长按以及单击事件会不执行了吧,别忘记onTouchLitener是优先于onClickListener和onLongClickListener执行的。同时通过分析代码,我们也明白了super.onTouchEvent
里面确实没有干什么对事件分发的影响事情,如果你对dispatchTouchEvent
进行了重写,那么super.onTouchEvent
的返回值对你来说没有任何影响
ViewGroup事件分发
ViewGroup与之相比会多一些东西,在理解上可能比较绕,因为它是真正的涉及到了分发这个功能,但是多看几遍也就那么一回事。它跟View的分发流程大体上差不多,但是多了一个onInterceptTouchEvent
方法。我们依然还是先看图说话
当事件到了ViewGroup中之后,依然是从dispatchTouchEvent
开始,然后去判断是否被onInterceptTouchEvent
拦截。如果没有被拦截,则开始遍历子View去寻找是否含有被点击的。如果找不到,则执行自己父类View的dispatchTouchEvent
,进而事件交由自身来处理,其中执行结果是true,表明被自己消费;执行结果是false,表明自己对事件不关心。如果能找到,则将事件交由子View来处理,实现了事件的下发。由于查找View的过程是一套递归流程,所以自然而然形成了下发及回溯的“U型管”效果
如果被拦截,则执行自己父类View的dispatchTouchEvent
,进而事件交由自身来处理,其中执行结果是true,表明被自己消费;执行结果是false,表明自己对事件不关心。
说了个大概之后继续看ViewGroup的源码。同样,我们进入ViewGroup的dispatchTouchEvent
,来看看2497行
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
在刚开始的时候重置触摸相关的状态。cancelAndClearTouchTargets
里面有很重要的一个参数mFirstTouchTarget
,它的意义在于标记有没有子View来消费手势事件。这时该方法中的clearTouchTargets
方法在down的时候将其置为null,这个值会参与到dispatchTouchEvent
的很多重要逻辑中
继续往下走,一下子就看到那个熟悉的身影onInterceptTouchEvent
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);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
先来关注下mGroupFlags
参数,这个参数是“内部拦截法”的核心,可以通过requestDisallowInterceptTouchEvent
方法来允许或者不允许父ViewGroup进行事件拦截。
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
通过最下面的if (mParent != null)
可以清楚的看到这是一个递归的形式。如果你在底层view调用parent.requestDisallowInterceptTouchEvent
,那么它会不断调用父类的这个方法,所以所有的父ViewGroup的这个标记位都是一样的。另外需要注意的是,该方法在down时不具备拦截的功能,因为它的上一级ViewGroup的这段代码在此刻已经执行完了。
再回到dispatchTouchEvent
中来,默认情况下disallowIntercept
为false,不进行拦截,这样就来到了onInterceptTouchEvent
。一般情况下其返回值默认就是false,表示不进行拦截
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;
}
那么这一小段代码结束之后,intercepted
值就是false,不拦截。继续向下
如果不被拦截,开始查找该ViewGroup内的子View有没有包含消费事件的
if (!canceled && !intercepted)
没有被取消事件或者拦截事件才可以进入
随后又有一个限制
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE)
基本上可以理解为只有down的时候才可以进入。接着看
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 (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
...........................................
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
...........................................
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
这边我浓缩了很多代码,有的涉及到多指触控的,这个不在我们本次讨论中。
反向遍历所有的子View,优先找出后addView进去的View。canViewReceivePointerEvents
表示当子View可见或者正在执行动画或准备执行动画条件下才可以接受触摸事件,isTransformedTouchPointInView
表示触摸点范围在当前子View内才可以接受触摸事件。只有满足这两个条件的子View才可进行下一步判断。
dispatchTransformedTouchEvent
绝对是一个重头戏,他关系着手势事件的分发。来看看代码,依然看重点部分
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {
final boolean handled;
..................................
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
..................................
handled = child.dispatchTouchEvent(transformedEvent);
}
..................................
return handled;
}
来来来,看到了吧。如果有子View的时候,执行的是子View的dispatchTouchEvent
,否则执行的是自身的dispatchTouchEvent
。所以为什么说手势事件分发是一个“U形管”呢,就在这个地方。当然这里只是进入子View的部分。无论是自身还是子View甚至子子View的dispatchTouchEvent
发生拦截,handler
都会为true,反之为false。如果dispatchTransformedTouchEvent
为true的话,那么刚才说到的mFirstTouchTarget
就在addTouchTarget
方法中被赋值,同时alreadyDispatchedToNewTouchTarget
标志也被修改为true,表明找到了可以消费事件的子VIew了
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
最后就是“U型管”的回溯部分了
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
} else {
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;
}
}
如果你的子View没有消费事件的,那么mFirstTouchTarget
是null,直接执行自身的dispatchTouchEvent
并返回false;如果有消费,就啥都不做啦,直接返回true
下面开始分析up事件的流程,因为move跟up大体上差不多,讲一个就行了。
还是要重新跑一边刚才dispatchTouchEvent
的流程。这个要区分mFirstTouchTarget
是不是null两种情况来说。
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 {
intercepted = true;
}
如果mFirstTouchTarget
不为null,说明之前有子View消费。既然不是down,所以之前查找子View的过程就没有了,直接来到最后判断mFirstTouchTarget
的地方。mFirstTouchTarget
为null直接执行自身的dispatchTouchEvent
,这一般都发生在Activity的DecorView的。mFirstTouchTarget
不为null并且alreadyDispatchedToNewTouchTarget
又是false,所以又会通过dispatchTransformedTouchEvent
来执行子View的dispatchTouchEvent
,并将handled
置为true,全套流程走完
差点忘记一个很重要的事情了,之前看到有人试图在onTouchEvent
的move里面修改返回值来达到调整事件分发流程,事实上这是无法实现要求的。从源码来看只有在up的时候或者事件取消,mFirstTouchTarget
才会被重置,在move跟up的时候每次都会使用down时候的mFirstTouchTarget
值
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
}
小实验
我们来做如下几个实验来看看你对之前所说知识理解的情况如何
先来一个ChildView,它继承自普通的View,仅仅在dispatchTouchEvent
以及onTouchEvent
相关action下打印提示而已
class ChildView: View {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
when(event?.action) {
MotionEvent.ACTION_DOWN -> {
println("ChildView dispatchTouchEvent ACTION_DOWN")
}
MotionEvent.ACTION_MOVE -> {
println("ChildView dispatchTouchEvent ACTION_MOVE")
}
MotionEvent.ACTION_UP -> {
println("ChildView dispatchTouchEvent ACTION_UP")
}
MotionEvent.ACTION_CANCEL -> {
println("ChildView dispatchTouchEvent ACTION_CANCEL")
}
}
return super.dispatchTouchEvent(event)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
when(event?.action) {
MotionEvent.ACTION_DOWN -> {
println("ChildView onTouchEvent ACTION_DOWN")
}
MotionEvent.ACTION_MOVE -> {
println("ChildView onTouchEvent ACTION_MOVE")
}
MotionEvent.ACTION_UP -> {
println("ChildView onTouchEvent ACTION_UP")
}
MotionEvent.ACTION_CANCEL -> {
println("ChildView onTouchEvent ACTION_CANCEL")
}
}
return super.onTouchEvent(event)
}
}
整个app就一个简单的布局,这里注意ChildView的clickable是true
<?xml version="1.0" encoding="utf-8"?>
<com.renyu.newsdetaildemo.view.ChildView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true" />
在activity里面给ChildView加一个onTouchListener
view_child.setOnTouchListener { _, event ->
when(event?.action) {
MotionEvent.ACTION_DOWN -> {
println("ChildView OnTouchListener ACTION_DOWN")
}
MotionEvent.ACTION_MOVE -> {
println("ChildView OnTouchListener ACTION_MOVE")
}
MotionEvent.ACTION_UP -> {
println("ChildView OnTouchListener ACTION_UP")
}
MotionEvent.ACTION_CANCEL -> {
println("ChildView OnTouchListener ACTION_CANCEL")
}
}
false
}
实验一:
ChildView的dispatchTouchEvent
返回值为super.dispatchTouchEvent
ChildView的onTouchEvent
返回值为super.onTouchEvent
ChildView的setOnTouchListener
返回值为false
ChildView dispatchTouchEvent ACTION_DOWN
ChildView OnTouchListener ACTION_DOWN
ChildView onTouchEvent ACTION_DOWN
ChildView dispatchTouchEvent ACTION_UP
ChildView OnTouchListener ACTION_UP
ChildView onTouchEvent ACTION_UP
可以看到ChildView事件传递顺序为dispatchTouchEvent
-> OnTouchListener
-> onTouchEvent
。因为setOnTouchListener
返回值是false,所以顺利进入onTouchEvent
判断方法中。又因为ChildView设置了clickable的原因,所以onTouchEvent
返回true,表明事件被消费。这样ChildView的上一层ViewGroup---DecorView的mFirstTouchTarget
值不为null,所以每次都会通过调用了ChildView的dispatchTouchEvent
走到ChildView来。
实验二:
ChildView的dispatchTouchEvent
返回值为super.dispatchTouchEvent
ChildView的onTouchEvent
返回值为super.onTouchEvent
ChildView的setOnTouchListener
返回值为true
ChildView dispatchTouchEvent ACTION_DOWN
ChildView OnTouchListener ACTION_DOWN
ChildView dispatchTouchEvent ACTION_UP
ChildView OnTouchListener ACTION_UP
对比之前实验一,因为setOnTouchListener返回值是true,所以onTouchEvent
事件没了。同样由于事件被ChildView消费,因此每次都会走到ChildView来。
实验三:
移除ChildView的setOnTouchListener
代码
ChildView的dispatchTouchEvent
返回值为super.dispatchTouchEvent
ChildView的onTouchEvent
去除super.onTouchEvent
,并将返回值设置为false
ChildView dispatchTouchEvent ACTION_DOWN
ChildView onTouchEvent ACTION_DOWN
dispatchTouchEvent
的返回值跟onTouchEvent
相同,由于后者返回false,所以事件未被消费。mFirstTouchTarget为空,下次事件不会再走到这里
实验四:
移除ChildView的setOnTouchListener
代码
ChildView的dispatchTouchEvent
返回值为super.dispatchTouchEvent
ChildView的onTouchEvent
去除super.onTouchEvent
,并将返回值设置为true
ChildView dispatchTouchEvent ACTION_DOWN
ChildView onTouchEvent ACTION_DOWN
ChildView dispatchTouchEvent ACTION_UP
ChildView onTouchEvent ACTION_UP
之前说了super.onTouchEvent
的存在与否对事件分发没有太大影响,关键还是看返回值。只要你返回值是true就代表消费了
实验五:
移除ChildView的setOnTouchListener
代码
ChildView的dispatchTouchEvent
返回值为super.dispatchTouchEvent
ChildView的onTouchEvent
保留super.onTouchEvent
,并将返回值设置为false
ChildView dispatchTouchEvent ACTION_DOWN
ChildView onTouchEvent ACTION_DOWN
这样又与实验三相同了
实验六:
移除ChildView的setOnTouchListener
代码
ChildView的dispatchTouchEvent
返回值为super.dispatchTouchEvent
ChildView的onTouchEvent
保留super.onTouchEvent
,并将返回值设置为true
ChildView dispatchTouchEvent ACTION_DOWN
ChildView onTouchEvent ACTION_DOWN
ChildView dispatchTouchEvent ACTION_UP
ChildView onTouchEvent ACTION_UP
这样又与实验四相同了
实验七:
移除ChildView的setOnTouchListener
代码
ChildView的dispatchTouchEvent
去除super.dispatchTouchEvent
,并将返回值设置为false
ChildView的onTouchEvent
返回值为super.onTouchEvent
ChildView dispatchTouchEvent ACTION_DOWN
dispatchTouchEvent
去除super.dispatchTouchEvent
只会影响其内部事件流程,对分发没有影响。所以这里没有执行onTouchEvent
。直接返回false,就表示没有发生消费事件,所以不会造成后续事件进入该View
实验八:
移除ChildView的setOnTouchListener
代码
ChildView的dispatchTouchEvent
去除super.dispatchTouchEvent
,并将返回值设置为true
ChildView的onTouchEvent
返回值为super.onTouchEvent
ChildView dispatchTouchEvent ACTION_DOWN
ChildView dispatchTouchEvent ACTION_UP
与实验七的区别在于返回值是true,事件被消费了,所以多一个dispatchTouchEvent
的up
实验九:
移除ChildView的setOnTouchListener
代码
ChildView的dispatchTouchEvent
保留super.dispatchTouchEvent
,并将返回值设置为false
ChildView的onTouchEvent
返回值为super.onTouchEvent
ChildView dispatchTouchEvent ACTION_DOWN
ChildView onTouchEvent ACTION_DOWN
可以对比下实验七
实验十:
移除ChildView的setOnTouchListener
代码
ChildView的dispatchTouchEvent
保留super.dispatchTouchEvent
,并将返回值设置为true
ChildView的onTouchEvent
返回值为super.onTouchEvent
ChildView dispatchTouchEvent ACTION_DOWN
ChildView onTouchEvent ACTION_DOWN
ChildView dispatchTouchEvent ACTION_UP
ChildView onTouchEvent ACTION_UP
可以对比下实验八
实验十一
我们把布局稍微改变一下,加入一个继承自LinearLayout的ParentView,具体功能同ChildView一样
class ParentView: LinearLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
when(event?.action) {
MotionEvent.ACTION_DOWN -> {
println("ParentView dispatchTouchEvent ACTION_DOWN")
}
MotionEvent.ACTION_MOVE -> {
println("ParentView dispatchTouchEvent ACTION_MOVE")
}
MotionEvent.ACTION_UP -> {
println("ParentView dispatchTouchEvent ACTION_UP")
}
MotionEvent.ACTION_CANCEL -> {
println("ParentView dispatchTouchEvent ACTION_CANCEL")
}
}
return super.dispatchTouchEvent(event)
}
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
when(ev?.action) {
MotionEvent.ACTION_DOWN -> {
println("ParentView onInterceptTouchEvent ACTION_DOWN")
}
MotionEvent.ACTION_MOVE -> {
println("ParentView onInterceptTouchEvent ACTION_MOVE")
}
MotionEvent.ACTION_UP -> {
println("ParentView onInterceptTouchEvent ACTION_UP")
}
MotionEvent.ACTION_CANCEL -> {
println("ParentView onInterceptTouchEvent ACTION_CANCEL")
}
}
return super.onInterceptTouchEvent(ev)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
when(event?.action) {
MotionEvent.ACTION_DOWN -> {
println("ParentView onTouchEvent ACTION_DOWN")
}
MotionEvent.ACTION_MOVE -> {
println("ParentView onTouchEvent ACTION_MOVE")
}
MotionEvent.ACTION_UP -> {
println("ParentView onTouchEvent ACTION_UP")
}
MotionEvent.ACTION_CANCEL -> {
println("ParentView onTouchEvent ACTION_CANCEL")
}
}
return super.onTouchEvent(event)
}
}
布局稍微改变一下
<?xml version="1.0" encoding="utf-8"?>
<com.renyu.newsdetaildemo.view.ParentView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/view_parent"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.renyu.newsdetaildemo.view.ChildView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/view_child"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true" />
</com.renyu.newsdetaildemo.view.ParentView>
把ParentView的onInterceptTouchEvent
改为true,并将其clickable设置为true
ParentView dispatchTouchEvent ACTION_DOWN
ParentView onInterceptTouchEvent ACTION_DOWN
ParentView onTouchEvent ACTION_DOWN
ParentView dispatchTouchEvent ACTION_UP
ParentView onTouchEvent ACTION_UP
参考上面的代码,onInterceptTouchEvent(ev)
为true,那么intercepted
就是true,那么查找子View的过程就没有了,因此mFirstTouchTarget
值为null直接执行自身的dispatchTouchEvent
。up的时候再进来,mFirstTouchTarget
为null,也就不会再执行onInterceptTouchEvent
了
实验十二
这里还有一种情况,如果我们给ParentView加clickable为true,同时在事件分发过程中改变onInterceptTouchEvent(ev)
的值,比如在move事件中,会发生什么事情?
ParentView dispatchTouchEvent ACTION_DOWN
ParentView onInterceptTouchEvent ACTION_DOWN
ChildView dispatchTouchEvent ACTION_DOWN
ChildView onTouchEvent ACTION_DOWN
ParentView dispatchTouchEvent ACTION_MOVE
ParentView onInterceptTouchEvent ACTION_MOVE
ChildView dispatchTouchEvent ACTION_CANCEL
ChildView onTouchEvent ACTION_CANCEL
ParentView dispatchTouchEvent ACTION_MOVE
ParentView onTouchEvent ACTION_MOVE
ParentView dispatchTouchEvent ACTION_MOVE
ParentView onTouchEvent ACTION_MOVE
ParentView dispatchTouchEvent ACTION_UP
ParentView onTouchEvent ACTION_UP
可以看到与实验十一相比多了一个ACTION_CANCEL
。这个又需要我们回到源码中来。
结合之前的源码,我们知道在中间过程中intercepted
值变为true了,但同时mFirstTouchTarget
在第一次move的时候还不为null。所以流程刚开始move的流程与move是一样的。但是不知道你之前在dispatchTouchEvent
里有没有注意到这句话
final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted
这个表明在设置了intercepted为true之后,cancelChild
就变成true了,这个是关键。这样在执行dispatchTransformedTouchEvent
方法的时候,子View的action的值就被设置为cancel,然后一级一级的传递下去
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;
}
..............................................
}
最终,mFirstTouchTarget
被置为null
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
由于之前设置了clickable,所以在下次循环过程中执行的将是自己父类View的dispatchTouchEvent
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
这12个实验结果你要是能完全想明白为什么是这样的话,你可以高声大喊,View事件传递我全明白了!!!!
要是你有一个想不明白原因的,请你还是耐心的再看一遍吧。
心得体会
其实从开始学习这套源码到今天博客的完成,我大致花费了3天时间,来来回回反复看源码看了很多遍终于理解透彻了。希望大家也能耐下心来来回多读几遍,肯定每一遍给你的感悟都会不一样
网友评论