一、相关函数
Activity里,有两个回调函数 :
public boolean dispatchTouchEvent(MotionEvent ev);
public boolean onTouchEvent(MotionEvent ev);
ViewGroup里,有三个回调函数 :
public boolean dispatchTouchEvent(MotionEvent ev);
public boolean onInterceptTouchEvent(MotionEvent ev);
public boolean onTouchEvent(MotionEvent ev);
View里,有两个回调函数 :
public boolean dispatchTouchEvent(MotionEvent ev);
public boolean onTouchEvent(MotionEvent ev);
最重要的一个函数是dispatchTouchEvent,
- 它用来进行事件的分发,如果事件能够传递给当前View,那么此方法一定会被调用。返回结果受当前View的onTouchEvent方法和下级View的diapatchTouchEvent方法的影响。表示是否消耗该事件。
一个事件序列,包括 ACTION_DOWN、ACTION_MOVE、ACTION_UP 都通过dispatchTouchEvent向下传递
二、dispatchTouchEvent
2.1、 Activity.dispatchTouchEvent
Activty中的源码如下:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction(); // 空方法
}
if (getWindow().superDispatchTouchEvent(ev)) {
/*
PhoneWindow类中实现:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
DecorView里的实现:
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
调用的是父类FrameLayout的方法
*/
return true;
}
return onTouchEvent(ev);
2.2、View.dispatchTouchEvent
View.java
public boolean dispatchTouchEvent(MotionEvent event) {
//mOnTouchListener 是否处理?
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//mOnTouchListener 消费事件,则onTouchEvent尝试消费
if (!result && onTouchEvent(event)) {
result = true;
}
return result
}
View.dispatchTouchEvent()中
- 优先由onTouch()处理MotionEvent(),onTouch()处理
- mOnTouchListener.onTouch()不处理,则由onTouchEvent()处理
2.3、ViewGroup.dispatchTouchEvent
2.3.1、mFirstTouchTarget
ViewGroup.java
class ViewGroup{
// First touch target in the linked list of touch targets.
private TouchTarget mFirstTouchTarget;
}
TouchTarget 关键属性如下:
private static final class TouchTarget {
// The touched child 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.
public TouchTarget next;
}
- View child:被点击的子控件,即消耗事件的目标控件。
- int pointerIdBits:目标捕获的所有指针的指针ID的组合位掩码"。
为了区分多点触控时不同的触控点,每一个触控点都会携带一个pointerId。而pointerIdBits即是所有被目标控件消耗的触控点的pointerId的组合。即pointerIdBits包含一个或以上的pointerId数据。 - TouchTarget next:
记录下一个TouchTarget对象,由此组成链表
mFirstTouchTarget 是ViewGroup中的一个私有变量,mFirstTouchTarget是"触摸目标"链表的头部。
mFirstTouchTarget 是ViewGroup中所有child中, 消费了触摸事件的TouchTarget,仅限于ViewGroup children中消费了触摸事件的View,而不包括ViewGroup本身。
2.3.2、dispatchToucheEevnt底层逻辑
底层逻辑一:根据 mFirstTouchTarget 指向,分发事件
- mFirstTouchTarget != null,说明ViewGroup的某一个子View已经处理了ToucheEvent,则直接将TouchEvent交由mFirstTouchTarget指向的子View处理
- mFirstTouchTarget == null,说明没有子View消费TouchEvent,进而由ViewGrop自身处理
ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// 分发给touchTarget
if (mFirstTouchTarget == null) {
// 没有子view处理这次事件流,就调用自己的(父类View)的dispatchTouchEvent(),分发给自己
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) { // 这个循环似乎只会循环一次,因为mFirstTouchTarget是按下事件的target,它的后继似乎是null
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
// 如果已经有子view处理事件并且找到了,方法结果是true
handled = true;
} else { // 如果事件被拦截,会走这里
// 还没有找到相应的子view,就依次调用每个touchTarget的子view或viewGroup父类(View)的dispatchTouchEvent()
// 一旦有一个的dispatchTouchEvent()返回true,整体就返回true
// 顺便如果事件被拦截,就销毁target链表
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
// 主要由intercepted决定cancelChild是不是true
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) { // 如果cancelChild为true,也就是事件被拦截,似乎所有事件都被当成了Action_Cancel处理分发,故而此时子view只会收到cancel事件
handled = true;
}
if (cancelChild) { // 非down的事件被拦截
if (predecessor == null) { // predecessor为null
mFirstTouchTarget = next; // 之前的mFirstTouchTarget的后继为null,现在mFirstTouchTarget自己变成了null,所以之后的事件,只能走上面的分发给viewGroup自己了
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
}
底层逻辑二:MotionEvent.ACTION_DOWN 时,计算消费事件的子View: TouchTarget
- 步骤一:ACTION_DOWN是事件序列的起点,首先清空mFirstTouchTarget
- 步骤二: ViewGroup事件拦截,此处先省略
- 步骤三:步骤三: ACTION_DOWN 且ViewGroup没有拦截的情况下,遍历MotionEvent命中的子View,View dispatchTouchEvent() = true,说明消费了事件,调用 addTouchTarget,添加mFirstTouchTarget
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// 步骤一 ACTION_DOWN 时,清空mFirstTouchTarget
if (actionMasked == MotionEvent.ACTION_DOWN) {
// ACTION_DOWN的时候清空touchTarget链表,因为按下事件是事件序列的开头
cancelAndClearTouchTargets(ev);
resetTouchState(); // 这个方法里,mFirstTouchTarget被赋值为null
}
//步骤二: 此处省略 ViewGroup事件拦截
.....
.....
//步骤三: ACTION_DOWN 且ViewGroup没有拦截的情况下,遍历点中的子View,尝试子View dispatchTouchEvent(),添加mFirstTouchTarget
if (!canceled && !intercepted) {// ViewGoup 没有拦截
// Action_Down是事件的起点,仅在ACTION_DOWN时,计算确定TouchTarget
if (actionMasked == MotionEvent.ACTION_DOWN)
{
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
// 从后往前遍历
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
// 可以当成childIndex = i
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// 寻找之前确定过的处理这次事件流的子view的target,新的ACTION_DOWN事件的话,newTouchTarget为null
newTouchTarget = getTouchTarget(child);
// 调用子view的dispatchTouchEvent(),若返回为true,则记录newTouchTarget,表示已经有子view处理事件了,退出循环
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
newTouchTarget = addTouchTarget(child, idBitsToAssign);
// 将当前子view的touchTarget,此时mFirstTouchTarget被赋值,不再为null
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
}
}
}
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
底层逻辑三:ViewGroup.onInterceptTouchEvent 事件拦截
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// 步骤一 ACTION_DOWN 时,清空mFirstTouchTarget
.... 省略
//步骤二: 此处省略 ViewGroup事件拦截
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// ACTION_DOWN或者此次事件流里之前的事件有子view处理
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) { // 正常是走这儿
intercepted = onInterceptTouchEvent(ev); // 调用ViewGroup.onInterceptTouchEvent()方法
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// 没有点到这个ViewGroup的子view(说白了就是空白位置),那这次事件流的剩余事件全部被ViewGroup拦截
intercepted = true;
}
//步骤三: ACTION_DOWN 且ViewGroup没有拦截(intercepted=false)的情况下,遍历点中的子View,尝试子View dispatchTouchEvent(),添加mFirstTouchTarget
...省略
}
onInterceptTouchEvent 发生在两种场景
场景一:拦截的是 Motion.ACTION_DOWN
ACTION_DOWN 是事件流的起点,拦截了ACTION_DOWN事件,mFirstTouchTarget为null,整个事件流归ViewGroup处理;
场景二: 拦截的是MotionEvent.ACTION_MOVE(mFirstTouchTarget != null)
拦截的不是down事件,比如move事件,此时mFirstTouchTarget不是null,当前事件归子view管,但子view接收到的却变成了cancel事件,这次事件流之后的事件全归了ViewGroup
三、滑动冲突解决
1、ViewGroup拦截子View的事件
ViewGroup.onInterceptTouchEvent() 返回true,事件就会交由ViewGroup来处理
2、子View禁止父ViewGroup拦截自己的事件
requestDisallowInterceptTouchEvent()禁止父ViewGroup拦截事件
@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);
}
}
四、MotionEvent
4.1、事件坐标
每个触摸事件都代表用户在屏幕上的一个动作,
而每个动作必定有其发生的位置。
- getX()和getY():这两个函数获得的x,y值是相对的坐标值,相对于消费这个事件的视图的左上点的坐标。
- getRawX()和getRawY():有这两个函数获得的x,y值是绝对坐标,是相对于屏幕的。
4.2、事件类型
int action = MotionEventCompat.getActionMasked(event);
switch(action) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
break;
}
4.3、Pointer
为了可以表示多个触摸点的动作,MotionEvent中引入了Pointer的概念。
一个pointer就代表一个触摸点。
一个MotionEvent对象中可能会存储多个pointer的相关信息,每个pointer都会有一个自己的id和index。
pointer的id在整个事件流中是不会发生变化的,但是index会发生变化。
MotionEvent类中的很多方法都是可以传入一个int值作为参数的,其实传入的就是pointer的index值。比如getX(pointerIndex)和getY(pointerIndex),此时,它们返回的就是index所代表的触摸点相关事件坐标值。
private final static int INVALID_ID = -1;
private int mActivePointerId = INVALID_ID;
private int mSecondaryPointerId = INVALID_ID;
private float mPrimaryLastX = -1;
private float mPrimaryLastY = -1;
private float mSecondaryLastX = -1;
private float mSecondaryLastY = -1;
public boolean onTouchEvent(MotionEvent event) {
int action = MotionEventCompat.getActionMasked(event);
switch (action) {
case MotionEvent.ACTION_DOWN:
int index = event.getActionIndex();
mActivePointerId = event.getPointerId(index);
mPrimaryLastX = MotionEventCompat.getX(event,index);
mPrimaryLastY = MotionEventCompat.getY(event,index);
break;
case MotionEvent.ACTION_POINTER_DOWN:
index = event.getActionIndex();
mSecondaryPointerId = event.getPointerId(index);
mSecondaryLastX = event.getX(index);
mSecondaryLastY = event.getY(index);
break;
case MotionEvent.ACTION_MOVE:
index = event.findPointerIndex(mActivePointerId);
int secondaryIndex = MotionEventCompat.findPointerIndex(event,mSecondaryPointerId);
final float x = MotionEventCompat.getX(event,index);
final float y = MotionEventCompat.getY(event,index);
final float secondX = MotionEventCompat.getX(event,secondaryIndex);
final float secondY = MotionEventCompat.getY(event,secondaryIndex);
break;
case MotionEvent.ACTION_POINTER_UP:
xxxxxx(涉及pointer id的转换,之后的文章会讲解)
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mActivePointerId = INVALID_ID;
mPrimaryLastX =-1;
mPrimaryLastY = -1;
break;
}
return true;
}
多点触控相关的事件类型:
- ACTION_POINTER_DOWN:代表用户又使用一个手指触摸到屏幕上,也就是说,在已经有一个触摸点的情况下,有新出现了一个触摸点。
- ACTION_POINTER_UP:代表用户的一个手指离开了触摸屏,但是还有其他手指还在触摸屏上。
也就是说,在多个触摸点存在的情况下,其中一个触摸点消失了。它与ACTION_UP的区别就是,它是在多个触摸点中的一个触摸点消失时(此时,还有触摸点存在,也就是说用户还有手指触摸屏幕)产生,而ACTION_UP可以说是最后一个触摸点消失时产生。
所以多点触控的事件流 可能是这个样子的:
ACTION_DOWN ->
ACTION_POINTER_DOWN ->
ACTION_POINTER_UP ->
ACTION_UP
4.4、getAction 和 getActionMasked
MotionEvent对象只包含一个触摸点的事件时,上边两个函数的结果是相同的,但是当包含多个触摸点时,二者的结果不同。
getAction获得的int值是由pointer的index值和事件类型值组合而成的(PointerIndex + 事件类型)
getActionWithMasked则只返回事件的类型值
getAction() returns 0x0105.
getActionMasked() will return 0x0005
其中0x0100就是pointer的index值。