Android学习一段时间,需求作多了必然会遇到滑动冲突问题,好比在一个ScrollView中要嵌套一个地图View,这时候触摸移动地图或者放大缩小地图就会变得不太准确甚至没有反应,这就是遇到了滑动冲突,ScrollView中上下滑动与地图的触摸手势发生冲突。想要解决滑动冲突就不得不提到Android的事件分发机制,只有吃透了事件分发,才能对滑动冲突的解决驾轻就熟。java
Android事件分发机制主要相关方法有如下三个:android
如下是这三个方法在Activity、ViewGroup和View中的存在状况:api
相关方法 | Activity | ViewGroup | View |
---|---|---|---|
dispatchTouchEvent | yes | yes | yes |
onInterceptTouchEvent | no | yes | no |
onTouchEvent | yes | yes | yes |
这三个方法都返回一个布尔类型,根据返回的不一样对事件进行不一样的分发拦截和响应。通常有三种返回true
、false
和super
引用父类对应方法。安全
dispatchTouchEvent 返回true:表示改事件在本层再也不进行分发且已经在事件分发自身中被消费了。
dispatchTouchEvent 返回 false:表示事件在本层再也不继续进行分发,并交由上层控件的onTouchEvent
方法进行消费。bash
onInterceptTouchEvent 返回true:表示将事件进行拦截,并将拦截到的事件交由本层控件 的onTouchEvent
进行处理。
onInterceptTouchEvent 返回false:表示不对事件进行拦截,事件得以成功分发到子View
。并由子View
的dispatchTouchEvent
进行处理。markdown
onTouchEvent 返回 true:表示onTouchEvent
处理完事件后消费了这次事件。此时事件终结,将不会进行后续的传递。
onTouchEvent 返回 false:事件在onTouchEvent
中处理后继续向上层View传递,且有上层View
的onTouchEvent
进行处理。app
除此以外还有一个方法也是常常用到的:ide
它的做用是子View用来通知父View不要拦截事件。下面先写一个简单的Demo
来看一下事件分发和传递:oop
这里的代码只是自定义了两个ViewGroup
和一个View
,在其对应事件分发传递方法中打印日志,来查看调用顺序状况,全部相关分发传递方法返回皆是super
父类方法。
例如: MyViewGroupA.java:布局
public class MyViewGroupA extends RelativeLayout { public MyViewGroupA(Context context) { super(context); } public MyViewGroupA(Context context, AttributeSet attrs) { super(context, attrs); } public MyViewGroupA(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { switch (ev.getAction()){ case MotionEvent.ACTION_DOWN: Log.d(getClass().getSimpleName(),"dispatchTouchEvent:ACTION_DOWN"); break; case MotionEvent.ACTION_MOVE: Log.d(getClass().getSimpleName(),"dispatchTouchEvent:ACTION_MOVE"); break; case MotionEvent.ACTION_UP: Log.d(getClass().getSimpleName(),"dispatchTouchEvent:ACTION_UP"); break; } return super.dispatchTouchEvent(ev); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { switch (ev.getAction()){ case MotionEvent.ACTION_DOWN: Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_DOWN"); break; case MotionEvent.ACTION_MOVE: Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_MOVE"); break; case MotionEvent.ACTION_UP: Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_UP"); break; } return super.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()){ case MotionEvent.ACTION_DOWN: Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_DOWN"); break; case MotionEvent.ACTION_MOVE: Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_MOVE"); break; case MotionEvent.ACTION_UP: Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_UP"); break; } return super.onTouchEvent(event); } } 复制代码
其余的代码都是相似的,这里再贴一下Acitivity里的布局:
<?xml version="1.0" encoding="utf-8"?> <com.example.sy.eventdemo.MyViewGroupA xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/viewGroupA" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/colorPrimary" tools:context=".MainActivity"> <com.example.sy.eventdemo.MyViewGroupB android:id="@+id/viewGroupB" android:layout_width="300dp" android:layout_height="300dp" android:layout_centerInParent="true" android:background="@android:color/white"> <com.example.sy.eventdemo.MyView android:id="@+id/myView" android:layout_width="200dp" android:layout_height="200dp" android:layout_centerInParent="true" android:background="@android:color/holo_orange_light" /> </com.example.sy.eventdemo.MyViewGroupB> </com.example.sy.eventdemo.MyViewGroupA> 复制代码
Demo中的Activity布局层级关系:
D/MainActivity: dispatchTouchEvent:ACTION_DOWN
D/MyViewGroupA: dispatchTouchEvent:ACTION_DOWN
D/MyViewGroupA: onInterceptTouchEvent:ACTION_DOWN
D/MyViewGroupB: dispatchTouchEvent:ACTION_DOWN
D/MyViewGroupB: onInterceptTouchEvent:ACTION_DOWN
D/MyView: dispatchTouchEvent:ACTION_DOWN
D/MyView: onTouchEvent:ACTION_DOWN
D/MyViewGroupB: onTouchEvent:ACTION_DOWN
D/MyViewGroupA: onTouchEvent:ACTION_DOWN
D/MainActivity: onTouchEvent:ACTION_DOWN
D/MainActivity: dispatchTouchEvent:ACTION_MOVE
D/MainActivity: onTouchEvent:ACTION_MOVE
D/MainActivity: dispatchTouchEvent:ACTION_UP
D/MainActivity: onTouchEvent:ACTION_UP
复制代码
结合日志能够大概看出(先只看ACTION_DOWN
事件):
事件的分发顺序:Activity-->MyViewGroupA-->MyViewGroupB-->MyView
自顶向下分发
事件的响应顺序:MyView-->MyViewGroupB-->MyViewGroupA-->Activity
自底向上响应消费
同时这里经过日志也发现一个问题:
ACTION_DOWN
事件有完整的从Activity到ViewGroup再到View的分发拦截和响应的运行日志,为何ACTION_MOVE
和ACTION_UP
事件没有?接着再测试一下以前提的requestDisallowInterceptTouchEvent
方法的使用。如今布局文件中将MyView添加一个属性android:clickable="true"
。此时在运行点击打印日志是这样的:
/-------------------ACTION_DOWN事件------------------
D/MainActivity: dispatchTouchEvent:ACTION_DOWN
D/MyViewGroupA: dispatchTouchEvent:ACTION_DOWN
D/MyViewGroupA: onInterceptTouchEvent:ACTION_DOWN
D/MyViewGroupB: dispatchTouchEvent:ACTION_DOWN
D/MyViewGroupB: onInterceptTouchEvent:ACTION_DOWN
D/MyView: dispatchTouchEvent:ACTION_DOWN
D/MyView: onTouchEvent:ACTION_DOWN
/-------------------ACTION_MOVE事件------------------
D/MainActivity: dispatchTouchEvent:ACTION_MOVE
D/MyViewGroupA: dispatchTouchEvent:ACTION_MOVE
D/MyViewGroupA: onInterceptTouchEvent:ACTION_MOVE
D/MyViewGroupB: dispatchTouchEvent:ACTION_MOVE
D/MyViewGroupB: onInterceptTouchEvent:ACTION_MOVE
D/MyView: dispatchTouchEvent:ACTION_MOVE
D/MyView: onTouchEvent:ACTION_MOVE
/-------------------ACTION_UP事件------------------
D/MainActivity: dispatchTouchEvent:ACTION_UP
D/MyViewGroupA: dispatchTouchEvent:ACTION_UP
D/MyViewGroupA: onInterceptTouchEvent:ACTION_UP
D/MyViewGroupB: dispatchTouchEvent:ACTION_UP
D/MyViewGroupB: onInterceptTouchEvent:ACTION_UP
D/MyView: dispatchTouchEvent:ACTION_UP
D/MyView: onTouchEvent:ACTION_UP
复制代码
这下ACTION_MOVE
和ACTION_UP
事件也有日志了。接下来在MyViewGroupB的onInterceptTouchEvent
的方法中修改代码以下:
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { switch (ev.getAction()){ case MotionEvent.ACTION_DOWN: Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_DOWN"); return false; case MotionEvent.ACTION_MOVE: Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_MOVE"); return true; case MotionEvent.ACTION_UP: Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_UP"); return true; } return false; } 复制代码
也就是拦截下ACTION_MOVE
和ACTION_UP
事件不拦截下ACTION_DOWN
事件,而后在运行查看日志:
/------------------ACTION_DOWN事件------------------------------
D/MainActivity: dispatchTouchEvent:ACTION_DOWN
D/MyViewGroupA: dispatchTouchEvent:ACTION_DOWN
D/MyViewGroupA: onInterceptTouchEvent:ACTION_DOWN
D/MyViewGroupB: dispatchTouchEvent:ACTION_DOWN
D/MyViewGroupB: onInterceptTouchEvent:ACTION_DOWN
D/MyView: dispatchTouchEvent:ACTION_DOWN
D/MyView: onTouchEvent:ACTION_DOWN
/------------------ACTION_MOVE事件-----------------------------
D/MainActivity: dispatchTouchEvent:ACTION_MOVE
D/MyViewGroupA: dispatchTouchEvent:ACTION_MOVE
D/MyViewGroupA: onInterceptTouchEvent:ACTION_MOVE
D/MyViewGroupB: dispatchTouchEvent:ACTION_MOVE
D/MyViewGroupB: onInterceptTouchEvent:ACTION_MOVE
/------------------ACTION_UP事件-------------------------------
D/MainActivity: dispatchTouchEvent:ACTION_UP
D/MyViewGroupA: dispatchTouchEvent:ACTION_UP
D/MyViewGroupA: onInterceptTouchEvent:ACTION_UP
D/MyViewGroupB: dispatchTouchEvent:ACTION_UP
D/MyViewGroupB: onTouchEvent:ACTION_UP
D/MainActivity: onTouchEvent:ACTION_UP
复制代码
根据日志可知ACTION_MOVE
和ACTION_UP
事件传递到MyViewGroupB就没有再向MyView传递了。接着在MyView的onTouchEvent方法中调用requestDisallowInterceptTouchEvent
方法通知父容器不要拦截事件。
@Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()){ case MotionEvent.ACTION_DOWN: getParent().requestDisallowInterceptTouchEvent(true); Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_DOWN"); break; case MotionEvent.ACTION_MOVE: Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_MOVE"); break; case MotionEvent.ACTION_UP: Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_UP"); break; } return super.onTouchEvent(event); } 复制代码
再次运行查看日志:
/------------------ACTION_DOWN事件------------------------------
D/MainActivity: dispatchTouchEvent:ACTION_DOWN
D/MyViewGroupA: dispatchTouchEvent:ACTION_DOWN
D/MyViewGroupA: onInterceptTouchEvent:ACTION_DOWN
D/MyViewGroupB: dispatchTouchEvent:ACTION_DOWN
D/MyViewGroupB: onInterceptTouchEvent:ACTION_DOWN
D/MyView: dispatchTouchEvent:ACTION_DOWN
D/MyView: onTouchEvent:ACTION_DOWN
/------------------ACTION_MOVE事件-----------------------------
D/MainActivity: dispatchTouchEvent:ACTION_MOVE
D/MyViewGroupA: dispatchTouchEvent:ACTION_MOVE
D/MyViewGroupB: dispatchTouchEvent:ACTION_MOVE
D/MyView: dispatchTouchEvent:ACTION_MOVE
D/MyView: onTouchEvent:ACTION_MOVE
/------------------ACTION_UP事件-------------------------------
D/MainActivity: dispatchTouchEvent:ACTION_UP
D/MyViewGroupA: dispatchTouchEvent:ACTION_UP
D/MyViewGroupB: dispatchTouchEvent:ACTION_UP
D/MyView: dispatchTouchEvent:ACTION_UP
D/MyView: onTouchEvent:ACTION_UP
复制代码
这时能够发现ACTION_MOVE
和ACTION_UP
事件又传递到了MyView中而且两个ViewGroup中都没有执行onInterceptTouchEvent
方法。 明显是requestDisallowInterceptTouchEvent
方法起了做用。可是又出现了两个新问题。
clickable="true"
以后ACTION_MOVE
和ACTION_UP
事件就会执行了?requestDisallowInterceptTouchEvent
方法是怎样通知父View不拦截事件,为何连onInterceptTouchEvent
方法也不执行了?想弄明白这些问题就只能到源码中寻找答案了。
在正式看源码以前先讲一个概念:事件序列
咱们常说的事件,通常是指从手指触摸到屏幕在到离开屏幕这么一个过程。在这个过程当中其实会产生多个事件,通常是以ACTION_DOWN
做为开始,中间存在多个ACTION_MOVE
,最后以ACTION_UP
结束。咱们称一次ACTION_DOWN-->ACTION_MOVE-->ACTION_UP
过程称为一个事件序列。
ViewGroup中有一个内部类TouchTarget,这个类将消费事件的View封装成一个节点,使得能够将一个事件序列的DOWN
、MOVE
、UP
事件构成一个单链表保存。ViewGroup中也有个TouchTarget
类型的成员mFirstTouchTarget
用来指向这个单链表头。在每次DOWN
事件开始时清空这个链表,成功消费事件后经过TouchTarget.obtain
方法得到一个TouchTarget
,将消费事件的View传入,而后插到单链表头。后续MOVE
、UP
事件能够经过判断mFirstTouchTarget
来知道以前是否有可以消费事件的View。
TouchTarget的源码:
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. //接受事件的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. //下一个TouchTarget的地址 public TouchTarget next; private TouchTarget() { } public static TouchTarget obtain(@NonNull View child, int pointerIdBits) { if (child == null) { throw new IllegalArgumentException("child must be non-null"); } 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() { if (child == null) { throw new IllegalStateException("already recycled once"); } synchronized (sRecycleLock) { if (sRecycledCount < MAX_RECYCLED) { next = sRecycleBin; sRecycleBin = this; sRecycledCount += 1; } else { next = null; } child = null; } } } 复制代码
接下来正式按照分发流程来阅读源码,从Activity的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
方法,onUserInteraction
方法在Activity中是一个空实现,在当前Activity下按下Home或者Back键时会调用此方法,这里不是重点,这里重点是关注下ACTION_DOWN
事件,ACTION_DOWN
类型事件的判断,在事件传递的逻辑中很是重要,由于每次点击事件都是以ACTION_DOWN
事件开头,因此ACTION_DOWN
事件又做为一次新的点击事件的标记。
紧接着看,在第二个if
判断中根据getWindow().superDispatchTouchEvent(ev)
的返回值决定了整个方法的返回。
若是getWindow().superDispatchTouchEvent(ev)
方法返回为true
则dispatchTouchEvent
方法返回true
,不然则根据Activity中的onTouchEvent
方法的返回值返回。
先来看Activity中的onTouchEvent
方法:
public boolean onTouchEvent(MotionEvent event) { if (mWindow.shouldCloseOnTouch(this, event)) { finish(); return true; } return false; } 复制代码
onTouchEvent
方法中根据window的shouldCloseOnTouch
方法决定返回的结果和是否finish当前Activity。进入抽象类Window
查看shouldCloseOnTouch
方法:
/** @hide */ public boolean shouldCloseOnTouch(Context context, MotionEvent event) { if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(context, event) && peekDecorView() != null) { return true; } return false; } 复制代码
这是个hide
方法,判断当前事件Event是不是ACTION_DOWN
类型,当前事件点击坐标是否在范围外等标志位,若是为true
就会返回到onTouchEvent
方法关闭当前Activity。
看完再回到dispatchTouchEvent
方法中,只剩下getWindow().superDispatchTouchEvent(ev)
方法,来看他啥时候返回true
啥时候返回false
。这里的getWindow
获取到Activity中的Window对象,调用Widnow
的superDispatchTouchEvent(ev)
方法,这个方法不在抽象类Window
当中,这里要去查看他的实现类PhoneWindow
。
@Override public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); } 复制代码
superDispatchTouchEvent
方法中又调用了mDecor.superDispatchTouchEvent
方法,这里的mDecor就是外层的DecorView
,superDispatchTouchEvent
方法:
public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); } 复制代码
方法中又调用了父类的dispatchTouchEvent
方法,DecorView继承自FrameLayout,而FrameLayout没有重写dispatchTouchEvent
方法因此也就是调用了其父类ViewGroup的dispatchTouchEvent
方法。
经过以上这一系列的调用,事件终于从Activity到PhoneWindow再到DecorView最终走到了ViewGroup的dispatchTouchEvent
方法中,接下来进入ViewGroup查看它的dispatchTouchEvent
方法。
@Override public boolean dispatchTouchEvent(MotionEvent ev) { ...... boolean handled = false; if (onFilterTouchEventForSecurity(ev)) { //-----------------代码块-1---------------------------------------------------------------- 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(); } //------------------代码块-1--完------------------------------------------------------------ //------------------代码块-2---------------------------------------------------------------- // 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; } //------------------代码块-2--完---------------------------------------------------------- // If intercepted, start normal event dispatch. Also if there is already // a view that is handling the gesture, do normal event dispatch. if (intercepted || mFirstTouchTarget != null) { ev.setTargetAccessibilityFocus(false); } // 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; //------------------代码块-3-------------------------------------------------------------- if (!canceled && !intercepted) { ...... if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { final int actionIndex = ev.getActionIndex(); // always 0 for down final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) : TouchTarget.ALL_POINTER_IDS; // Clean up earlier touch targets for this pointer id in case they // have become out of sync. removePointersFromTouchTargets(idBitsToAssign); 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(); 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 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. if (childWithAccessibilityFocus != null) { if (childWithAccessibilityFocus != child) { continue; } childWithAccessibilityFocus = null; i = childrenCount - 1; } if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); 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; } resetCancelNextUpFlag(child); 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; } // The accessibility focus didn't handle the event, so clear // the flag and do a normal dispatch to all children. ev.setTargetAccessibilityFocus(false); } if (preorderedList != null) preorderedList.clear(); } if (newTouchTarget == null && mFirstTouchTarget != null) { // Did not find a child to receive the event. // Assign the pointer to the least recently added target. newTouchTarget = mFirstTouchTarget; while (newTouchTarget.next != null) { newTouchTarget = newTouchTarget.next; } newTouchTarget.pointerIdBits |= idBitsToAssign; } } } //------------------代码块-3--完---------------------------------------------------------- //------------------代码块-4-------------------------------------------------------------- // Dispatch to touch targets. if (mFirstTouchTarget == null) { //mFirstTouchTarget为空说明没有子View响应消费该事件 // 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; } } //------------------代码块-4--完---------------------------------------------------------- ...... return handled; } 复制代码
ViewGroup的dispatchTouchEvent
方法比较长,虽然已经省略了一部分代码但代码仍是很是多,而且代码中存在不少if-else
判断,容易看着看着就迷失在if
与else
之间。因此这里把他分红了四块代码来看。不过在看这四块代码以前先看dispatchTouchEvent
方法中第一个if
判断:
boolean handled = false; if (onFilterTouchEventForSecurity(ev)){ ...... } 复制代码
这里初始化的handled
就是dispatchTouchEvent
方法最后的返回值,onFilterTouchEventForSecurity
这个方法过滤了认为不安全的事件,方法里主要是判断了view和window是否被遮挡,dispatchTouchEvent
方法中全部的分发逻辑都要在onFilterTouchEventForSecurity
返回为true
的前提之下,不然直接返回handled
即为false
。
接下来看第一段代码:
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
事件被认为是一个新的事件序列开始,因此重置touch状态,将mFirstTouchTarget
链表置空。这里能够进resetTouchState
方法看下,方法中除了重置了一些状态还调用了clearTouchTargets
方法清空链表。
private void resetTouchState() { clearTouchTargets(); resetCancelNextUpFlag(this); mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; mNestedScrollAxes = SCROLL_AXIS_NONE; } /** * Clears all touch targets. */ private void clearTouchTargets() { TouchTarget target = mFirstTouchTarget; if (target != null) { do { TouchTarget next = target.next; target.recycle(); target = next; } while (target != null); mFirstTouchTarget = null; } } 复制代码
接着看到代码块2:
// Check for interception. //检查是否拦截事件 final boolean intercepted; //是ACTION_DOWN事件或者mFirstTouchTarget不为空进入if if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { //继续判断是否在调用了requestDisallowInterceptTouchEvent(true)设置了禁止拦截标记 final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; //设置禁止拦截设标记disallowIntercept为true,!disallowIntercept即为false if (!disallowIntercept) { //根据ViewGroup的nInterceptTouchEvent(ev)方法返回是否拦截 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. //不是ACTION_DOWN事件或者mFirstTouchTarget=null,就拦截 intercepted = true; } 复制代码
这段代码中主要是判断是否对事件进行拦截,intercepted
是拦截标记,true
表明拦截,false
表示不拦截。这里首先判断是事件类型是DOWN
或者mFirstTouchTarget
不等于空(不等于空说明有子View消费了以前的DOWN
事件),知足这个条件,就进入if进一步判断,不然直接设置intercepted
为false
不拦截。在if中判断FLAG_DISALLOW_INTERCEPT
这个标记位,这个标记位就是在requestDisallowInterceptTouchEvent()
方法中设置的。这里跳到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); } } 复制代码
看到requestDisallowInterceptTouchEvent
方法里根据disallowIntercept
进行不一样位运算,mGroupFlags
默认为0,FLAG_DISALLOW_INTERCEPT
为0x80000
,若是传入设置为true
,则进行或运算,mGroupFlags
结果为0x80000
,再回到代码块2里和FLAG_DISALLOW_INTERCEPT
作与运算结果仍为0x80000
,此时不等于0。反之传入false
,最终位运算结果为0。也就是说调用requestDisallowInterceptTouchEvent
方法传入true
致使disallowIntercep
为true
,进而致使if
条件不知足,使得intercepted
为false
此时对事件进行拦截。反之,则进入if
代码块调用onInterceptTouchEvent(ev)
方法,根据返回值来决定是否拦截。
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. View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus() ? findChildWithAccessibilityFocus() : null; //再判断事件类型是DOWN事件继续执行if代码块,这里的三个标记分别对应单点触摸DOWN多点触摸DOWN和鼠标移动事件 if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { final int actionIndex = ev.getActionIndex(); // always 0 for down final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) : TouchTarget.ALL_POINTER_IDS; // Clean up earlier touch targets for this pointer id in case they // have become out of sync. removePointersFromTouchTargets(idBitsToAssign); //这里拿到子VIew个数 final int childrenCount = mChildrenCount; //循环子View找到能够响应事件的子View将事件分发 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(); 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 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. if (childWithAccessibilityFocus != null) { if (childWithAccessibilityFocus != child) { continue; } childWithAccessibilityFocus = null; i = childrenCount - 1; } //这个子View没法接受这个事件或者事件点击不在这个子View内就跳过此次循环 if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; } //到这里说明这个子View能够处理该事件,就到TochTarget链表里去找对应的TochTarget,没找到返回null 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. //不为空说明view已经处理过这个事件,说明是多点触摸,就再加一个指针 newTouchTarget.pointerIdBits |= idBitsToAssign; break; } resetCancelNextUpFlag(child); //调用dispatchTransformedTouchEvent方法将事件分发给子View 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(); //dispatchTransformedTouchEvent返回true说明子View响应消费了这个事件 //因而调用addTouchTarget方法得到包含这个View的TouchTarget节点并将其添加到链表头 newTouchTarget = addTouchTarget(child, idBitsToAssign); //将已经分发的标记设置为true alreadyDispatchedToNewTouchTarget = true; break; } // The accessibility focus didn't handle the event, so clear // the flag and do a normal dispatch to all children. ev.setTargetAccessibilityFocus(false); } if (preorderedList != null) preorderedList.clear(); } //若是newTouchTarget为null且mFirstTouchTarget不为null,说明没找到子View来响应消费该事件,可是TouchTarget链表不为空 //则将newTouchTarget赋为TouchTarget链表中mFirstTouchTarget.next if (newTouchTarget == null && mFirstTouchTarget != null) { // Did not find a child to receive the event. // Assign the pointer to the least recently added target. newTouchTarget = mFirstTouchTarget; while (newTouchTarget.next != null) { newTouchTarget = newTouchTarget.next; } newTouchTarget.pointerIdBits |= idBitsToAssign; } } } 复制代码
接着看代码块3,在这段很长的代码里,首先一个if
中判断了该事件是否知足没有被拦截和被取消,以后第二个if
判断了事件类型是否为DOWN
,知足了没有被拦截和取消的DOWN
事件,接下来ViewGroup才会循环其子View找到点击事件在其内部而且可以接受该事件的子View,再经过调用dispatchTransformedTouchEvent
方法将事件分发给该子View处理,返回true说明子View成功消费事件,因而调用addTouchTarget
方法,方法中经过TouchTarget.obtain
方法得到一个包含这View的TouchTarget
节点并将其添加到链表头,并将已经分发的标记设置为true
。
接下来看代码块4:
// Dispatch to touch targets. //走到这里说明在循环遍历全部子View后没有找到接受该事件或者事件不是DOWN事件或者该事件已被拦截或取消 if (mFirstTouchTarget == null) { //mFirstTouchTarget为空说明没有子View响应消费该事件 //全部调用dispatchTransformedTouchEvent方法分发事件 //注意这里第三个参数传的是null,方法里会调用super.dispatchTouchEvent(event)即View.dispatchTouchEvent(event)方法 // No touch targets so treat this as an ordinary view. handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { // mFirstTouchTarget不为空说明有子View能响应消费该事件,消费过以前的DOWN事件,就将这个事件还分发给这个View // 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; //这里传入的是target.child就是以前响应消费的View,把该事件还交给它处理 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; } } 复制代码
以前在代码块3中处理分发了没被拦截和取消的DOWN
事件,那么其余MOVE
、UP
等类型事件怎么处理呢?还有若是遍历完子View却没有能接受这个事件的View又怎么处理呢?代码块4中就处理分发了这些事件。首先判断mFirstTouchTarget
是否为空,为空说明没有子View消费该事件,因而就调用dispatchTransformedTouchEvent
方法分发事件,这里注意dispatchTransformedTouchEvent
方法第三个参数View传的null
,方法里会对于这种没有子View能处理消费事件的状况,就调用该ViewGroup的super.dispatchTouchEvent
方法,即View的dispatchTouchEvent
,把ViewGroup当成View来处理,把事件交给ViewGroup处理。具体看dispatchTransformedTouchEvent
方法中的这段代码:
if (child == null) { handled = super.dispatchTouchEvent(event); } else { handled = child.dispatchTouchEvent(event); } 复制代码
dispatchTransformedTouchEvent
方法中child
即传入的View为空则调用super.dispatchTouchEvent
方法分发事件,就是View类的分发方法,不为空则调用子View方法,即child.dispatchTouchEvent
分发事件,因此归根结底都是调用了View类的dispatchTouchEvent
方法处理。
至此,ViewGroup中的分发过流程结束,再来总结一下这个过程:首先过滤掉不安全的事件,接着若是事件类型是DOWN
事件认为是一个新的事件序列开始,就清空TouchTarget
链表重置相关标志位(代码块一),而后判断是否拦截该事件,这里有两步判断:一是若是是DOWN
事件或者不是DOWN
事件可是mFirstTouchTarget
不等于null
(这里mFirstTouchTarget
若是等于null
说明以前没有View消费DOWN
事件,在代码块三末尾,能够看到若是有子View消费了DOWN
事件,会调用addTouchTarget
方法,得到一个保存了该子View的TouchTarget
,并将其添加到mFirstTouchTarget
链表头),则进入第二步禁止拦截标记的判断,不然直接设置为须要拦截,进入第二步判断设置过禁止拦截标记为true
的就不拦截,不然调用ViewGroup的onInterceptTouchEvent
方法根据返回接过来决定是否拦截(代码块二)。接下来若是事件没被拦截也没被取消并且仍是DOWN
事件,就循环遍历ViewGroup中的子View找到事件在其范围内而且能接受事件的子View,经过dispatchTransformedTouchEvent
方法将事件分发给该子View,而后经过addTouchTarget
方法将包含该子View的TouchTarget
插到链表头(代码块三)。最后若是没有找到可以接受该事件的子View又或者是MOVE
、UP
类型事件等再判断mFirstTouchTarget
是否为空,为空说明以前没有View能接受消费该事件,则调用dispatchTransformedTouchEvent
方法将事件交给自身处理,不为空则一样调用dispatchTransformedTouchEvent
方法,可是是将事件分发给该子View处理。
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; } 复制代码
在ViewGroup的dispatchTouchEvent
中没设置过禁止拦截的事件默认都会经过onInterceptTouchEvent
方法来决定是否拦截,onInterceptTouchEvent
方法里能够看到默认是返回false
,只有在事件源类型是鼠标而且是DOWN
事件是鼠标点击按钮和是滚动条的手势时才返回true
。因此默认通常ViewGroup的onInterceptTouchEvent
方法返回都为false
,也就是说默认不拦截事件。
ViewGroup中没有覆盖onTouchEvent
方法,因此调用ViewGroup的onTouchEvent
方法实际上调用的仍是它的父类View的onTouchEvent
方法。
在ViewGroup中将事件不管是分发给子View的时候仍是本身处理的,最终都会执行默认的View类的dispatchTouchEvent
方法:
public boolean dispatchTouchEvent(MotionEvent event) { ...... boolean result = false; ...... if (onFilterTouchEventForSecurity(event)) { ...... 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; } } ...... return result; } 复制代码
这里一样省略一些代码只看关键的,首先一样和ViewGroup同样,作了事件安全性的过滤,接着先判断了mOnTouchListener
是否为空,不为空而且该View是ENABLED
可用的,就会调用mOnTouchListener
的onTouch
方法,若是onTouch
方法返回true
说明事件已经被消费了,就将result
标记修改成true
,这样他就不会走接下来的if
了。若是没有设置mOnTouchListener
或者onTouch
方法返回false
,则会继续调用onTouchEvent
方法。这里能够发现mOnTouchListener
的onTouch
方法的优先级是在onTouchEvent
以前的,若是在代码中设置了mOnTouchListener
监听,而且onTouch
返回true
,则这个事件就被在onTouch
里消费了,不会在调用onTouchEvent
方法。
//这个mOnTouchListener就是常常在代码里设置的View.OnTouchListener mMyView.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { //这里返回true事件就消费了,不会再调用onTouchEvent方法 return true; } }); 复制代码
public boolean onTouchEvent(MotionEvent event) { /---------------代码块-1------------------------------------------------------------------- final float x = event.getX(); final float y = event.getY(); final int viewFlags = mViewFlags; final int action = event.getAction(); final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; if ((viewFlags & ENABLED_MASK) == DISABLED) { if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; // A disabled view that is clickable still consumes the touch // events, it just doesn't respond to them. return clickable; } /---------------代码块-1------完------------------------------------------------------------- /---------------代码块-2------------------------------------------------------------------- if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } /---------------代码块-2------完------------------------------------------------------------- /---------------代码块-3------------------------------------------------------------------- if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { switch (action) { case MotionEvent.ACTION_UP: mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; if ((viewFlags & TOOLTIP) == TOOLTIP) { handleTooltipUp(); } if (!clickable) { removeTapCallback(); removeLongPressCallback(); mInContextButtonPress = false; mHasPerformedLongPress = false; mIgnoreNextUpEvent = false; break; } 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)) { //调用了OnClickListener 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: if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) { mPrivateFlags3 |= PFLAG3_FINGER_DOWN; } mHasPerformedLongPress = false; if (!clickable) { checkForLongClick(0, x, y); break; } 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, x, y); } break; case MotionEvent.ACTION_CANCEL: if (clickable) { setPressed(false); } removeTapCallback(); removeLongPressCallback(); mInContextButtonPress = false; mHasPerformedLongPress = false; mIgnoreNextUpEvent = false; mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; break; case MotionEvent.ACTION_MOVE: if (clickable) { drawableHotspotChanged(x, y); } // Be lenient about moving outside of buttons if (!pointInView(x, y, mTouchSlop)) { // Outside button // Remove any future long press/tap checks removeTapCallback(); removeLongPressCallback(); if ((mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; } break; } return true; } /---------------代码块-3------完------------------------------------------------------------- return false; } 复制代码
onTouchEvent
方法里的代码也很多,不过大部分都是响应事件的一些逻辑,与事件分发流程关系不大。仍是分红三块,先看第一个代码块:
final float x = event.getX(); final float y = event.getY(); final int viewFlags = mViewFlags; final int action = event.getAction(); //这里CLICKABLE、CONTEXT_CLICKABLE和CONTEXT_CLICKABLE有一个知足,clickable就为true final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; //这里先判断当前View是否可用,若是是不可用进入if代码块 if ((viewFlags & ENABLED_MASK) == DISABLED) { //若是是UP事件而且View处于PRESSED状态,则调用setPressed设置为false if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; // A disabled view that is clickable still consumes the touch // events, it just doesn't respond to them. //这里若是View是不可用状态,就直接返回clickable状态,不作任何处理 return clickable; } 复制代码
代码块1中首先得到View是否可点击clickable
,而后判断View若是是不可用状态就直接返回clickable
,可是没作任何响应。View默认的clickable
为false
,Enabled
为ture
,不一样的View的clickable
默认值也不一样,Button
默认clickable
为true
,TextView
默认为false
。
if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } 复制代码
代码块2中会对一个mTouchDelegate
触摸代理进行判断,不为空会调用代理的onTouchEvent
响应事件而且返回true
。
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { switch (action) { case MotionEvent.ACTION_UP: mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; if ((viewFlags & TOOLTIP) == TOOLTIP) { handleTooltipUp(); } if (!clickable) { removeTapCallback(); removeLongPressCallback(); mInContextButtonPress = false; mHasPerformedLongPress = false; mIgnoreNextUpEvent = false; break; } 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)) { //调用了OnClickListener 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: if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) { mPrivateFlags3 |= PFLAG3_FINGER_DOWN; } mHasPerformedLongPress = false; if (!clickable) { checkForLongClick(0, x, y); break; } 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, x, y); } break; case MotionEvent.ACTION_CANCEL: if (clickable) { setPressed(false); } removeTapCallback(); removeLongPressCallback(); mInContextButtonPress = false; mHasPerformedLongPress = false; mIgnoreNextUpEvent = false; mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; break; case MotionEvent.ACTION_MOVE: if (clickable) { drawableHotspotChanged(x, y); } // Be lenient about moving outside of buttons if (!pointInView(x, y, mTouchSlop)) { // Outside button // Remove any future long press/tap checks removeTapCallback(); removeLongPressCallback(); if ((mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; } break; } return true; } 复制代码
代码块3中首先判断了 clickable || (viewFlags & TOOLTIP) == TOOLTIP
知足了这个条件就返回true
消费事件。接下来的switch
中主要对事件四种状态分别作了处理。这里稍微看下在UP
事件中会调用一个performClick
方法,方法中调用了OnClickListener
的onClick
方法。
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); notifyEnterOrExitForAutoFillIfNeeded(true); return result; } 复制代码
最后看到onTouchEvent
的最后一行默认返回的仍是false
,就是说只有知足上述的条件之一才会返回ture
。
至此事件分发的相关源码就梳理完了,我画了几张流程图,能更清晰的理解源码逻辑。
ViewGroup的dispatchTouchEvent逻辑:
阅读了源码以后,先来解决以前提到的三个问题。
ACTION_DOWN
事件有完整的从Activity到ViewGroup再到View的分发拦截和响应的运行日志,为何ACTION_MOVE
和ACTION_UP
事件没有?A1:日志Demo代码全部事件传递方法都是默认调用super
父类对应方法,因此根据源码逻辑可知当事件序列中的第一个DOWN
事件来临时,会按照Activity-->MyViewGroupA-->MyViewGroupB-->MyView
的顺序分发,ViewGroup中onInterceptTouchEvent
方法默认返回false
不会拦截事件,最终会找到合适的子View(这里即MyView)dispatchTransformedTouchEvent
方法,将事件交给子View的dispatchTouchEvent
处理,在dispatchTouchEvent
方法中默认会调用View的onTouchEvent
方法处理事件,这里由于MyView是继承View的,因此默认clickable
为false
,而onTouchEvent
方法中当clickable
为false
时默认返回的也是false
。最终致使ViewGroup中dispatchTransformedTouchEvent
方法返回为false
。进而致使mFirstTouchTarget
为空,因此后续MOVE
、UP
事件到来时,由于mFirstTouchTarget
为空,事件拦截标记直接设置为true
事件被拦截,就不会继续向下分发,最终事件无人消费就返回到Activity的onTouchEvent
方法。因此就会出现这样的日志输出。
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 { //mFirstTouchTarget为空intercepted为true且不会调用onInterceptTouchEvent方法 intercepted = true; } 复制代码
clickable="true"
以后ACTION_MOVE
和ACTION_UP
事件就会执行了?A2:如A1中所说,clickable
设置为true
,View的onTouchEvent
方法的返回就会为true
,消费了DOWN
事件,就会建立一个TouchTarget
插到单链表头,mFirstTouchTarget
就不会是空了,MOVE
、UP
事件到来时,就会由以前消费了DOWN
事件的View来处理消费MOVE
、UP
事件。
requestDisallowInterceptTouchEvent
方法是怎样通知父View不拦截事件,为何连onInterceptTouchEvent
方法也不执行了?A3:源码阅读是有看到,requestDisallowInterceptTouchEvent
方法时经过位运算设置标志位,在调用传入参数为true
后,事件在分发时disallowIntercept
会为true
,!disallowIntercept
即为false
,致使事件拦截标记intercepted
为false
,不会进行事件拦截。
View.OnClickListener
的onClick
方法与View.OnTouchListener
的onTouch
执行顺序?A4::View.OnClickListener
的onClick
方法是在View的onTouchEvent
中performClick
方法中调用的。 而View.OnTouchListener
的onTouch
方法在View的dispatchTouchEvent
方法中看到是比onTouchEvent
方法优先级高的,而且只要OnTouchListener.Touch
返回为true
,就只会调用OnTouchListener.onTouch
方法不会再调用onTouchEvent
方法。因此View.OnClickListener
的onClick
方法顺序是在View.OnTouchListener
的onTouch
以后的。
关于滑动冲突,在《Android开发艺术探索》中有详细说明,我这里把书上的方法结论与具体实例结合起来作一个总结。
不一样的场景有不一样的处理规则,例如上面的场景一,规则通常就是当左右滑动时,外部View拦截事件,当上下滑动时要让内部View拦截事件,这时候处理滑动冲突就能够根据滑动是水平滑动仍是垂直滑动来判断谁来拦截事件。场景而这种同个方向上的滑动冲突通常要根据业务逻辑来处理规则,何时要外部View拦截,何时要内部View拦截。场景三就更加复杂了,可是一样是根据具体业务逻辑,来判断具体的滑动规则。
外部拦截法是从父View着手,全部事件都要通过父View的分发和拦截,何时父View须要事件,就将其拦截,不须要就不拦截,经过重写父View的onInterceptTouchEvent
方法来实现拦截规则。
private int mLastXIntercept; private int mLastYIntercept; public boolean onInterceptTouchEvent(MotionEvent event) { boolean intercepted = false; int x = (int)event.getX(); int y = (int)event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: { intercepted = false; break; } case MotionEvent.ACTION_MOVE: { if (知足父容器的拦截要求) { intercepted = true; } else { intercepted = false; } break; } case MotionEvent.ACTION_UP: { intercepted = false; break; } default: break; } mLastXIntercept = x; mLastYIntercept = y; return intercepted; } 复制代码
按照以上伪代码,根据不一样的拦截要求进行修改就能够解决滑动冲突。
内部拦截法的思想是父View不拦截事件,由子View来决定事件拦截,若是子View须要此事件就直接消耗掉,若是不须要就交给父View处理。这种方法须要配合requestDisallowInterceptTouchEvent
方法来实现。
private int mLastX; private int mLastY; @Override public boolean dispatchTouchEvent(MotionEvent event) { int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: { parent.requestDisallowInterceptTouchEvent(true); break; } case MotionEvent.ACTION_MOVE: { int deltaX = x - mLastX; int deltaY = y - mLastY; if (父容器须要此类点击事件) { parent.requestDisallowInterceptTouchEvent(false); } break; } case MotionEvent.ACTION_UP: { break; } default: break; } mLastX = x; mLastY = y; return super.dispatchTouchEvent(event); } //父View的onInterceptTouchEvent方法 @Override public boolean onInterceptTouchEvent(MotionEvent event) { int action = event.getAction(); if (action == MotionEvent.ACTION_DOWN) { return false; } else { return true; } } 复制代码
这里父View不拦截ACTION_DOWN
方法的缘由,根据以前的源码阅读可知若是ACTION_DOWN
事件被拦截,以后的全部事件就都不会再传递下去了。
实例一:ScrollView与ListView嵌套
这个实例是同向滑动冲突,先看布局文件:
<?xml version="1.0" encoding="utf-8"?> <cScrollView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/scrollView1" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".ScrollDemo1Activity"> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical"> <com.example.sy.eventdemo.MyView android:layout_width="match_parent" android:layout_height="350dp" android:background="#27A3F3" android:clickable="true" /> <ListView android:id="@+id/lv" android:layout_width="match_parent" android:background="#E5F327" android:layout_height="300dp"></ListView> <com.example.sy.eventdemo.MyView android:layout_width="match_parent" android:layout_height="500dp" android:background="#0AEC2E" android:clickable="true" /> </LinearLayout> </cScrollView> 复制代码
这里MyView就是以前打印日志的View没有作任何其余处理,用于占位使ScrollView超出一屏能够滑动。
运行效果:
首先自定义ScrollView,重写他的onInterceptTouchEvent
方法,拦击除了DOWN
事件之外的事件。
public class MyScrollView extends ScrollView { public MyScrollView(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onTouchEvent(ev); return false; } return true; } } 复制代码
这里没有拦截DOWN
事件,因此DOWN
事件没法进入ScrollView的onTouchEvent
事件,又由于ScrollView的滚动须要在onTouchEvent
方法中作一些准备,因此这里手动调用一次。接着再自定义一个ListView,来决定事件拦截,重写dispatchTouchEvent
方法。
package com.example.sy.eventdemo; import android.content.Context; import android.os.Build; import android.support.annotation.RequiresApi; import android.util.AttributeSet; import android.view.MotionEvent; import android.widget.ListView; /** * Create by SY on 2019/4/22 */ public class MyListView extends ListView { public MyListView(Context context) { super(context); } public MyListView(Context context, AttributeSet attrs) { super(context, attrs); } public MyListView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } private float lastY; @RequiresApi(api = Build.VERSION_CODES.KITKAT) @Override public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { getParent().getParent().requestDisallowInterceptTouchEvent(true); } else if (ev.getAction() == MotionEvent.ACTION_MOVE) { if (lastY > ev.getY()) { // 这里判断是向上滑动,并且不能再向上滑了,说明到头了,就让父View处理 if (!canScrollList(1)) { getParent().getParent().requestDisallowInterceptTouchEvent(false); return false; } } else if (ev.getY() > lastY) { // 这里判断是向下滑动,并且不能再向下滑了,说明到头了,一样让父View处理 if (!canScrollList(-1)) { getParent().getParent().requestDisallowInterceptTouchEvent(false); return false; } } } lastY = ev.getY(); return super.dispatchTouchEvent(ev); } } 复制代码
判断是向上滑动仍是向下滑动,是否滑动到头了,若是滑到头了就让父View拦截事件由父View处理,不然就由本身处理。将布局文件中的空间更换。
<?xml version="1.0" encoding="utf-8"?> <com.example.sy.eventdemo.MyScrollView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/scrollView1" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".ScrollDemo1Activity"> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical"> <com.example.sy.eventdemo.MyView android:layout_width="match_parent" android:layout_height="350dp" android:background="#27A3F3" android:clickable="true" /> <com.example.sy.eventdemo.MyListView android:id="@+id/lv" android:layout_width="match_parent" android:background="#E5F327" android:layout_height="300dp"></com.example.sy.eventdemo.MyListView> <com.example.sy.eventdemo.MyView android:layout_width="match_parent" android:layout_height="500dp" android:background="#0AEC2E" android:clickable="true" /> </LinearLayout> </com.example.sy.eventdemo.MyScrollView> 复制代码
运行结果:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".ScrollDemo2Activity"> <com.example.sy.eventdemo.MyViewPager android:id="@+id/viewPager" android:layout_width="match_parent" android:layout_height="match_parent"></com.example.sy.eventdemo.MyViewPager> </LinearLayout> 复制代码
ViewPager的每一个页面的布局也很简单就是一个ListView:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".ScrollDemo2Activity"> <com.example.sy.eventdemo.MyViewPager android:id="@+id/viewPager" android:layout_width="match_parent" android:layout_height="match_parent"></com.example.sy.eventdemo.MyViewPager> </LinearLayout> 复制代码
开始没有处理滑动冲突的运行效果是这样的:
case MotionEvent.ACTION_MOVE: int gapX = x - lastX; int gapY = y - lastY; //当水平滑动距离大于垂直滑动距离,让父view拦截事件 if (Math.abs(gapX) > Math.abs(gapY)) { intercept = true; } else { //不然不拦截事件 intercept = false; } break; 复制代码
onInterceptTouchEvent
中当水平滑动距离大于垂直滑动距离,让父view拦截事件,反之父View不拦截事件,让子View处理。
运行结果: