1 源码分析
1.1 事件分发所要经过的对象
首先从三个常见的对象开始分析:
Activity ==> Window ==> ViewGroudp ==> View
手指触摸屏幕首先碰到的是Activity,然后再一步步传下去。每一步都有可能被拦截并且返回。
有个比较形象的比喻就是Activity是老板,Window是技术经理(本身也是技术能手),ViewGroup像是组长(本身也是技术能手),View是具体的员工。
老板发布任务,技术经理查看并交给组长,组长查看再交给员工。中间随时都有可能被拦截完成任务后返回给老板。
1.2 MotionEvent 触摸事件
- ACTION_DOWN 手指刚接触屏幕
- ACTION_MOVE 手指在屏幕滑动
- ACTION_UP 手指离开屏幕
1.3 事件传递经历的方法
-
public boolean dispachTouchEvent(MotionEvent event)
该方法用于分发触摸事件
-
public boolean onIntercepTouchEvent(MotionEvent event)
该方法用于拦截触摸式见,在View中是没有该方法的。应为View直接就是末端了,没必要拦截。
-
public boolean onTouchEvent(MotionEvent event)
该方法用于消费事件
1.4 Activity
具体先看先老板也就是Activity内部是怎么拦截的,先看下Activity的dispatchTouchEvent方法:
/**
* Called to process touch screen events. You can override this to
* intercept all touch screen events before they are dispatched to the
* window. Be sure to call this implementation for touch screen events
* that should be handled normally.
*
* @param ev The touch screen event.
*
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
。。。
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
翻译的大概意思是:改方法被用于屏幕触摸事件。你可以重写该方法,用于拦截所有的触摸事件在他们分发给window之前。对于应该正常处理的触摸屏事件,请务必调用此实现。也就是说该方法是事件分发的起始点。
这边有两个分支getWindow().superDispatchTouchEvent(ev)和onTouchEvent(ev)。
先看下第一个分支,他是将分发事件通过getWindow委托给PhoneWindow去处理,也就是说核心看看superDispatchTouchEvent方法:
//@PhoneWindow:
//mDecor这个是左右界面的祖先,他是一个FrameLayout也就是ViewGroup
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
//@PhoneWindow:
//由于ViewGroup的父类是View,所以最终还是调用到了ViewGroup的dispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
所以,第一步是先跑到ViewGroup的dispatchTouchEvent方法判断是否被拦截。
如果第一步没有拦截,那么接下来执行第二部onTouchEvent(ev):
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
//@Window
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
final boolean isOutside =
event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(context, event)
|| event.getAction() == MotionEvent.ACTION_OUTSIDE;
if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
//在边界外
return true;
}
//在边界内,则表示没有被消费。返回false
return false;
}
shouldCloseOnTouch是Window自带的方法,意思就是在边界内则是没有被消费返回false,在边界外则是被消费了(被判断不可反馈)。
从上可知大概分为两个步骤。1 -> 抛给DecorView的super.dispatchTouchEvent(event)分发。由于DecorView是所有界面的起点,所以该分发开始传递直到有View消费了触摸事件。2 -> 如果发现没有View消费触摸事件则交给自身的onTouchEvent处理。他会判断在边界内还是边界外。边界内的话表示确实没人处理,返回false。
1.6 ViewGroup
这边承接上面的Activity,点击DecorView之后会调用ViewGroup的dispatchTouchEvent分发:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
。。。
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
。。。
// onInterceptTouchEvent分发判断是否需要拦截
// 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;
}
。。。
return handled;
}
首先注意到intercepted = onInterceptTouchEvent(ev)这行代码。他用来判断ViewGroup是否要拦截点击事件。
再往看会有个while循环,再它里面有个while循环遍历所有子View或者ViewGroup。
这边 onInterceptTouchEvent(ev)默认是返回false,当然如果有需求可以通过配置让ViewGroup去拦截。
这里有个关键的方法:dispatchTransformedTouchEvent,他用于分发事件给子类。接下来分析下dispatchTouchEvent里该方法的三个调用地方:
有三个步骤会去调用dispatchTransformedTouchEvent方法:
1.在Event是Down的情况下。在遍历子View的过程中,如果事件被消费,那么会通过往链表TouchTarget中填充新的target。
//如果被拦截的情况下就不会进入该函数
if (!canceled && !intercepted){
。。。
for (int i = childrenCount - 1; i >= 0; i--){
//如果子类不能接收触摸事件或者不在点击范围内。那么继续查找
if (!child.canReceivePointerEvents() || !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
。。。
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
。。。
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
。。。
}
//这边是往链表中添加target方法,也就是说如果有子View消费了事件。那么mFirstTouchTarget为非空
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
2.如果没有子View或者被拦截的情况下会调用,实际是调用View的事件处理函数。
3.在Event不是Down的情况下。将点击事件直接分发给第一点中列表所涉及到的TouchTarget链表。
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
// 如果没有子View消费或者被ViewGroup拦截的情况
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;
// 如果第一个步骤有子类View消费,那么alreadyDispatchedToNewTouchTarget为TRUE
// target == newTouchTarget,第一次触摸的View和新的触摸View 为同一个View.
// 同时满足这两个条件,也就是说之是事件类型是down的情况
if (alreadyDispatchedToNewTouchTarget是 && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
//如果ViewGroup拦截的话cancelChild会变成true,也就是会被子View当成cancel事件
//如果没有被拦截的话,就是说碰到DOWN以外的事件类型的话。直接把事件传给对应的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;
}
}
接下来看看dispatchTransformedTouchEvent里面是怎么实现的:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
。。。
}
很显然如果child为null的时候,表示没有子类拦截。那么就调用ViewGroup的dispatchTouchEvent方法。还有一点就是cancel这个标志位。上面说了,如果事件被ViewGroup拦截,那么cancel为true,所以会执行event.setAction(MotionEvent.ACTION_CANCEL)。也就是说该事件会被当成cancel事件传入子View。
1.7 View
第一步分析dispatchTouchEvent方法:
public boolean dispatchTouchEvent(MotionEvent event) {
。。。
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
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;
}
public void setOnTouchListener(OnTouchListener l) {
getListenerInfo().mOnTouchListener = l;
}
首先会判断mOnTouchListener.onTouch,判断返回如果为true。如果为true的话表示被消费了,那么接下来if (!result && onTouchEvent(event))就不会执行到onTouchEvent,也就是说触摸事件的优先级是onTouch > onTouchEvent.
接下来看看onTouchEvent里是怎么实现的:
public boolean onTouchEvent(MotionEvent event) {
switch (action) {
case MotionEvent.ACTION_UP:
。。。
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClickInternal();
}
。。。
break;
case MotionEvent.ACTION_DOWN:
。。。
break;
case MotionEvent.ACTION_CANCEL:
。。。
break;
case MotionEvent.ACTION_MOVE:
。
break;
}
return true;
}
return false;
}
public boolean performClick() {
。。。
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
。。。
return result;
}
在ACTION_UP的时候会调用post(mPerformClick)方法,该方法最终会调用到li.mOnClickListener.onClick方法。
所以总结下调用的优先级流程是 onTouch > onTouchEvent > onClick。
2 滑动冲突的分类与解决
滑动冲突分为两种:
-
外部和内部的滑动方向一致
-
外部和内部的滑动方向不一致
滑动冲突本质上是外部和内部的滑动各管各的,而实际的交互是内部外部要有个统一的交互。所以具体的解决方法应该是将交互统一个外部或者内部来处理。也就是说具体有两种方式:
-
交给外部统一改动,以下是伪代码:
//默认的情况是false,也就是父类不会去拦截 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; } return intercepted; }
-
交给内部统一改动,以下是伪代码:
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
// 禁止parent拦截down事件
getParent().requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (希望父类拦截的条件) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
return super.dispatchTouchEvent(event);
}