相关方法介绍:
dispatchTouchEvent方法:事件的分发。返回值一般由里面的onTouchEvent方法或者下架View的dispatchTouchEvent方法决定。
onInterceptTouchEvent:事件的拦截,如果Down事件一旦拦截了,那么之后的move和up事件也会一起拦截。事件不会在向下传递。
onTouchEvent:事件的处理。如果在子View中设定了mOnTouchListener,那么会优先调用mOnTouchListener的方法。如果返回true,
那么不会在调用onTouchEvent。如果返回false,那么会继续走onTouchEvent。如果一个View的down事件没有
消耗,直接返回的false,那么其他事件都不会在传递到这个View上。事件开始向上传递。
事件传递源头:Activity的dispatchTouchEvent方法。
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
由源码可以知道,触摸事件由getWindow().superDispatchTouchEvent(ev)传递下去,如果下面的UI控件没有消耗该时间
由activity的onToucHEvent去消耗。getWindow().superDispatchTouchEvent(ev)的具体实现如下:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
由代码可知,事件由整个窗口布局的根View DecorView负责进行分发。
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
在 DecorView中,直接调用了super.dispatchTouchEvent(event),而DecorView是一个FrameLayout,即该方法
走入了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();
}
如果事件是DOWN事件,那么初始化一些状态。
第二段:
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;
}
这里主要分析拦截动作。从代码可以看出如果是DOWN事件则一定会进入到onInterceptTouchEvent方法判断是否拦截
如果down事件被父控件拦截了,那么mFirstTouchTarget必定会是null,所以onInterceptTouchEvent不会在执行,
默认后续的事件都返回intercepted = true;表示拦截。而如果不是DOWN事件,那么DOWN事件必须由子控件响应过(mFirstTouchTarget!=null表示之前的DOWN事件被子空间消耗了
否则mFirstTouchTarget会依然是null)才会继续判断是否拦截。如果子控件没有响应过即子控件没有消耗过DOWN事件,那么
该地方不会在进行判断,直接默认拦截,事件不会在传递到下层子控件
由此可以得出三个结论:
A、如果父控件拦截了down事件,那么down事件不会在传递给子控件。所有其他事件都不会在传递下去并交由父控件处理
B、如果父控件没有拦截down事件,并且子控件消耗了down事件,那么后续的move up等事件父控件依然会判断是否拦截,如果拦截那么后续的事件不会在往下传递,如果拦截move事件,那么第一个move事件会变成一个cancel事件传递到子控件,详情参见第四段代码分析
C、如果父控件没有拦截down事件,并且子控件也没有消耗down事件,那么后续的事件不会在传递到子控件当中,将有父控件处理。因为mFirstTouchTarget为null
disallowIntercept该标志位表示是否拦截,由子控件去调用父控件的requestDisallowInterceptTouchEvent方法设定,如果设定为true,ViewGroup将无法拦截除了ACTION_DOWN以外的其它事件
1、子view不希望父view拦截事件可以调用mParent.requestDisallowInterceptTouchEvent(true)
2、viewgroup的mGroupFlags在下一个cycle来临的时候,FLAG_DISALLOW_INTERCEPT标志位会被清零
3、requestDisallowInterceptTouchEvent这个函数一般不是自己调用的,而是给子View调用的
4、requestDisallowInterceptTouchEvent是解决滑动冲突的大杀器,目前大部分原生控件都是使用
第三段:
if (!canceled && !intercepted) {
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// 找出可以接受事件的子控件
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
for (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;
}
}
}
}
第三段代码是核心代码,主要做了以下几个事情,前提条件是父控件没有进行拦截和取消事件:
1、从当前触摸区域找出可以相应事件的子控件;
2、通过getTouchTarget方法找到触摸的目标控件,如果之前DOWN事件触摸过,那么直接跳出循环,由后面的代码进行事件传递。
3、如果getTouchTarget方法返回一个null,则之前没有触摸过,那么则通过dispatchTransformedTouchEvent方法传递事件给对应区域的子控件。
dispatchTransformedTouchEvent方法是事件传递的关键方法,在该方法中,如果传入的child为null则表示没有找到对应的子控件消耗。那么会由父控件去消耗
但是在第三段代码中,只有找到了子控件才会调用dispatchTransformedTouchEvent方法,然后会继续调用子控件的child.dispatchTouchEvent(event);进入下一个轮训
如果子控件child.dispatchTouchEvent(event)方法返回false,表示子控件没有消耗该事件,dispatchTransformedTouchEvent也会为false,那么newTouchTarget和mFirstTouchTarget都会为null
这也就是为什么第二段代码会由结论C了。
第四段:
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.
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;
}
}
这里代码也很好理解,首先,如果在第三段代码没有找到能消耗事件的子控件,或者父控件进行了拦截没有进入到第三段代码中
那么这里会直接调用handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
child传入会null,那么里面会直接调用super.dispatchTouchEvent(event);会走到View类中的dispatchTouchEvent方法进行事件传递
表示当前触摸事件由当前父控件进行消耗。
而如果在第三段代码中找到了消耗down事件,那么在down事件走到这里之后,会直接返回handled=true。而后续的move和up事件
因为找到了mFirstTouchTarget,那么不会再走进入第三段代码,会调用
if (dispatchTransformedTouchEvent(ev, cancelChild,arget.child, target.pointerIdBits)) {
handled = true;
}
这里注意,如果进行了MOVE这个触摸事件拦截,那么会触发一个cancel的事件动作传递给之前消耗DOWN事件的子控件,并且在后面将mFirstTouchTarget置空
这样后续的move事件就不会在传递给之前的子控件了。所以一旦父控件执行了onInterceptTouchEvent并且返回true,那么后续的事件就不会再往下分发。
此时由于子控件已经消耗了down事件,如果后续的move和up事件在这里没有消耗,dispatchTransformedTouchEvent返回false,那么handled也为false,那么后续的
事件不会在传递给父控件去消耗,而是传递给activity中的onTouchEvent方法去处理。
view对touch事件的处理
1、先看view如何分发事件:dispatchTouchEvent
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;
}
}
如果view设置的enable为true并且也设定了touchEvent的监听器,那么就会先响应touchEvent的监听器。如果enable为false或者是没有设定监听器,那么会响应
view的onTouchEvent的方法
1、view的onTouchEvent的方法,如果view设置的enable为false,那么会进入下面代码
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
这个时候,如果view的clickable和longClickable是true的话,依旧会返回true,也就是说view即使设置的enable为false,还是能够响应touchEvent事件的
如果view设置的enable为true的话,那么进入action的判断状态
switch (action) {
case MotionEvent.ACTION_UP:
if (!post(mPerformClick)) {
performClick();
}
break;
case MotionEvent.ACTION_DOWN:
checkForLongClick(0, x, y);
break;
case MotionEvent.ACTION_MOVE:
break;
}
在ACTION_UP的地方,会响应click事件;在ACTION_DOWM的地方,会根据时间响应longClick事件。