Android进阶必备:滑动冲突解决与事件分发机制(附视频讲解)这篇看完还不懂请寄刀片
1、前言
Android学习一段时间,需求做多了必然会遇到滑动冲突问题,比如在一个ScrollView中要嵌套一个地图View,这时候触摸移动地图或者放大缩小地图就会变得不太准确甚至没有反应,这就是遇到了滑动冲突,ScrollView中上下滑动与地图的触摸手势发生冲突。想要解决滑动冲突就不得不提到Android的事件分发机制,只有吃透了事件分发,才能对滑动冲突的解决得心应手。
作者:胖宅老鼠
链接:https://juejin.im/post/6844903829482242056
B站视频讲解:https://www.bilibili.com/video/BV1oK4y1a7ep
2、事件分发机制相关方法
Android事件分发机制主要相关方法有以下三个:
- 事件分发:public boolean dispatchTouchEvent(MotionEvent ev)
- 事件拦截:public boolean onInterceptTouchEvent(MotionEvent ev)
- 事件响应:public boolean onTouchEvent(MotionEvent ev)
以下是这三个方法在Activity、ViewGroup和View中的存在情况:
相关方法 | Activity | ViewGroup | View |
---|---|---|---|
dispatchTouchEvent | yes | yes | yes |
onInterceptTouchEvent | no | yes | no |
onTouchEvent | yes | yes | yes |
这三个方法都返回一个布尔类型,根据返回的不同对事件进行不同的分发拦截和响应。一般有三种返回true
、false
和super
引用父类对应方法。
dispatchTouchEvent 返回true:表示改事件在本层不再进行分发且已经在事件分发自身中被消费了。
dispatchTouchEvent 返回 false:表示事件在本层不再继续进行分发,并交由上层控件的onTouchEvent
方法进行消费。
onInterceptTouchEvent 返回true:表示将事件进行拦截,并将拦截到的事件交由本层控件 的onTouchEvent
进行处理。
onInterceptTouchEvent 返回false:表示不对事件进行拦截,事件得以成功分发到子View
。并由子View
的dispatchTouchEvent
进行处理。
onTouchEvent 返回 true:表示onTouchEvent
处理完事件后消费了此次事件。此时事件终结,将不会进行后续的传递。
onTouchEvent 返回 false:事件在onTouchEvent
中处理后继续向上层View传递,且有上层View
的onTouchEvent
进行处理。
除此之外还有一个方法也是经常用到的:
- public void requestDisallowInterceptTouchEvent(boolean disallowIntercept)
它的作用是子View用来通知父View不要拦截事件。下面先写一个简单的Demo
来看一下事件分发和传递:
简单的日志的Demo:
这里的代码只是自定义了两个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);}@Overridepublic 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);}@Overridepublic 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);}@Overridepublic 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.MyViewGroupBandroid:id="@+id/viewGroupB"android:layout_width="300dp"android:layout_height="300dp"android:layout_centerInParent="true"android:background="@android:color/white"><com.example.sy.eventdemo.MyViewandroid: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布局层级关系:
除去外层Activity和Window的层级,从MyViewGroup开始是自己定义的打印日志View。接下来运行Demo查看日志:
D/MainActivity: dispatchTouchEvent:ACTION_DOWND/MyViewGroupA: dispatchTouchEvent:ACTION_DOWND/MyViewGroupA: onInterceptTouchEvent:ACTION_DOWND/MyViewGroupB: dispatchTouchEvent:ACTION_DOWND/MyViewGroupB: onInterceptTouchEvent:ACTION_DOWND/MyView: dispatchTouchEvent:ACTION_DOWND/MyView: onTouchEvent:ACTION_DOWND/MyViewGroupB: onTouchEvent:ACTION_DOWND/MyViewGroupA: onTouchEvent:ACTION_DOWND/MainActivity: onTouchEvent:ACTION_DOWND/MainActivity: dispatchTouchEvent:ACTION_MOVED/MainActivity: onTouchEvent:ACTION_MOVED/MainActivity: dispatchTouchEvent:ACTION_UPD/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_DOWND/MyViewGroupA: dispatchTouchEvent:ACTION_DOWND/MyViewGroupA: onInterceptTouchEvent:ACTION_DOWND/MyViewGroupB: dispatchTouchEvent:ACTION_DOWND/MyViewGroupB: onInterceptTouchEvent:ACTION_DOWND/MyView: dispatchTouchEvent:ACTION_DOWND/MyView: onTouchEvent:ACTION_DOWN/-------------------ACTION_MOVE事件------------------D/MainActivity: dispatchTouchEvent:ACTION_MOVED/MyViewGroupA: dispatchTouchEvent:ACTION_MOVED/MyViewGroupA: onInterceptTouchEvent:ACTION_MOVED/MyViewGroupB: dispatchTouchEvent:ACTION_MOVED/MyViewGroupB: onInterceptTouchEvent:ACTION_MOVED/MyView: dispatchTouchEvent:ACTION_MOVED/MyView: onTouchEvent:ACTION_MOVE/-------------------ACTION_UP事件------------------D/MainActivity: dispatchTouchEvent:ACTION_UPD/MyViewGroupA: dispatchTouchEvent:ACTION_UPD/MyViewGroupA: onInterceptTouchEvent:ACTION_UPD/MyViewGroupB: dispatchTouchEvent:ACTION_UPD/MyViewGroupB: onInterceptTouchEvent:ACTION_UPD/MyView: dispatchTouchEvent:ACTION_UPD/MyView: onTouchEvent:ACTION_UP
这下ACTION_MOVE
和ACTION_UP
事件也有日志了。接下来在MyViewGroupB的onInterceptTouchEvent
的方法中修改代码如下:
@Overridepublic 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_DOWND/MyViewGroupA: dispatchTouchEvent:ACTION_DOWND/MyViewGroupA: onInterceptTouchEvent:ACTION_DOWND/MyViewGroupB: dispatchTouchEvent:ACTION_DOWND/MyViewGroupB: onInterceptTouchEvent:ACTION_DOWND/MyView: dispatchTouchEvent:ACTION_DOWND/MyView: onTouchEvent:ACTION_DOWN/------------------ACTION_MOVE事件-----------------------------D/MainActivity: dispatchTouchEvent:ACTION_MOVED/MyViewGroupA: dispatchTouchEvent:ACTION_MOVED/MyViewGroupA: onInterceptTouchEvent:ACTION_MOVED/MyViewGroupB: dispatchTouchEvent:ACTION_MOVED/MyViewGroupB: onInterceptTouchEvent:ACTION_MOVE/------------------ACTION_UP事件-------------------------------D/MainActivity: dispatchTouchEvent:ACTION_UPD/MyViewGroupA: dispatchTouchEvent:ACTION_UPD/MyViewGroupA: onInterceptTouchEvent:ACTION_UPD/MyViewGroupB: dispatchTouchEvent:ACTION_UPD/MyViewGroupB: onTouchEvent:ACTION_UPD/MainActivity: onTouchEvent:ACTION_UP
根据日志可知ACTION_MOVE
和ACTION_UP
事件传递到MyViewGroupB就没有再向MyView传递了。接着在MyView的onTouchEvent方法中调用requestDisallowInterceptTouchEvent
方法通知父容器不要拦截事件。
@Overridepublic 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_DOWND/MyViewGroupA: dispatchTouchEvent:ACTION_DOWND/MyViewGroupA: onInterceptTouchEvent:ACTION_DOWND/MyViewGroupB: dispatchTouchEvent:ACTION_DOWND/MyViewGroupB: onInterceptTouchEvent:ACTION_DOWND/MyView: dispatchTouchEvent:ACTION_DOWND/MyView: onTouchEvent:ACTION_DOWN/------------------ACTION_MOVE事件-----------------------------D/MainActivity: dispatchTouchEvent:ACTION_MOVED/MyViewGroupA: dispatchTouchEvent:ACTION_MOVED/MyViewGroupB: dispatchTouchEvent:ACTION_MOVED/MyView: dispatchTouchEvent:ACTION_MOVED/MyView: onTouchEvent:ACTION_MOVE/------------------ACTION_UP事件-------------------------------D/MainActivity: dispatchTouchEvent:ACTION_UPD/MyViewGroupA: dispatchTouchEvent:ACTION_UPD/MyViewGroupB: dispatchTouchEvent:ACTION_UPD/MyView: dispatchTouchEvent:ACTION_UPD/MyView: onTouchEvent:ACTION_UP
这时可以发现ACTION_MOVE
和ACTION_UP
事件又传递到了MyView中并且两个ViewGroup中都没有执行onInterceptTouchEvent
方法。 明显是requestDisallowInterceptTouchEvent
方法起了作用。但是又出现了两个新问题。
- 问题二:为什么将设置
clickable="true"
之后ACTION_MOVE
和ACTION_UP
事件就会执行了? - 问题三:
requestDisallowInterceptTouchEvent
方法是怎样通知父View不拦截事件,为什么连onInterceptTouchEvent
方法也不执行了?
想弄明白这些问题就只能到源码中寻找答案了。
3、事件分发机制源码
在正式看源码之前先讲一个概念:事件序列
我们常说的事件,一般是指从手指触摸到屏幕在到离开屏幕这么一个过程。在这个过程中其实会产生多个事件,一般是以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.//接受事件的Viewpublic 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方法:
接下来正式按照分发流程来阅读源码,从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方法:
先来看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
。
@Overridepublic 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
方法。
ViewGroup的dispatchTouchEvent方法:
通过以上这一系列的调用,事件终于从Activity到PhoneWindow再到DecorView最终走到了ViewGroup的dispatchTouchEvent
方法中,接下来进入ViewGroup查看它的dispatchTouchEvent
方法。
@Overridepublic 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 downfinal 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 indexfor (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不为空进入ifif (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {//继续判断是否在调用了requestDisallowInterceptTouchEvent(true)设置了禁止拦截标记final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;//设置禁止拦截设标记disallowIntercept为true,!disallowIntercept即为falseif (!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)
方法来看一下:
@Overridepublic void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {// We're already in this state, assume our ancestors are tooreturn;}if (disallowIntercept) {mGroupFlags |= FLAG_DISALLOW_INTERCEPT;} else {mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;}// Pass it up to our parentif (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 downfinal 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,没找到返回nullnewTouchTarget = 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方法将事件分发给子Viewif (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 indexfor (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);//将已经分发的标记设置为truealreadyDispatchedToNewTouchTarget = 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.nextif (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处理。
ViewGroup的onInterceptTouchEvent方法:
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
方法,所以调用ViewGroup的onTouchEvent
方法实际上调用的还是它的父类View的onTouchEvent
方法。
View的dispatchTouchEvent方法:
在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() {@Overridepublic boolean onTouch(View v, MotionEvent event) {//这里返回true事件就消费了,不会再调用onTouchEvent方法return true;}});
View的onTouchEvent方法:
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 checkremoveLongPressCallback();// Only perform take click actions if we were in the pressed stateif (!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)) {//调用了OnClickListenerperformClick();}}}if (mUnsetPressedState == null) {mUnsetPressedState = new UnsetPressedState();}if (prepressed) {postDelayed(mUnsetPressedState,ViewConfiguration.getPressedStateDuration());} else if (!post(mUnsetPressedState)) {// If the post failed, unpress right nowmUnsetPressedState.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 awaysetPressed(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 buttonsif (!pointInView(x, y, mTouchSlop)) {// Outside button// Remove any future long press/tap checksremoveTapCallback();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就为truefinal 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设置为falseif (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 checkremoveLongPressCallback();// Only perform take click actions if we were in the pressed stateif (!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)) {//调用了OnClickListenerperformClick();}}}if (mUnsetPressedState == null) {mUnsetPressedState = new UnsetPressedState();}if (prepressed) {postDelayed(mUnsetPressedState,ViewConfiguration.getPressedStateDuration());} else if (!post(mUnsetPressedState)) {// If the post failed, unpress right nowmUnsetPressedState.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 awaysetPressed(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 buttonsif (!pointInView(x, y, mTouchSlop)) {// Outside button// Remove any future long press/tap checksremoveTapCallback();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逻辑:
View的dispathTouchEvent逻辑:
事件分发整体逻辑
4、事件分发机制相关问题
阅读了源码之后,先来解决之前提到的三个问题。
Q1:为什么日志Demo中只有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;}
Q2:为什么将设置clickable="true"
之后ACTION_MOVE
和ACTION_UP
事件就会执行了?
A2:如A1中所说,clickable
设置为true
,View的onTouchEvent
方法的返回就会为true
,消费了DOWN
事件,就会创建一个TouchTarget
插到单链表头,mFirstTouchTarget
就不会是空了,MOVE
、UP
事件到来时,就会由之前消费了DOWN
事件的View来处理消费MOVE
、UP
事件。
Q3:requestDisallowInterceptTouchEvent
方法是怎样通知父View不拦截事件,为什么连onInterceptTouchEvent
方法也不执行了?
A3:源码阅读是有看到,requestDisallowInterceptTouchEvent
方法时通过位运算设置标志位,在调用传入参数为true
后,事件在分发时disallowIntercept
会为true
,!disallowIntercept
即为false
,导致事件拦截标记intercepted
为false
,不会进行事件拦截。
Q4: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
之后的。
5、滑动冲突
关于滑动冲突,在《Android开发艺术探索》中有详细说明,我这里把书上的方法结论与具体实例结合起来做一个总结。
1.滑动冲突的场景
常见的场景有三种:
- 外部滑动与内部滑动方向不一致
- 外部滑动与内部滑动方向一致
- 前两种情况的嵌套
2.滑动冲突的处理规则
不同的场景有不同的处理规则,例如上面的场景一,规则一般就是当左右滑动时,外部View拦截事件,当上下滑动时要让内部View拦截事件,这时候处理滑动冲突就可以根据滑动是水平滑动还是垂直滑动来判断谁来拦截事件。场景而这种同个方向上的滑动冲突一般要根据业务逻辑来处理规则,什么时候要外部View拦截,什么时候要内部View拦截。场景三就更加复杂了,但是同样是根据具体业务逻辑,来判断具体的滑动规则。
3.滑动冲突的解决方法
- 外部拦截法
- 内部拦截法
外部拦截法是从父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;
@Overridepublic 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方法@Overridepublic boolean onInterceptTouchEvent(MotionEvent event) {int action = event.getAction();if (action == MotionEvent.ACTION_DOWN) {return false;} else {return true;}}
这里父View不拦截ACTION_DOWN
方法的原因,根据之前的源码阅读可知如果ACTION_DOWN
事件被拦截,之后的所有事件就都不会再传递下去了。
4.滑动冲突实例
实例一: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"><LinearLayoutandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:orientation="vertical"><com.example.sy.eventdemo.MyViewandroid:layout_width="match_parent"android:layout_height="350dp"android:background="#27A3F3"android:clickable="true" /><ListViewandroid:id="@+id/lv"android:layout_width="match_parent"android:background="#E5F327"android:layout_height="300dp"></ListView><com.example.sy.eventdemo.MyViewandroid:layout_width="match_parent"android:layout_height="500dp"android:background="#0AEC2E"android:clickable="true" /></LinearLayout>
</cScrollView>
这里MyView就是之前打印日志的View没有做任何其他处理,用于占位使ScrollView超出一屏可以滑动。
运行效果:
可以看到ScrollView与ListView发生滑动冲突,ListView的滑动事件没有触发。接着来解决这个问题,用内部拦截法。
首先自定义ScrollView,重写他的onInterceptTouchEvent
方法,拦击除了DOWN
事件以外的事件。
public class MyScrollView extends ScrollView {public MyScrollView(Context context, AttributeSet attrs) {super(context, attrs);}@Overridepublic 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)@Overridepublic 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"><LinearLayoutandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:orientation="vertical"><com.example.sy.eventdemo.MyViewandroid:layout_width="match_parent"android:layout_height="350dp"android:background="#27A3F3"android:clickable="true" /><com.example.sy.eventdemo.MyListViewandroid:id="@+id/lv"android:layout_width="match_parent"android:background="#E5F327"android:layout_height="300dp"></com.example.sy.eventdemo.MyListView><com.example.sy.eventdemo.MyViewandroid:layout_width="match_parent"android:layout_height="500dp"android:background="#0AEC2E"android:clickable="true" /></LinearLayout>
</com.example.sy.eventdemo.MyScrollView>
运行结果:
实例二:ViewPager与ListView嵌套
这个例子是水平和垂直滑动冲突。使用V4包中的ViewPager与ListView嵌套并不会发生冲突,是因为在ViewPager中已经实现了关于滑动冲突的处理代码,所以这里自定义一个简单的ViewPager来测试冲突。布局文件里就一个ViewPager:
<?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.MyViewPagerandroid: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.MyViewPagerandroid:id="@+id/viewPager"android:layout_width="match_parent"android:layout_height="match_parent"></com.example.sy.eventdemo.MyViewPager></LinearLayout>
开始没有处理滑动冲突的运行效果是这样的:
看到现在只能上下滑动响应ListView的滑动事件,接着我们外部拦截发解决滑动冲突,核心代码如下:
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处理。
运行结果:
这下冲突就解决了。这两个例子分别对应了上面的场景一和场景二,关于场景三的解决方法其实也是一样,都是根据具体需求判断事件需要由谁来响应消费,然后重写对应方法将事件拦截或者取消拦截即可,这里就不再具体举例了。
6、总结
- Android事件分发顺序:Activity–>ViewGroup–>View
- Android事件响应顺序:View–>ViewGroup–>Activity
- 滑动冲突解决,关键在于找到拦截规则,根据操作习惯或者业务逻辑确定拦截规则,根据规则重新对应拦截方法即可。
欢迎关注我的CSDN,分享Android干货,交流Android技术。
对文章有何见解,或者有何技术问题,都可以在评论区一起留言讨论,我会虔诚为你解答。
也欢迎大家来我的B站找我玩,有各类Android架构师进阶技术难点的视频讲解
B站直通车:https://space.bilibili.com/544650554
Android进阶必备:滑动冲突解决与事件分发机制(附视频讲解)这篇看完还不懂请寄刀片相关推荐
- Android应用测速组件实现原理,这篇看完还不懂跟我去摆地摊,全网首发
apply plugin: 'rabbit-tracer-transform' 为了支持网络监控功能,需要在OkHttpClient初始化时插入拦截器(目前只支持OkHttp的网络监控): OkHtt ...
- Android View系列(二):事件分发机制源码解析
概述 在介绍点击事件规则之前,我们需要知道我们分析的是MotionEvent,即点击事件,所谓的事件分发就是对MotionEvent事件的分发过程,即当一个MotionEvent生成以后,系统需要把这 ...
- View事件分发机制(源码分析篇)
01.Android中事件分发顺序 1.1 事件分发的对象是谁 事件分发的对象是事件.注意,事件分发是向下传递的,也就是父到子的顺序. 当用户触摸屏幕时(View或ViewGroup派生的控件),将产 ...
- Android程序员必备的六大顶级开发工具,快加入你的清单,看完没有不懂的
如果你认为B4A仅适用于对Android应用开发不太了解的新手,请再想一想.你可以用B4A做几乎所有用Java做的事情,加分项是它比Java快. ###LEAKCANARY LeakCanary帮助指 ...
- Android View的事件分发机制和滑动冲突解决方案
这篇文章会先讲Android中View的事件分发机制,然后再介绍Android滑动冲突的形成原因并给出解决方案.因水平有限,讲的不会太过深入,只希望各位看了之后对事件分发机制的流程有个大概的概念,并且 ...
- 事件分发机制 事件拦截 滑动冲突 MD
目录 事件分发机制分析案例 默认行为 试验 0 结论 dispatchTouchEvent 返回 true 试验 1 试验 2 结论 onInterceptTouchEvent 返回 true 试验 ...
- 【朝花夕拾】Android自定义View篇之(六)Android事件分发机制(中)从源码分析事件分发机制...
前言 转载请注明,转自[https://www.cnblogs.com/andy-songwei/p/11039252.html]谢谢! 在上一篇文章[[朝花夕拾]Android自定义View篇之(五 ...
- 【朝花夕拾】Android自定义View篇之(六)Android事件分发机制(中)从源码分析事件分发逻辑及经常遇到的一些“诡异”现象
前言 转载请注明,转自[https://www.cnblogs.com/andy-songwei/p/11039252.html]谢谢! 在上一篇文章[[朝花夕拾]Android自定义View篇之(五 ...
- Android事件分发机制浅析
文章目录 前言 一.MotionEvent 二.事件分发 1.代码模型 2.代码分析 0x00.Activity 0x01.ViewGroup 1.是否分发事件 2.事件分发前清除标记 关于Touch ...
最新文章
- centos远程开机的操作
- 即时通信是机遇也是挑战
- mysql带有OR关键字的多条件查询
- 支付宝当面付扫码支付支付后不回调_科普:支付宝刷脸支付流程介绍
- WideCharToMultiByte和MultiByteToWideChar函数的用法(转)
- 计算机网络的保护策略,计算机网络安全问题保护策略论文原稿
- oracle+long列,oracle中对LONG列进行查询
- 自定义选中文字背景色
- python安装目录结构_1.5 python安装目录介绍《Python基础开发入门到精通》
- 用WPS2000制作勾股定理教学课件(转)
- linux 设置注释颜色,【转】vim注释和光标高亮行列的颜色设置
- JDK成年了,JDK18版本发布,走进JDK18新特性
- STC15单片机实战项目 - 原理图设计
- Vbox安装增强功能
- 腾讯云对象存储操作流程
- [转]雷雨天防雷要点
- bios禁用intel VT-x
- 今日头条——校招在线编程题《头条校招》
- 软件系统——需求调研
- Windows批处理bat常用命令教程
热门文章
- 2021ICPC网络赛第二场The 2021 ICPC Asia Regionals Online Contest (II) 【L Euler Function】
- centos7 安装anaconda3,notebook,解决matplotlib 中文乱码
- Ubuntu入门,Ubuntu基本软件,Ubuntu起始配置
- 有没有支持P2P架构的直播技术?
- 雷达数据采集DCA1000EVM使用指南
- UMAX祝您获取更多用户
- TFET and hybrid TFET-FinFET文章解读
- 架构就是bull shit
- android获取多媒体库的视频、音频、图片
- Android修改主机名和IP地址问题