转载请注明出处: 西木的博客
这几天在项目中用到了多层自定义view嵌套的情况,每一层的View都有自己点击、长按或者scroll事件要处理,所以经常需要去解决冲突的处理问题,那么这一期,我们就来讲讲Android中TouchEvent的传递,以及多点触控机制。
Android程序在运行时,若用户点击某一个view,就会生成一个MotionEvent,然而这个event并不会立即被传递到用户点击的view的监听器中,而是先通过顶层ViewGroup一层一层向下传递的,中间层的ViewGroup,也就是目标 View的每一层祖先,都会先收到该event,而且可以决定是否拦截该event,让其不能进一步向下传递至目标event,最典型的就是ListView,我们在滚动listview中得元素时,这个时候产生的event, 它们的目标view应该是 被触摸到的那一行view,而不是整个listview,但是listview有权拦截这个event,并针对该event做出自己的变化。那么TouchEvent是以什么顺序进行传递,并决定是否拦截的呢?请听我慢慢道来。
每当touchEvent第一次到达当前view或者viewGroup时,第一个被调用的函数便是DispatchTouchEvent,该函数,顾名思义,就是指如果有touch事件到达当前view,当前view应该以何种机制去处理这个view,是忽略呢,还是传递给子view呢,或者是直接调用当前view上的监听器呢?如果当前view是处于ViewHierarchy叶子层的view,不包含任何子view,那么就不应该传递给子view进行处理,我们来看看View类默认的dispatchTouchEvent是如何处理的:
/** * Pass the touch screen motion event down to the target view, or this * view if it is the target. * * @param event The motion event to be dispatched. * @return True if the event was handled by the view, false otherwise. */
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
if (onFilterTouchEventForSecurity(event)) {
//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;
}
}
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
// Clean up after nested scrolls if this is the end of a gesture;
// also cancel it if we tried an ACTION_DOWN but we didn't want the rest
// of the gesture.
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
我们可以看到,dispatchTouchEvent直接调用了 mOnTouchListener的onTouch进行处理,如果mOnTouchListener结果为false,则会调用onTouchEvent函数。这是在叶子层view分发事件的逻辑,只需简单调用该view上的监听函数而已。那如果当前view 还包括很多子view呢?我们再来看ViewGroup中dispatchTouchEvent的实现:
ViewGroup中dispatchTouchEvent的实现相对复杂,我们忽略掉非关键部分,并拆分成几部分进行介绍
// 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();
}
这段代码其实就是初始化一个点击事件相关的属性,一个touch事件总是由Action_down事件开始,以aciton_up事件结束,那viewGroup每次检测到down事件,相当于一个touch事件的开始。
// 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;
}
这段代码,顾名思义就是检查是否要拦截(intercept)当前event,在第一次发生down事件时,我们总是执行检查,执行检查我们先要检查一个叫做disallowIntercept的标记是否存在,如果这个标记存在,就设intercepted = false, 即不执行拦截,这个标记的意思就是当前view的某个子view,请求其父view不要拦截touchEvent,大家肯定会想起有个函数就是执行这个功能的: requestDisallowInterceptTouchEvent, 那我们先来讲解这个函数:
这个函数实现非常简单,我们先贴上源代码:
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);
}
}
这段代码,首先设置了mGroupFlags标记,disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0),布尔型变量disallowIntercept代表是否拦截事件,但是大家肯定会想,如果我作为父view开启了,不拦截子view的事件,但是如果事件在祖先view上就被拦截了呢?这时候,连父view都收不到事件,更别说去拦截了,因此这个函数最后会递归调用每一层的祖先view,请求其不要拦截事件。
说完了requestDisallowInterceptTouchEvent,让我们回到祖先,继续研究dispatchTouchEvent,如果disallowIntercept为false,就会调用onInterceptTouchEvent尝试进行拦截,这个函数里面会做真正的拦截,决定对何种touchEvent在哪种情况下进行拦截。
关于onInterceptTouchEvent,目前只需要知道这几点:
后面讨论具体的case时,我们再对onInterceptTouchEvent进行更加详细的用法了解。
继续看dispatchTouchEvent函数,接下来的一部分,将会检查cancel标记是否被设置,接着如果cancel和intercept都为false的话,就会尝试去找目标touchTarget,并做真正的dispatch。
对于touchTarget需要注意的是:
dispatchTouchEvent最后会调用dispatchTransformedTouchEvent,将event转换后传递给子view。
同样定位到源码:
//......省略一大波代码
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
//...省略一小批代码
如果child参数为null,则会调用super.dispatchTouchEvent(event),即调用View类中得dispatchTouchEvent,前面分析过,View类中的分发机制即直接调用当前view上的监听函数。如果child参数不为null,则会将event的坐标进行计算,转换为相对于child原点为参考的坐标值。再调用child的dispatchTouchEvent函数,至此就将event成功的传给了子view。
按照上述流程,一步一步向下查找,我们就能将event传递到用户真正发生点击时位于的view,有一个问题需要考虑的是:如果子view不想处理这个事件呢?该事件该何去何从呢?
我们先来看一个例子,如下图所示,蓝色部分是一个RelativeLayout, 含有红色方块和橘色方块两个子view,当我们点击了重合部分时,事件传递的顺序是如何的呢?
假设三个view事件监听器都返回false,即表示不处理该事件,但是在收到事件时打出log:
/Junli﹕ inner orange view ontouch
/Junli﹕ inner red view onTouch
/Junli﹕ wrapper onTouch
可以看到事件会最先传递给最上层的橘色view,然后交给红色view处理,最后蓝色父view自己处理,这个执行顺序看上去合情合理,代码中也可很明显的看出:
for (int i = childrenCount - 1; i >= 0; i--) {
//子view是否能接受event,以及view是覆盖当前event的坐标
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
//...
newTouchTarget = getTouchTarget(child);
//... 传递给子view进行处理
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
//...
break;
}
}
//没有找到第一目标,则父view自己处理事件,child=null 调用dispatchTransform...
if (mFirstTouchTarget == null) {
// 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.
// ....
}
到此,我们大致了上分析了事件被目标view忽略后,往回传递的流程。这一节的讲解就到这里,谢谢大家耐心的阅读。当然,对于事件的处理还有很多方面我们没有cover到,我会在下一期中继续讲解,小伙伴们,不见不散^-^