View和ViewGroup的事件传递机制
view的事件传递一直是我一个软肋,以前我知道个大概,就是表面的意思是这样的,但是具体到这一些细节还是出了问题,所以,我花了较多时间看了下源码,并从其他的人的博客得到一些启发,但是还多博客都是一上来就是源码的,给人一种慢慢看完了不知道在说什么,只有等到自己梳理一遍,才完全知道。
本文要说明view的事件传递机制,从源码入手,清晰说明MotionEvent在view和activity的中传递的过程,虽然源码较多,但是我采取一种主线下去的感觉:
另外欢迎大家指出其中错误
TouchTarget类的简单说明
Activity和View之间的motionEvent的传递
ViewGroup的关于MotionEvent传递方法的介绍
View的关于MotionEvent传递方法的介绍
总结
感谢
TouchTarget的介绍
touchtarget是viewgroup的一个静态类,在这面我们贴下的它的源码
private static final class TouchTarget {
private static final int MAX_RECYCLED = 32;
private static final Object sRecycleLock = new Object[0];
private static TouchTarget sRecycleBin;
private static int sRecycledCount;
public static final int ALL_POINTER_IDS = -1; // all ones
// The touched child view.
public View child;
// The combined bit mask of pointer ids for all pointers captured by the target.
public int pointerIdBits;
// The next target in the target list.
public TouchTarget next;
private TouchTarget() {
}
public static TouchTarget obtain(View child, int pointerIdBits) {
final TouchTarget target;
synchronized (sRecycleLock) {
if (sRecycleBin == null) {
target = new TouchTarget();
} else {
target = sRecycleBin;
sRecycleBin = target.next;
sRecycledCount--;
target.next = null;
}
}
target.child = child;
target.pointerIdBits = pointerIdBits;
return target;
}
public void recycle() {
synchronized (sRecycleLock) {
if (sRecycledCount < MAX_RECYCLED) {
next = sRecycleBin;
sRecycleBin = this;
sRecycledCount += 1;
} else {
next = null;
}
child = null;
}
}
}
不难发现TouchTarget是一个链表结构的final类,其中sRecycleBin保存着当前的所有touchtarget的对象(static),另外next的中包含下一个信息。并且通过sRecycledCount来计数。
接下来我们简单说下其中obtain()和recycle()的方法。
obtain()
target = sRecycleBin;
sRecycleBin = target.next;
sRecycledCount--;
target.next = null;
新生成的target 的中next指向以前的以前存在的对象,就是依次往回加的感觉,同时sRecycledCount是减一的。
接下来就是recycle()的
next = sRecycleBin;
sRecycleBin = this;
sRecycledCount += 1;
就是把链表中最上面的去掉,也就是最新obtain()的那个给去掉。同样也是谁调用recycle()的这个对象在链表被去除了。但是不影响链表其他的对象,这点非常重要。
TouchTarget类暂时说到这儿了。
Activity和View之间的motionEvent的传递
我们知道activity显示的view是通过window来显示,同样在事件的传递的过程中,activity的motionevent也是通过window去传递给view的,看下面的源码你就知道了
activity中dispatchTouchEvent()
1 public boolean dispatchTouchEvent(MotionEvent ev) {
2 if (ev.getAction() == MotionEvent.ACTION_DOWN) {
3 onUserInteraction();
4 }
5 if (getWindow().superDispatchTouchEvent(ev)) {
6 return true;
7 }
8 return onTouchEvent(ev);
9 }
注意第6行中getWindow().superDispatchTouchEvent(ev)这行代码的意思,给中getWindow即为phoneWindow这个类
phoneWindow中的superDispatchTouchEvent的方法
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatcTouchEvent(event);
}
mDecor的其实就是activity的根view,这个时候也就将MotionEvent从activity传到了view中。
从上面我们可以看出了,activity对MotionEvent的传递先给交给mDecor(ViewGroup)进行分发,如果分发不成功,则交给自己的自己的onTouch()方法。这个结论先记住。
ViewGroup的关于MotionEvent传递
首先在说传递之前,我给大家说明两个词的概念
1 处理:MotionEvent交给它来处理,并不代表它能消费掉MotionEvent。
2 消费:MotionEvent不仅交给它处理,同样的也消费了这个MotionEvent。
明白上面两个词的概念后。我们先说明下,ViewGroup的关于MotionEvent传递的方法有些:
1 dispatchTouchEvent
2 onInterceptTouchEvent
3 requestDisallowInterceptTouchEvent
4 onTouchEvent
借助一段伪代码先概括下的ViewGroup关于MotionEvent事件传递
public boolean dispatchTouchEvent(MotionEvent event){
boolean handle = false;
if(!onInterceptTouchEvent(event) || requestDisallowInterceptTouchEvent(event)&& event.getActionCode != MotionEvent.ACTION_DOWN){
handle = child.dispatchEvent();
}
if(!handle){
handle = onTouchEvent();
}
return handle;
}
这点我们先验证一下:首先我们先定义一个NoDispatchTouchEventViewGroup和另一个TouchEventViewGroup的两个viewgroup
NoDispatchTouchEventViewGroup的中的dispatchTouchEvent的方法如下dispatchTouchEvent中让直接返回false这里写代码片
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.e("TAG", "NoDispatchTouchEvent dispatchTouchEvent ");
return false;
}
TouchEventViewGroup dispatchTouchEvent的方法我们不做处理,但是我们在onTouchEvent()中让他消费这个事件
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e("TAG", "TouchEventViewGroup 的onTouchEvent");
return true;
}
activity的layout文件如下,就是让TouchEventViewGroup里面放着NoDispatchTouchEventViewGroup
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.idreamo.rrtoyewx.rrtoyewx.view.TouchEventViewGroup
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.idreamo.rrtoyewx.rrtoyewx.view.NoDispatchTouchEventViewGroup
android:layout_width="match_parent"
android:layout_height="match_parent">
</com.idreamo.rrtoyewx.rrtoyewx.view.NoDispatchTouchEventViewGroup>
</com.idreamo.rrtoyewx.rrtoyewx.view.TouchEventViewGroup>
</RelativeLayout>
这个时候打印的结果如下
可以看到运行了一次NoDispatchTouchEventViewGroup的中的dispathTouchEvent(),然后以后就直接交给了TouchEventViewGroup的onTouchEvent()的方法。这也就是上面所说的处理和消费,可以看到,NoDispatchTouchEventViewGroup处理过这个事件的中down事件(down 是一个系列事件的最开始),但是并没有消费掉,这也就是后来他接受不到这个系列事件后来的事件的原因。而这系列的处理过程是在viewgroup的dispatchTouchEvent()中实现。也就是意味着viewgroup的dispatchTouchEvent()决定了事件是否向下传递。
正如上面的我们伪代码一样。但是此时我们改下上面这个代码。就是在NoDispatchTouchEventViewGroup的dispatchTouchEvent()的返回值改为true;打印的log的为
并不没有打印NoDispatchTouchEventViewGroup的ontouchevent()
这也就意味了viewgroup的dispatchTouchEvent()的控制着自身的onTouchEvent()的处理过程,dispatchTouchEvent()不仅管控着如何处理这个事件(传递),同时也管控着如消费这个事件。
到这里是不是觉得viewgroup的dispatchTouchEvent()方法很神奇,好的,正如所愿,接下来我们来看下dispatchTouchEvent()的源码:
public boolean dispatchTouchEvent(MotionEvent ev) {
1 if (mInputEventConsistencyVerifier != null) {
2 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.
3 if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
4 ev.setTargetAccessibilityFocus(false);
}
5 boolean handled = false;
6 if (onFilterTouchEventForSecurity(ev)) {
7 final int action = ev.getAction();
8 final int actionMasked = action & MotionEvent.ACTION_MASK;
// Handle an initial down.
9 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.
10 cancelAndClearTouchTargets(ev);
11 resetTouchState();
12 }
// Check for interception.
13 final boolean intercepted;
14 if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
15 final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
16 if (!disallowIntercept) {
17 intercepted = onInterceptTouchEvent(ev);
18 ev.setAction(action); // restore action in case it was changed
19 } else {
20 intercepted = false;
21 }
22 } else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
23 intercepted = true;
24 }
// If intercepted, start normal event dispatch. Also if there is already
// a view that is handling the gesture, do normal event dispatch.
25 if (intercepted || mFirstTouchTarget != null) {
26 ev.setTargetAccessibilityFocus(false);
27 }
// Check for cancelation.
28 final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed.
29 final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
30 TouchTarget newTouchTarget = null;
31 boolean alreadyDispatchedToNewTouchTarget = false;
32 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.
33 View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
34 if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
35 final int actionIndex = ev.getActionIndex(); // always 0 for down
36 final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
37 removePointersFromTouchTargets(idBitsToAssign);
38 final int childrenCount = mChildrenCount;
39 if (newTouchTarget == null && childrenCount != 0) {
40 final float x = ev.getX(actionIndex);
41 final float y = ev.getY(actionIndex);
42 final ArrayList<View> preorderedList = buildOrderedChildList();
43 final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
44 final View[] children = mChildren;
45 for (int i = childrenCount - 1; i >= 0; i--) {
46 final int childIndex = customOrder
? getChildDrawingOrder(childrenCount, i) : i;
47 final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(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.
48 if (childWithAccessibilityFocus != null) {
49 if (childWithAccessibilityFocus != child) {
50 continue;
51 }
52 childWithAccessibilityFocus = null;
53 i = childrenCount - 1;
54 }
55 if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
56 ev.setTargetAccessibilityFocus(false);
57 continue;
58 }
59 newTouchTarget = getTouchTarget(child);
60 if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
61 newTouchTarget.pointerIdBits |= idBitsToAssign;
62 break;
63 }
64 resetCancelNextUpFlag(child);
65 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
66 mLastTouchDownTime = ev.getDownTime();
67 if (preorderedList != null) {
// childIndex points into presorted list, find original index
68 for (int j = 0; j < childrenCount; j++) {
69 if (children[childIndex] == mChildren[j]) {
70 mLastTouchDownIndex = j;
71 break;
72 }
73 }
74 } else {
75 mLastTouchDownIndex = childIndex;
76 }
77 mLastTouchDownX = ev.getX();
78 mLastTouchDownY = ev.getY();
79 newTouchTarget = addTouchTarget(child, idBitsToAssign);
80 alreadyDispatchedToNewTouchTarget = true;
81 break;
82 }
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
83 ev.setTargetAccessibilityFocus(false);
84 }
85 if (preorderedList != null) preorderedList.clear();
86 }
87 if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
88 newTouchTarget = mFirstTouchTarget;
89 while (newTouchTarget.next != null) {
90 newTouchTarget = newTouchTarget.next;
91 }
92 newTouchTarget.pointerIdBits |= idBitsToAssign;
93 }
94 }
95 }
// Dispatch to touch targets.
96 if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
97 handled = dispatchTransformedTouchEvent(ev, canceled, null,
98 TouchTarget.ALL_POINTER_IDS);
99 } else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
100 TouchTarget predecessor = null;
101 TouchTarget target = mFirstTouchTarget;
102 while (target != null) {
103 final TouchTarget next = target.next;
104 if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
105 handled = true;
106 } else {
107 final boolean cancelChild = resetCancelNextUpFlag(target.child)
108 || intercepted;
109 if (dispatchTransformedTouchEvent(ev, cancelChild,
110 target.child, target.pointerIdBits)) {
111 handled = true;
112 }
113 if (cancelChild) {
114 if (predecessor == null) {
115 mFirstTouchTarget = next;
116 } else {
117 predecessor.next = next;
118 }
119 target.recycle();
120 target = next;
121 continue;
122 }
123 }
124 predecessor = target;
125 target = next;
126 }
127 }
// Update list of touch targets for pointer up or cancel, if needed.
128 if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
129 resetTouchState();
130 } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
131 final int actionIndex = ev.getActionIndex();
132 final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
133 removePointersFromTouchTargets(idBitsToRemove);
134 }
135 }
136 if (!handled && mInputEventConsistencyVerifier != null) {
137 mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
138 }
139 return handled;
140 }
是不是后悔了,一开始的兴奋被这140多行代码彻底浇灭了。没事,其实我也不是很明白每一行的代码的意思。但是我采用这个最简单教大家来看,我们都知道一个系列的事件是由一个down事件和若干个move事件和一个up事件组成的,所以在这里我们就按照这个思路来上看上面的代码。
先看down事件,过滤下上面的代码
首先是9到12行
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
cancelAndClearTouchTargets(ev)中有个clearTouchTargets()的方法,看一下,我们会发现其中调用recycle()的方法,正如我门上面所说的mFirstTouchTarget将会从sRecycleBin中移除,另外
cancelAndClearTouchTargets的方法中同样将child也从sRecycleBin给移除了
private void clearTouchTargets() {
TouchTarget target = mFirstTouchTarget;
if (target != null) {
do {
TouchTarget next = target.next;
target.recycle();
target = next;
} while (target != null);
mFirstTouchTarget = null;
}
}
resetTouchState()
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
这里将mGroupFlags给重置了。这点我们只需要关注着么多,总之9到12行的代码主要将viewgroup的在down 之前进行状态重置和初始化。
接下来12到21行主要说明了拦截的处理,但是呢,我们是down 的事件所以进入if内部,但是此时呢我们应该知道mFirstTouchTarget = null的,进入先判断disallowIntercept()的这个值,这个值主要是通过FLAG_DISALLOW_INTERCEPT的这个标志来决定的,而这个标志是通过requestDisallowInterceptTouchEvent来决定的
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);
}
}
通过child调用getParent().requestDisallowInterceptTouchEvent的方法让这个标志位改变,并且将一致延续到最上层viewgroup。
如果child要求这个事件,此时会让onInterceptTouchEvent的返回值实效,但是这个前提是motionevent的actionCode为down,或者mFirstTouchTarget!=null的时候,而mFirstTouchTarget!=null是在down事件传递下去才会有值(这一点下面可以知道)所以我们记住第一个结论,
通过child调用getParent().requestDisallowInterceptTouchEvent()的可以屏蔽掉父亲的onInterceptTouchEvent拦截的返回值,但是如果需要在这个系列的事件都有效,则需要将这个系列的事件down传递到孩子,即父亲不拦截down事件的向下传递,当然父亲的onInterceptTouchEvent的这个默认返回值为false。
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
好了 ,接下里我们先默认这个down事件不拦截向下分发接下来进入33到94行中代码,在进入判断之前,不仅时MotionEvent.ACTION_DOWN,还有split && actionMasked == MotionEvent.ACTION_POINTER_DOWN,前面是单指,后面是有的新的手指加入,在这我们先考虑MotionEvent.ACTION_DOWN的情况下。同时更多关于MotionEvent的知识可以看下我之前写的博客android MotionEvent的相关的类的介绍
接下来进入for循环中对每一个孩子进行遍历,按照从后向前的顺序检查这个MotionEvent是不是在这个child上(canViewReceivePointerEvents()和isTransformedTouchPointInView()),并交给他去处理(dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)),接下来我们看下dispatchTransformedTouchEvent()部分的源码
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;
}
故此时,将事件分发给了孩子的dispatchTouchEvent()中,完成了down的事件的分发。并且当child的dispatchTouchEvent()的返回值为true的时候,这个时候将mFirstTouchTarget给赋值了。
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
看下addTouchTarget的源码,其中
private TouchTarget addTouchTarget(View child, int pointerIdBits) {
TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
如果此时viewgroup不是mDecorView,则我们知道TouchTarget的sRecycleBin其实是有值的(static),所以到现在你是不是觉得TouchTarget的在事件传递过程中扮演是什么角色了,它其实就是记录着事件传递过程中成功消费这个事件的所有的view和他的父亲,mFirstTouchTarget最上面的那个TouchTarget对象记录着事件被消费的childview的信息,通过链表来联系。接下来在这段代码中还有87到94 ,这段代码时处理多指的代码,当然这里不做多分析。当然这里主要说明了down的事件被孩子dispatchTouchEvent()返回true的时候,那么如果孩子返回false呢?接下向下看96到98 ,我们可以看到mFirstTouchTarget= null的时候,即这个down事件没有被孩子dispatchTouchEvent()的返回true的时候。回去调用handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
dispatchTransformedTouchEvent的源码我们也贴出来,我们看到此时去调用了view.dispatchToucEvent(),这里我简单说下view.dispatchTouchEvent()简单源码,首先判断这个控件有没有onTouchListener,如果是则直接return true 并且调用相应的回调方法,否则,则调用本身的onTouchEvent()的方法,所以这里我们是viewgroup默认是没有onTouchListener,所以这个时候会去调用onTouchEvent(),根据其中的返回值来说明是否被down事件是否被消费了。最后将handle给返回。
好了,上面上基本将down的在viewgroup的dispatchTouchEvent()如何传递梳理一下,接下来我们看下move和up,其实move和up差不多的。
move的时候,如果mFirstTouchTarget!=null,是可以进入到是否判断拦截的。
mFirstTouchTarget!=null,如果不是不拦截的,直接来到106行,调用了while循环去遍历孩子,判断谁之前消费了他,紧接再叫他处理dispatchTransformedTouchEvent(event),但是如果拦截呢?此时有113—120代码,可以看出来此时mFirstTouchTarget=null了,但是,此时谁去处理这个事件呢?理论上是交给activity的onTouchEvent()去处理,但是如果在消费这个事件的父亲上面有的可能针对这个事件去消费的话,则会交它去消费的( 特殊情况下,需要手动实现,不做考虑)。这个系列的事件此后的事件都默认不会交给这个child去处理了。有种感觉就是你要做,就得从头到尾都处理好这件事,如果其中有个事件你没做好,对不起,以后你都没机会了,不管你前面处理多好多好。
接下来我们来看个例子。例子1
一方面来验证我们前面所说的,另一方面来加深我们对之前的理解。我们自定义两个view ,一个ViewGroup,和一个View,代码如下:
TouchView extends View
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int actionCode = ev.getAction()&MotionEvent.ACTION_MASK;
Log.e("TAG", "TouchView dispatchTouchEvent ,actionCode : "+actionCode);
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int actionCode = event.getAction();
Log.e("TAG","TouchView onTouchEvent"+", actionCode :" +actionCode);
return true;
}
TouchViewGroup extends ViewGroup
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int actionCode = ev.getAction()& MotionEvent.ACTION_MASK;
Log.e("TAG", "TouchViewGroup dispatchTouchEvent ,actionCode"+actionCode);
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int actionCode = event.getAction()&MotionEvent.ACTION_MASK;
Log.e("TAG","TouchViewGroup onTouchEvent"+", actionCode :" +actionCode);
return true;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int actionCode = ev.getAction()&MotionEvent.ACTION_MASK;
Log.e("TAG", "TouchViewGroup onInterceptTouchEvent ,actionCode: "+actionCode);
return super.onInterceptTouchEvent(ev);
}
另外我们在MainActivity重写了其中的onTouchEvent
@Override
public boolean onTouchEvent(MotionEvent event) {
int actionCode = event.getAction() & MotionEvent.ACTION_MASK;
Log.e("TAG","MainActivity onTouchEvent actionCode :" + actionCode);
return super.onTouchEvent(event);
}
最后再贴下布局文件
<com.example.rrtoyewx.person.TouchViewGroup
android:id="@+id/tv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white">
<com.example.rrtoyewx.person.TouchView
android:layout_width="match_parent"
android:layout_height="match_parent">
</com.example.rrtoyewx.person.TouchView>
</com.example.rrtoyewx.person.TouchViewGroup>
这是最简单的事件传递机制,我们在最上层的TouchView的onTouchEvent(),返回值为true,成功消费这个系列的事件。接下来我们log的打印的情况
可以看到,正如我们上面分析的一样的,当事件传递过来的时候,整体传递的过程为 TouchViewGroup.dispatchTouchEvent—->TouchViewGroup.onInterceptTouchEvent——>TouchView.dispatchTouchEvent—>TouchView.onTouchEvent.这也是最简单的事件传递。
接下来我们就来修改下代码
例子2 第一,我们在TouchViewGroup的onInterceptTouchEvent的返回值改为true;因为默认是false的,修改如下
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int actionCode = ev.getAction()&MotionEvent.ACTION_MASK;
Log.e("TAG", "TouchViewGroup onInterceptTouchEvent ,actionCode: "+actionCode);
return true;
}
此时应该正如我们上面所分析的一样,最终会交给了activity的onTouchEvent(),因为down的时候,把事件交给上面的view的处理时,TouchView没有接受到事件,就交给了TouchViewGroup,TouchViewGroup虽然处理了事件,但是并没有消费事件,所以TouchViewGroup的中mFirstTouchTarget并没有赋值。看下log是不是正如我们所说的
看了log 确实正如我们所说的,
例子3,前面这两个似乎都挺容易理解的,接下来我们慢慢加深点,看大家能不能理解了,我们在TouchViewGroup的onInterceptTouchEvent的方法中,对down事件不进行拦截,我们对第一个move事件也不拦截,但是呢,我们对之后的事件开始拦截。代码修改如下:
public boolean onInterceptTouchEvent(MotionEvent ev) {
int actionCode = ev.getAction()&MotionEvent.ACTION_MASK;
Log.e("TAG", "TouchViewGroup onInterceptTouchEvent ,actionCode: "+actionCode);
switch (actionCode){
case MotionEvent.ACTION_DOWN:
return false;
case MotionEvent.ACTION_MOVE:
if(mFirst){
mFirst = false;
return false;
}else{
return true;
}
default:
mFirst = true;
return true;
}
}
在看log之前,我们先分析下,down事件是可以传递下去,而且被消费了,mFirstTouchTarget被赋值了,此时当一个move事件来,也是可以被消费了,但是第二个move事件的时候,在判断是否拦截了,这个时候这个时间就没办了消费了,并且从之前的代码说明了此时mFirstTouchTarget = null;所以接下来事件都不会交到里面的那个TouchView了,全部会交给谁上次消费了这个事件,但是它的上次层的TouchViewGroup依然可以处理到这个事件,不像例子2,不会直接交给了activity里面,这点尤为重要
似乎log正如我们上面所分析的,第一个down事件成功被消费后,第二个事件来了似乎和我们上面所说的不太一样,并且你发现此时的actioncode变成3,也就意味着这是一个cancle的事件,其实这点我也没找到相应的转化代码,但是我的理解的是,viewgroup的里面的部分源码
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
}
可以看到先交给他去处理,接下来拦截,所以这个事件是无效。希望懂的童鞋的能在下面的留言给我答案。我真的很想知道了。。。。
但是这个无关大碍。接下来的log证实我们上面所说的,以后的事件他都会交给TouchViewGroup(TouchView的上面的view)的去处理,尽管它并没有消费掉,因为此时TouchViewGroup的mFirstTouchTarget==null,但是TouchViewGroup他上层的mFirstTouchTarget是有值的,并指向TouchViewGroup的。所以每次都会传到这儿的,不会像例子2种所有的viewGroup的mFirstTouchTarget都是==null的。这点希望大家慢慢体会下。
这个时候,我们就可以做些事情了,比如TouchViewGroup拦截了事件,可以针对其中的某一些事件做些消费。代码实现也很简单。只需要我们我TouchViewGroup里面的onTouchEvent()做响应处理就可以了。
例子4 ,我们似乎忘了一个requestDisallowInterceptTouchEvent方法了。接下来我们再改下代码
TouchViewGroup的代码
public boolean dispatchTouchEvent(MotionEvent ev) {
int actionCode = ev.getAction()& MotionEvent.ACTION_MASK;
Log.e("TAG", "TouchViewGroup dispatchTouchEvent ,actionCode"+actionCode);
switch (actionCode){
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
requestDisallowInterceptTouchEvent(true);
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int actionCode = event.getAction()&MotionEvent.ACTION_MASK;
Log.e("TAG","TouchViewGroup onTouchEvent"+", actionCode :" +actionCode);
return super.onTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int actionCode = ev.getAction()&MotionEvent.ACTION_MASK;
Log.e("TAG", "TouchViewGroup onInterceptTouchEvent ,actionCode: "+actionCode);
switch (actionCode){
case MotionEvent.ACTION_DOWN:
return true;
case MotionEvent.ACTION_MOVE:
return true;
default:
return true;
}
}
大家可以猜下log的打印情况(其实这是一种拦截的错误的写法),至于为什么会出现下面的log,我上面已经说出来,
例子5 ,正确的写法
TouchViewGroup的代码
public boolean dispatchTouchEvent(MotionEvent ev) {
int actionCode = ev.getAction()& MotionEvent.ACTION_MASK;
Log.e("TAG", "TouchViewGroup dispatchTouchEvent ,actionCode"+actionCode);
switch (actionCode){
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
requestDisallowInterceptTouchEvent(true);
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int actionCode = event.getAction()&MotionEvent.ACTION_MASK;
Log.e("TAG","TouchViewGroup onTouchEvent"+", actionCode :" +actionCode);
return super.onTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int actionCode = ev.getAction()&MotionEvent.ACTION_MASK;
Log.e("TAG", "TouchViewGroup onInterceptTouchEvent ,actionCode: "+actionCode);
switch (actionCode){
case MotionEvent.ACTION_DOWN:
return false;
case MotionEvent.ACTION_MOVE:
return true;
default:
return true;
}
}
我上面说了,要想用requestDisallowInterceptTouchEvent的有效。必须在TouchViewGroup的不拦截down事件。我们看下log
成功的反拦截了。
例子6 ,其实这是我以前犯了一错误,其实那样是拦截不到,我贴下代码,大家可以看下,如果你看懂了,那么基本上ViewGroup的事件传递基本上都看懂了。
TouchViewGroup的代码
public boolean dispatchTouchEvent(MotionEvent ev) {
int actionCode = ev.getAction()& MotionEvent.ACTION_MASK;
Log.e("TAG", "TouchViewGroup dispatchTouchEvent ,actionCode"+actionCode);
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int actionCode = event.getAction()&MotionEvent.ACTION_MASK;
Log.e("TAG","TouchViewGroup onTouchEvent"+", actionCode :" +actionCode);
return super.onTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int actionCode = ev.getAction()&MotionEvent.ACTION_MASK;
Log.e("TAG", "TouchViewGroup onInterceptTouchEvent ,actionCode: "+actionCode);
switch (actionCode){
case MotionEvent.ACTION_DOWN:
return false;
case MotionEvent.ACTION_MOVE:
return true;
default:
return true;
}
}
什么都没做,就是拦截move事件
接下来就是TouchView的代码
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int actionCode = ev.getAction() & MotionEvent.ACTION_MASK;
Log.e("TAG", "TouchView dispatchTouchEvent ,actionCode : " + actionCode);
switch (actionCode) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
getParent().requestDisallowInterceptTouchEvent(true);
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int actionCode = event.getAction();
Log.e("TAG", "TouchView onTouchEvent" + ", actionCode :" + actionCode);
return true;
}
或者是这样
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int actionCode = ev.getAction() & MotionEvent.ACTION_MASK;
Log.e("TAG", "TouchView dispatchTouchEvent ,actionCode : " + actionCode);
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int actionCode = event.getAction();
Log.e("TAG", "TouchView onTouchEvent" + ", actionCode :" + actionCode);
switch (actionCode) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
getParent().requestDisallowInterceptTouchEvent(true);
break;
}
return true;
}
打印的结果都是这样子的
结果却是没拦截到。是不是觉得很奇怪,不急,我在改点东西,我让第一move事件不拦截,其他都拦截,就像我们上面写的代码一样
TouchViewGroup的代码
public boolean onInterceptTouchEvent(MotionEvent ev) {
int actionCode = ev.getAction()&MotionEvent.ACTION_MASK;
Log.e("TAG", "TouchViewGroup onInterceptTouchEvent ,actionCode: "+actionCode);
switch (actionCode){
case MotionEvent.ACTION_DOWN:
return false;
case MotionEvent.ACTION_MOVE:
if(mFirst){
mFirst = false;
return false;
}else{
return true;
}
default:
mFirst = true;
return true;
}
}
此时log,你会惊讶的,因为此时居然可以反拦截了
到这儿,似乎你有点纳闷了,为什么跳过了一个down的事件的就可以拦截了,其实是顺序的问题。你要想想,执行的顺序是什么?先执行TouchViewGroup dispatchTouchEvent ,这个时候已经在判断FLAG_DISALLOW_INTERCEPT的标志位,并没有改变,直到你在TouchView的dispatchTouchEvent去改变,这个时候,已经是然并卵了。最好就是在TouchView的down的时候就开始反拦截。然后根据需求来在决定在以后需不需要不反拦截,而不是一上来不反拦截,而是到了该拦截再去拦截,这个时候已经迟了。
ok,说了这么多的dispatchTouchEvent(),你似乎还想说onTouchEvent()还没说,其实呢。ViewGroup中的onTouchEvent()跟view的onTouchEvent()是一样,我们放在下面来说。
View中MotionEvent的介绍
view的就两个方法比较重要,其实view事件传递是比较简单的。
dispatchTouchEvent()
onTouchEvent()
首先看下dispatchTouchEvent()的源码,我们直接跳重点看吧
if (onFilterTouchEventForSecurity(event)) {
//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;
}
}
dispatchTouchEvent的其他的代码,都是无关紧要的。直接看这段,可以看到先判断了是否存在了touchlistener,如果有,则执行了
mOnTouchListener的ontouch的方法。根据返回值,决定需不要执行onTouchEvent(event),也就是说OnTouchListener的ontouch的优先级高于onTouchEvent()的方法。是不是觉得view的dispatchTouchEvent特别简单。其实这很正常,想想view是负责处理和消费事件的,所以它的dispatchTouchEvent出了分发给自己,还能怎么办?你可能会问这是不是意味着onTouchEvent()会很难,实际上它的onTouchEvent()还真不难.不信接着看下来
onTouchEvent()核心代码
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
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 (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
setPressed(true, x, y);
}
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)) {
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;
if (performButtonActionOnTouchDown(event)) {
break;
}
// Walk up the hierarchy to determine if we're inside a scrolling container.
boolean isInScrollingContainer = isInScrollingContainer();
// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
checkForLongClick(0);
}
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_MOVE:
drawableHotspotChanged(x, y);
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
setPressed(false);
}
}
break;
}
return true;
}
看了上面的代码是不是觉得很操蛋,但是你注意到了最后return true。这个是不是很有趣,我不管里面具体实现是什么,只要你进入了if里面就时代表你处理并消费了事件。好的,我们看下进入if的条件viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE,我翻译直白的点,其实就是判断这个view时可点击的,可长按的,还有一个我也不知道,好的,记住前两个就行了。换句话说,只要这个view是可点击的,可长按的,这两个属性怎么设置的呢?第一种,当你给他她们设置了setOnClickListener,或者setOnLongClickListener(),这个时候已经设置可点击了或者可长按的
view中 setOnLongClickListener的源码。
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
if (!isLongClickable()) {
setLongClickable(true);
}
getListenerInfo().mOnLongClickListener = l;
}
从设置监听我们就可以看到第二种方法setLongClickable()和setClickable();第三种方法,xml android:clickable="true"
android:longClickable="true"
默认的Clickable或者LongClickable为true还是有一些控件的,比如button,checkbox等。
好了上面已经说明了如何进入这个if语句中,也就是如何让view去消费这个事件,关于if的里面的代码逻辑,其实就是在up的时候判断是否可以触发点击事件,在move的时候是否能触发长按事件。
最后说一点,判断一个view能否消费事件仅仅只是判断是否可点击或者可长按的,与是不是enable没关系。
总结
1 viewgroup的dispatchTouchEvent()的伪代码如下
public boolean dispatchTouchEvent(MotionEvent event){
boolean handle = false;
if(!onInterceptTouchEvent(event) || requestDisallowInterceptTouchEvent(event)&& event.getActionCode != MotionEvent.ACTION_DOWN){
handle = child.dispatchEvent();
}
if(!handle){
handle = onTouchEvent();
}
return handle;
}
2 viewgroup一开始就拦截向child传递的motionEvent,如果他onTouchEvent()没有消费掉事件,则这个系列以后的事件都没办法传递到它这儿
3 viewgroup一开始不拦截向child传递的motionEvent,并且child成功的消费了motionEvent,则当拦截以后,这个系列的事件以后的事件都能能传到viewgroup这个,尽管它并没有消费掉这个事件
4 如果child想反拦截,那么首先第一必须得down事件传递下去并消费掉,使得接下来的事件能够传递下去。
5 反拦截的顺序一定要注意先后,详情请好好看看viewgroup的例子6,
6 view中onTouchListener的执行顺行要先于onClickListener和onLongChilckListener,并且onTouchListener中onTouch()返回值决定需不需要执行onClickListener和onLongChilckListener。
7 view是否消费事件,仅仅由自己的clickable属性和LongClickable的属性有关。