此文接于上一篇完全理解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
一个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,这样就可以解决水平和竖直滑动冲突的问题了。
<com.shanlovana.rcimageview.HorizontalScrollViewEx android:layout_width="match_parent" android:layout_height="200dp"> <ListView android:id="@+id/listView" android:layout_width="match_parent" android:layout_height="match_parent" /> <Button android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/holo_blue_bright" android:text="2" /> <Button android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/holo_green_dark" android:text="3" /> </com.shanlovana.rcimageview.HorizontalScrollView>内部拦截法是父容器不拦截任何事件,所有事件都传递给子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源码下载地址