Android进阶知识:事件分发与滑动冲突(一)
1、前言
Android学习一段时间,需求做多了必然会遇到滑动冲突问题,比如在一个ScrollView中要嵌套一个地图View,这时候触摸移动地图或者放大缩小地图就会变得不太准确甚至没有反应,这就是遇到了滑动冲突,ScrollView中上下滑动与地图的触摸手势发生冲突。想要解决滑动冲突就不得不提到Android的事件分发机制,只有吃透了事件分发,才能对滑动冲突的解决得心应手。
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;}}}
Android进阶知识:事件分发与滑动冲突(一)相关推荐
- Android进阶知识:绘制流程(上)
1.前言 之前写过一篇Android进阶知识:事件分发与滑动冲突,主要研究的是关于Android中View事件分发与响应的流程.关于View除了事件传递流程还有一个很重要的就是View的绘制流程.一个 ...
- android touch事件无反应,android的touch事件分发响应机制
想要弄明白android的touch事件分发响应机制需要先充分理解一下几个知识点: View和ViewGroup touch事件的构成 ViewGroup如何对事件分发和拦截 View和ViewGro ...
- 总结系列——聊聊android里面的事件分发
前言 android 中的事件分发,已经是老生常谈的问题了,一般都是从Activity开始聊,但是真的是这样吗?hongyang大神之前一篇纠错,解决了我一个很大的困惑,也算是扫盲吧,这篇文章,是从头 ...
- 【Android】Touch事件分发
题外话: /*** A ViewGroup is a special view that can contain other views* (called children.) The view gr ...
- Android进阶知识(二十五):Bitmap简介及其高效加载
Android进阶知识(二十五):Bitmap简介及其高效加载 一.Bitmap Bitmap代表一个位图,在Android中指的是一张图片,可以是png.jpg等格式的图片.BitmapDraw ...
- Android View的事件分发机制和滑动冲突解决方案
这篇文章会先讲Android中View的事件分发机制,然后再介绍Android滑动冲突的形成原因并给出解决方案.因水平有限,讲的不会太过深入,只希望各位看了之后对事件分发机制的流程有个大概的概念,并且 ...
- Android中的事件分发和处理
上次跟大家分享了一下自定义View的一下要点,这次跟大家聊一下View的事件分发及处理,为什么主题都是View,因为作为一名初级应用层Android工程师,跟我打交道最多的莫过于各种各样的View,只 ...
- Android View的事件分发机制解析
作者:网易·周龙 最近刚看完android-Ultra-Pull-To-Refresh下拉刷新的源码,发现在写自定义控件时,对于View的事件的传递总是搞不太清楚,而View事件的分发机制,又是解决可 ...
- 对Android view/viewgroup事件分发的理解
首先看看讲事件分发的博客: http://blog.csdn.net/xiaanming/article/details/21696315 和 http://www.csdn123.com/html/ ...
最新文章
- WPF 根据绑定值设置DataGrid行背景色
- iOS APP提交上架流程
- 消息队列RocketMQ应对双十一流量洪峰的“六大武器”
- PHP易混淆函数的区分
- SAP ABAP F4的检索帮助(包括自定义检索帮助)
- Java---利用程序实现在控制台聊天
- HTML head 头标签
- 检查 ubuntu 版本_如何检查Ubuntu版本–快速简便的方法
- 供应商主数据和客户主数据各个字段进行显示、必输、隐藏和可选输入的配置学习...
- 国家开放大学2021春1067知识产权法题目
- SQL注入原理解析以及举例1
- x210ii开发板使用fastboot下载出现没有权限的问题
- 《深度学习》花书-读书笔记汇总贴(汇总19/19)
- 米谟科技 3D音频VR编辑器——sound flare声弹是什么
- 百度智能云首秀CES的主场感觉:用两个“中国第一”抛出AI新态势
- 精短高效的XML解析器,纯C单一程序,应用于银行的国税库行横向联网接口系统中,稳定可靠,运行速度飞快
- python 朋友圈点赞收费吗_微信点赞扣费是真是假 微信点赞要收钱了吗
- 影院活动管理系统 项目测试与部署
- 推荐一款结构化数据处理开源库,极大提高开发效率
- 三位数自动递增编号函数_Excel单元格自动填充编号、序列、18位长数字与数字+字母+数字...