简介
上一篇文章【view】- 触摸事件分发(1)讲解了从底层到上层的触摸事件传递。这篇文章将具体Activity,View组件的触摸事件的传递,以及事件的分类,拦截,不同的处理会引起触摸事件流程那些变化等。
注意
如果对源码分析不感兴趣,只想知道结论,可以直接翻到文章最后看总结,如果想了解源码实现,可以参照这篇文章,自己追踪源码。
Activity
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction();}
if (getWindow().superDispatchTouchEvent(ev)) {return true;}
return onTouchEvent(ev);
}
如果是按下事件,在进行事件分发前调用onUserInteraction()方法,在Activity该方法是没有任何实现,我们可以重新该方法。
getWindow().superDispatchTouchEvent(ev)会把触摸事件交给整个View组件之间传递,如果返回true,在Activity中触摸事件传递结束,如果反复false者调用onTouchEvent(MotionEvent event)方法。
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
Activity中逻辑很简单,下面讲解在DecorView顶层View之间的触摸事件传递。
DecorView
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
调用父类ViewGroup中的dispatchTouchEvent(MotionEvent ev)
辅助功能,残障。
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}
如果窗口是被遮盖,不可见,onFilterTouchEventForSecurity返回false,放弃这次触摸事件的处理。
public boolean onFilterTouchEventForSecurity(MotionEvent event) {
if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
&& (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
// Window is obscured, drop this touch.
return false;
}
return true;
}
如果是第一次按下操作,清除所有以前的状态,由于应用程序切换,ANR或某些其他状态更改,框架可能已放弃上一个手势的上移或取消事件,所以进行一些清理操作。
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
int类型mGroupFlags变量的初始化,我把常量换成具体的值
rivate void initViewGroup() {
...
mGroupFlags |= 0x1;
mGroupFlags |= 0x4;
mGroupFlags |= 0x10
mGroupFlags |= 0x40
mGroupFlags |= 0x4000
if (mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB) {
mGroupFlags |= 0x200000
}
setDescendantFocusability(FOCUS_BEFORE_DESCENDANTS);
...
public void setDescendantFocusability(int focusability) {
...
mGroupFlags &= ~0x60000;
mGroupFlags |= (0x20000 & 0x60000);
}
检查拦截,如果是MotionEvent.ACTION_DOWN或者mFirstTouchTarget不等于null,调用onInterceptTouchEvent方法,判断当前View是否要要拦截触摸事件。
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;
}
如果被拦截,开始正常事件分发。另外,如果已经有一个正在处理手势的视图,则进行常规事件调度。
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
如果事件没有撤销并且没有拦截,进入if代码块,把触摸事件分发给子控件。
获取手指索引,从注释看出,对于按下操作actionIndex ==0,单指操作。
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
对子控件进行排序,因为控件会存在相互叠加的部分,优先顶层
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
从顶层View,也就是最先收到触摸事件的View向下遍历,获取子控件。
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
...
}
将不可见,在动画中,手指在控件之外不能接收触摸事件的控件过滤掉
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
将触摸事件分发给符合要求的子控件。
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign))
在dispatchTransformedTouchEvent方法中,如果触摸事件已经取消或者撤销了,那么调用父类dispatchTouchEvent方法。
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;
}
分发事件给子控件,调用子控件的dispatchTouchEvent方法
if (newPointerIdBits == oldPointerIdBits) {
...
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
...
handled = child.dispatchTouchEvent(event);
...
}
return handled;
}
...
}
如果子控件没有重写这个方法,那么会调用到View的dispatchTouchEvent方法。接下来分析View中的dispatchTouchEvent方法。
如果是按下操作,停止嵌套滚动
if (actionMasked == MotionEvent.ACTION_DOWN) {
stopNestedScroll();
}
调用控件的onTouchEvent方法
if (!result && onTouchEvent(event)) {
result = true;
}
看一下View中的onTouchEvent方法。
如果设置了委托触摸事件处理实例,者直接调用委托的onTouchEvent。
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {return true;}
}
执行控件的点击事件监听回调中的onClick方法。
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
...
if (mPerformClick == null) {mPerformClick = new PerformClick();}
if (!post(mPerformClick)) {performClickInternal();}
}
}
View中的onTouchEvent方法剩余的逻辑自己分析,不在讲解。
回调ViewGroup中的dispatchTouchEvent方法中。如果dispatchTransformedTouchEvent方法返回true,代表被子空间或者父类处理了触摸事件,那么退出循环,不在分发事件给下一个子控件。
总结
下面容器以ViewGroup为中dispatchTouchEvent讲解,默认其它容器没有重写该容器里的方法。
ACTION_DOWN
-
第一次触发down事件
如果当前触摸事件窗口是被遮盖(比如有其它可见窗口遮挡在上面),即onFilterTouchEventForSecurity返回false,那么丢弃该次触摸事件处理,handled=false,返回handled。把事件交给Activity处理。
-
onFilterTouchEventForSecurity返回true,即当前控件可以对触摸事件进行处理。
当前控件是否允许拦截触摸事件。boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
没有其它设置的话,此时mGroupFlags值是0x244053,&0x80000等于0,所以disallowIntercept值是false,表示ViewGroup默认允许拦截触摸事件,但是需要询问ViewGroup的onInterceptTouchEvent是否可以拦截,如果不重写该方法,返回false,依旧表示ViewGroup不拦截事件。
正常情况下,变量canceled = false,split = true。遍历容器中所有的子控件,将触摸事件分发给子控件。在分发dispatchTransformedTouchEvent方法中,如果传入的View是null,者调用ViewGroup的父类的dispatchTouchEvent,如果不是null,调用子控件的dispatchTouchEvent,并返回dispatchTouchEvent返回的值。
如果子控件中有消耗了触摸事件(即dispatchTouchEvent返回true),者停止遍历,后面的子控件将接收不到该触摸事件并创建TouchTarget对象target,将消耗事件的View和手指编号存在这个target中,且target.next = mFirstTouchTarget,将新创建的target赋值给mFirstTouchTarget并返回target。
这个时候其实newTouchTarget和mFirstTouchTarget是同一个实例对象,且alreadyDispatchedToNewTouchTarget赋值为true。
根据上面,可以得知handled被直接赋值为true,而下一个target是null,while循环终止。
最后直接返回handled值。
如果子控件没有消耗,后面的逻辑更简单,直接交给容器的父类处理。
如果容器一开始要拦截该触摸事件呢?即intercepted被赋值为true,那么mFirstTouchTarget也会为null,直接将事件交给父类处理,返回父类处理的结果。
-
父类处理触摸事件
这里ViewGroup的父类是View。首先停止嵌套滚动,如果View符合接收触摸事件的要求,调用View的onTouchEvent方法,如果消耗了事件,result赋值true,然后返回result值。onTouchEvent方法中,如果View的可点击属性没有使能,直接返回clickable(是否点击使能)值。
如果设置的委托事件处理实例,将触摸事件交给委托实例,然后直接返回true。
后面的逻辑就会执行点击事件,回调点击事件监听方法,包括点击,长按等,最后返回true。所以有些时候,重写了onTouchEvent发现点击事件不能回调问题。
-
-
第二次触发down事件
比如在界面滑动等,可能会触发第二次down,第二次由于newTouchTarget = getTouchTarget(child);
返回不为null,所以会直接终止循环。
if (newTouchTarget != null) { ... break; }
ACTION_UP
按下并松开手指,这时候触发ACTION_UP事件。通过上面的分析这时候,如果ViewGroup在ACTION_DOWN不拦截,那么mFirstTouchTarget不等于null,那么不管现在ViewGroup是否拦截,不好意思,我都只会把触摸事件交给上一个消耗了ACTION_DOWN事件的View,如果都不消耗,我给Activity。
总结:如果你连ACTION_DOWN事件都不消耗,那么后面的事件序列你也别想消耗。