此文接于上一篇完全理解Android TouchEvent事件分发机制(一)
可以看出来,事件一旦被某一层消费掉,其它层就不会再消费了
到这里其实对事件分发的机制就有个大概了解,知道里面的原理是怎么回事。
下面就让我们来去梳理一下这个事件分发所走的逻辑。
我们仔细思考一下,为什么有的事件有UP有的没有?
为什么Up和Down的顺序不同呢?
为什么要按照这个顺序执行呢?
这个例子主要是为了说明分发、拦截、消费的流程
以例一为例,在每个 View 都不拦截 down 事件的情况下,down 事件是这样传递的
super.dispatchTouchEvent方法,上面我们介绍过,
这个方法内部实际上是调用的onTouchEvent方法
所以最后的输出日志顺序就是从父到子依次调用分发和拦截,然后从子到父依次调用消费。
例二也是同理,区别在于
当Father拿到事件的时候,选择了拦截下来不再询问其他,
但是Father也没消费,直接又还回给了Grandpa,
Grandpa同样也没有消费这个事件。
所以最终的顺序就是,从Grandpa到Father再返回Grandpa就结束了,没有经过LogImageView。
例三的情况就不太一样了
- 当Grandpa->Father->LogImageView 传递到LogImageView时,LogImageView不消费又返回给了Father,Father在onTouchEvent消费掉了事件。
- 然后反馈给Father说事件已经消费。,就等于parent.dispatchTouchEvent返回true给上一级的Grandpa,
Grandpa不会再调用grandpa.onTouchEvent方法。
从这里我们可以总结出来:
dispatchTouchEvent返回值的作用是用于标志这个事件是否“消费了”,
无论是自己或者下面的子一级用掉了都算是消费掉。
再如这个例子中,如果我们让LogImageView消费掉事件,
那么Father收到LogImageView的消息后,也会调用parent.dispatchTouchEvent返回true给Grandpa,
所以这个方法返回值的true是只要用掉就行,无论自己还是下面某一级,
而非我把事件传递下去就是true了,下面没人消费最终其实还是返回false的。
至此,我们来总结一下这三个方法的大致作用:
- dispatchTouchEvent方法内容里处理的是分发过程。可以理解为从Grandpa->Father->LogImageView一层层分发的动作
dispatchTouchEvent的返回值则代表是否将事件分发出去用掉了,自己用或者给某一层子级用都算分发成功。比如Father消费了事件,或者他发出去给的LogImageView消费了事件,这两种情况下B的dispatchTouchEvent都会返回true给Grandpa
onInterceptTouchEvent会在第一轮从父到子的时候在分发时调用,以它去决定是否拦截掉此事件不再向下分发。如果拦截下来,就会调用自己的onTouchEvent处理;如果不拦截,则继续向下传递
onTouchEvent代表消费掉事件。方法内容是具体的事件处理方法,如何处理点击滑动等。
onTouchEvent的返回值则代表对上级的反馈,通知这个东西我用掉啦,然后他的父级就会让分发方法也返回true
下面我们来解释为什么例一、二中没有Up,而例三中有
一个Action_DOWN,一个ACTION_UP,若干个ACTION_MOVE,才构成完整的事件。
前俩例子里为什么没有Up呢,很好理解,
因为他们都没有消费事件啊,所以他们只有DOWN事件,因此只有Down,没Up。
例三做类比,Father消费掉了这个事件
流程依然是先从Grandpa开始分配
(grandpa.dispatchTouchEvent)
(grandpa.onInterceptTouchEvent 判断是否拦截)Grandpa还是没拦下来,继续分发事件(grandpa不拦截,然后调用child.dispatchTouchEvent)
事件到了Father,Father进行了消费。(parent没有调用拦截方法)
Father调用onTouchEvent返回true消费掉事件,完成了整个行为。
【例四】
在Grandpa类的onInterceptTouchEvent中添加个判断,
如果动作是UP就return true拦截掉,DOWN则不拦截和之前一样
打印如下:
04-04 07:16:43.353 2344-2344/com.shanlovana.rcimageview E/ShanCanCan: GrandPaViewGroup dispatchTouchEvent Event 0
04-04 07:16:43.355 2344-2344/com.shanlovana.rcimageview E/ShanCanCan: GrandPaViewGroup onInterceptTouchEvent Event 0
04-04 07:16:43.355 2344-2344/com.shanlovana.rcimageview E/ShanCanCan: FatherViewGroup dispatchTouchEvent Event 0
04-04 07:16:43.356 2344-2344/com.shanlovana.rcimageview E/ShanCanCan: FatherViewGroup onInterceptTouchEvent Event 0
04-04 07:16:43.356 2344-2344/com.shanlovana.rcimageview E/ShanCanCan: LogImageView dispatchTouchEvent Event 0
04-04 07:16:43.357 2344-2344/com.shanlovana.rcimageview E/ShanCanCan: LogImageView onTouchEvent Event 0
04-04 07:16:43.357 2344-2344/com.shanlovana.rcimageview E/ShanCanCan: FatherViewGroup onTouchEvent Event 0
04-04 07:16:43.392 2344-2344/com.shanlovana.rcimageview E/ShanCanCan: GrandPaViewGroup dispatchTouchEvent Event 1
04-04 07:16:43.392 2344-2344/com.shanlovana.rcimageview E/ShanCanCan: GrandPaViewGroup onInterceptTouchEvent Event 1
04-04 07:16:43.392 2344-2344/com.shanlovana.rcimageview E/ShanCanCan: FatherViewGroup dispatchTouchEvent Event 3
04-04 07:16:43.392 2344-2344/com.shanlovana.rcimageview E/ShanCanCan: FatherViewGroup onTouchEvent Event 3
前面Down行为和例三一样,后面就不同了
UP流程变了,然后多了个CANCEL的动作
这里我们可以理解为
GrandPa调用dispatchTouchEvent分发UP事件
GrandPa调用onInterceptTouchEvent返回true,拦截UP,DOWN事件则是正常的传递
FatherView调用dispatchTouchEvent分发CANCEL动作
FatherView使用CANCEL动作调用onTouchEvent方法,结束
大概也就了解的差不多了,还剩一个TouchTarget目标的概念,
为什么例三中Up和Down流程不同?
回头去看完整源码:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 1.每次起始动作就重置之前的TouchTarget等参数
cancelAndClearTouchTargets(ev);
resetTouchState();
}
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
// 2.如果是起始动作才拦截,或者已经有人消费掉了事件,再去判断拦截
// 起始动作是第一次向下分发的时候,每个view都可以决定是否拦截,然后进一步判断是否消费,很好理解
// 如果有人消费掉了事件,那么也拦截~ 就像例四中的情况,也可以再次判断是否拦截的
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// 3.这里可以设置一个disallowIntercept标志,如果是true,就是谁收到事件后都不准拦截!!!
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
// 4.如果未拦截,只有Down动作才去子一级去找目标对象~
// 因为找目标这个操作只有Down中才会处理
if (actionMasked == MotionEvent.ACTION_DOWN ) {
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
for (int i = childrenCount - 1; i >= 0; i--) {
newTouchTarget = getTouchTarget(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
}
}
if (mFirstTouchTarget == null) {
// 5.把自己当做目标,去判断自己的onTouchEvent是否消费
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// 6.如果有人消费掉了事件,找出他~
TouchTarget target = mFirstTouchTarget;
while (target != null) {
// 7.消费对象信息其实是一个链式对象,记载着一个一个传递的人的信息,遍历调用它child的分发方法
final TouchTarget next = target.next;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
target = next;
}
}
}
return handled;
}
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;
}
就是判断如果没child了(是ViewGroup但是没子控件,或者自己就是View),
如果没child,就调用View的dispatchTouchEvent方法,
实质就是调用onTouchEvent判断是否消费掉事件
如果有child,就调用child的dispatchTouchEvent将事件一层层向下分发
例三四中的复杂情况
其中关键主要在于多了一个TouchTarget的处理.
向下传递的核心主要是在于dispatchTransformedTouchEvent方法
第一轮动作的Down时,只要不拦截,就会在注释4代码处遍历所有child调用该方法层层传递下去
而后续其他动作时,就会进入注释6代码条件,然后遍历TouchTarget中的信息用该方法层层分发
注意不要误解:
第一次Down的时候会for循环所有child
第二轮Up的时候也会while(target.next)的迭代循环挨个判断,但是next是遍历同级,不是子级
dispatchTrancformTouchEvent(target.child)这里的.child才是向子一级一层一层分发传递的地方.
这个TouchTarget对象,主要保存的是传递路线信息,它是一个链式结构
不过这个路线不是Grandpa->Father->LogImageView的一个单子,而是每个ViewGroup都会保存一个向下的路线信息.
Cancel部分
dispatchTrancformTouchEvent中会判断,如果cancel=true动作,
则会把动作改成ACTION_CANCEL一层一层的传下去.
注意:
onTouch和onTouchEvent有什么区别,又该如何使用?
从源码中可以看出,这两个方法都是在View的dispatchTouchEvent中调用的,onTouch优先于onTouchEvent执行。如果在onTouch方法中通过返回true将事件消费掉,onTouchEvent将不会再执行。
另外需要注意的是,onTouch能够得到执行需要两个前提条件,第一mOnTouchListener的值不能为空,第二当前点击的控件必须是enable的。因此如果你有一个控件是非enable的,那么给它注册onTouch事件将永远得不到执行。对于这一类控件,如果我们想要监听它的touch事件,就必须通过在该控件中重写onTouchEvent方法来实现。
到这里,我们的事件传递就全部讲解完成了,下面让我们看看他的实际用途吧。
用途:
滑动事件冲突的解决方法:
外部拦截法
外部拦截法是指在有点击事件时都要经过父容器,那么在父容器时如果需要拦截就拦截自己处理,不需要则传递给下一层进行处理,
下面看个例子
首先定义一个水平滑动的HorizontalScrollView,看主要代码
主要的拦截是需要重写onInterceptTouchEvent
此示例是借鉴于lylodlig的一篇文章,在此感谢。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
//down事件不拦截,否则无法传给子元素
intercepted = false;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
intercepted = true;
}
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
//水平滑动则拦截
if (Math.abs(deltaX) > Math.abs(deltaY) + 5) {
intercepted = true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
//不拦截,否则子元素无法收到
intercepted = false;
break;
}
//因为当ViewGroup中的子View可能消耗了down事件,在onTouchEvent无法获取,
// 无法对mLastX赋初值,所以在这里赋值一次
mLastX = x;
mLastY = y;
mLastYIntercept = y;
mLastXIntercept = x;
return intercepted;
}
在down事件不需要拦截,返回false,否则的话子view无法收到事件,将全部会由父容器处理,这不是希望的;up事件也要返回false,否则最后子view收不到
看看move事件,当水平滑动距离大于竖直距离时,代表水平滑动,返回true,由父类来进行处理,否则交由子view处理。这里move事件就是主要的拦截条件判断,如果你遇到的不是水平和竖直的条件这么简单,就可以在这里进行改变,比如,ScrollView嵌套了ListView,条件就变成,当ListView滑动到底部或顶部时,返回true,交由父类滑动处理,否则自身ListView滑动。
在onTouchEvent中主要是做的滑动切换的处理
@Override
public boolean onTouchEvent(MotionEvent event) {
mVelocityTracker.addMovement(event);
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (getScrollX() < 0) {
scrollTo(0, 0);
}
scrollBy(-deltaX, 0);
break;
case MotionEvent.ACTION_UP:
int scrollX = getScrollX();
mVelocityTracker.computeCurrentVelocity(1000);
float xVelocityTracker = mVelocityTracker.getXVelocity();
if (Math.abs(xVelocityTracker) > 50) {//速度大于50则滑动到下一个
mChildIndex = xVelocityTracker > 0 ? mChildIndex - 1 : mChildIndex + 1;
} else {
mChildIndex = (scrollX + mChildWith / 2) / mChildWith;
}
mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1));
int dx = mChildIndex * mChildWith - scrollX;
smoothScrollBy(dx, 0);
mVelocityTracker.clear();
break;
}
mLastY = y;
mLastX = x;
return true;
}
在这个嵌套一个普通的ListView,这样就可以解决水平和竖直滑动冲突的问题了。
内部拦截法
内部拦截法是父容器不拦截任何事件,所有事件都传递给子view,如果需要就直接消耗掉,不需要再传给父容器处理
下面重写一个ListView,只需要重写一个dispatchTouchEvent方法就OK
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
//子View的所有父ViewGroup都会跳过onInterceptTouchEvent的回调
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (Math.abs(deltaX) > Math.abs(deltaY) + 5) {//水平滑动,使得父类可以执行onInterceptTouchEvent
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(ev);
}
在down事件调用getParent().requestDisallowInterceptTouchEvent(true),这句代码的意思是使这个view的父容器都会跳过onInterceptTouchEvent,在move中判断如果是水平滑动就由父容器去处理,父容器只需要把之前的onInterceptTouchEvent改为下面那样,其他不变.
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int x = (int) ev.getX();
int y = (int) ev.getY();
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
mLastX = x;
mLastY = y;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
return true;
}
return false;
} else {
//如果是非down事件,说明子View并没有拦截父类的onInterceptTouchEvent
//说明该事件交由父类处理,所以不需要再传递给子类,返回true
return true;
}
}
解决这些问题的基本的思想就是这些,希望可以帮助到您。
Github源码下载地址