在进入正题之前我们先啰嗦点别的东西。想必大家都做过对一个Button同时做Click和Touch事件的监听处理吧,里边出现的情况估计大家也是不陌生的。可是里边的原理至少我没有去看源码弄清楚过,现在我就带着以下问题去源码中找找答案了。
问题一:为什么只有View的Touch事件的监听中return false时View的Click事件监听才能得到执行?
问题二:View的Click事件到底是什么时候才执行的?
我们将代码跟到View的setOnTouchListener,代码如下:
/**
* Register a callback to be invoked when a touch event is sent to this view.
* @param l the touch listener to attach to this view
*/
public void setOnTouchListener(OnTouchListener l) {
getListenerInfo().mOnTouchListener = l;
}
其参数正是我们调用时创建的View.OnTouchListener,所以getListenerInfo().mTouchListener不为null。我们再将目标跟进到getListenerInfo()方法内部。代码如下:
ListenerInfo getListenerInfo() {
if (mListenerInfo != null) {
return mListenerInfo;
}
mListenerInfo = new ListenerInfo();
return mListenerInfo;
}
可以看到这里做了一个单例处理。故此,mListenerInfo不为null。
下面我们看一下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) {
........
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;
}
}
..........
我只将相关的代码拿出来了,不相关的就省略了。根据上面的分析,mListenerInfo和mTouchListener都不为null,且View是可点击的所以,(mViewFlags & ENABLED_MASK)== ENABLED 也成立,而li.mOnTouchListener.onTouch(this,event) 是否成立就取决于我们调用setOnTouchListener(new View.OnTouchListener({....}))时,重写的onTouch方法的返回值。如果该返回值为true时,将会对result赋值为true,所以下面的代码if(!result&&onTouchEvent(event)),onTouchEvent(event)将得不到执行。到此,问题一我们分析清楚了,下面我们看一下问题二。
继续将目标跟进到onTouchEvent(event)方法内部,代码如下:
public boolean onTouchEvent(MotionEvent event) {
.......
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
.......
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
// 关键代码
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
......
}
}
同样我也只是贴出了关键部分的代码,我们可以看到,在MotionEvent.ACTION_UP,事件中有调用performClickInternal(),貌似跟click事件有关。我们不妨继续跟进该方法,代码如下:
/**
* Entry point for {@link #performClick()} - other methods on View should call it instead of
* {@code performClick()} directly to make sure the autofill manager is notified when
* necessary (as subclasses could extend {@code performClick()} without calling the parent's
* method).
*/
private boolean performClickInternal() {
// Must notify autofill manager before performing the click actions to avoid scenarios where
// the app has a click listener that changes the state of views the autofill service might
// be interested on.
notifyAutofillManagerOnClick();
// 关键代码
return performClick();
}
代码中我们可以看到,performClick()方法作为了performClickInternal()方法的返回值。好像离Click事件监听更近了一步,我们继续跟进performClick()方法。代码如下:
public boolean performClick() {
// We still need to call this method to handle the cases where performClick() was called
// externally, instead of through performClickInternal()
notifyAutofillManagerOnClick();
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
// 关键代码
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
代码不多我索性全贴出来了,我们可以看到li.mOnClickListener.onClick(this);这不正是我们设置点击事件时调用的那个OnClickListener吗。所以,到此我们第二个问题也找到了答案,Click事件是在MotionEvent.ACTION_UP的时候调用到的。这个问题分析清楚了,下面我们该进入正题了。
--------------------------由滑动冲突看事件分发------------------------------
概述:
在做APP开发的过程中经常会有多个可滑动控件的嵌套使用的需求,这样就很有可能会出现滑动冲突。滑动冲突的解决也有多种方法,可不论哪种方法它的原理都离不开事件分发机制。可以说如果不懂事件分发机制的原理那我们面对滑动冲突就只能是去百度或Google了,并且在面试的过程中,事件分发机制也是深受面试官青睐的一个问题。所以事件分发机制的重要性是可想而知的。下面为了能够更清晰的讲述事件分发的原理我自己制造了一个冲突场景(在实际开发中这个冲突出现的可能性很小)。
冲突实例:
首先自定义一个ViewPager,重写其onInterceptTouchEvent(MotionEvent ev)方法。
public class BadViewPager extends ViewPager {
public BadViewPager(@NonNull Context context) {
super(context);
}
public BadViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
/**
* 这个方法在开发中一般是不需要重写的,因为ViewPager已经对其做了处理,
* 这里是为了描述问题才这样写的。
* 我们可以通过return不同的值来分析不同的场景
* @param ev
* @return
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// return true;
return false;
}
}
然后自定义一个ListView,代码如下所示:
public class MyListView extends ListView {
public MyListView(Context context) {
super(context);
}
public MyListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
}
下面我贴一下Activity和ViewPager条目的布局文件:
Activity:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.dispatchevent.view.BadViewPager
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
ViewPager的条目布局:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.dispatchevent.view.MyListView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:background="@color/colorAccent"/>
</FrameLayout>
至于ViewPagerAdapter和Activity的代码我就省略不写了。
大家都知道事件序列是有DOWN --> MOVE ..... ->> UP组成的。
我们先分析一下Down事件的流程:首先看一些ViewGroup中的dispatchToUCEvent()方法。代码如下:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
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;
}
}
}
代码是经过极度精简的。
首先:由于分析的是Down事件所以这个判断一定能执行,先暂定disallowIntercept为false,intercepted是onIntercpetTouchEvent方法的返回值,大多数情况下也为false。继续往下分析:
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
// canceled和intercepted都为false
if (!canceled && !intercepted) {
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
// 分析1
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
for (int i = childrenCount - 1; i >= 0; i--) {
// 分析2
// isTransformedTouchPointInView方法用来判断点击区域是否在当前View的内部
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
// 分析3
newTouchTarget = getTouchTarget(child);
// // 分析4
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// 分析5
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
现在我们看分析1:
跟进buildTouchDispatchChildList()方法。代码如下:
public ArrayList<View> buildTouchDispatchChildList() {
return buildOrderedChildList();
}
继续查看buildOrderedChildList()方法,代码如下:
ArrayList<View> buildOrderedChildList() {
for (int i = 0; i < childrenCount; i++) {
.....
while (insertIndex > 0 && mPreSortedChildren.get(insertIndex - 1).getZ() > currentZ) {
insertIndex--;
}
mPreSortedChildren.add(insertIndex, nextChild);
}
return mPreSortedChildren;
}
经分析,最终是一个根据View的Z值倒序添加到集合中的排序,这样做可以使得后续的点击事件好处理一些。
分析3,我们看一下getTouchTarget(child)方法的原码。
private TouchTarget getTouchTarget(@NonNull View child) {
for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
if (target.child == child) {
return target;
}
}
return null;
}
因为mFirstTouchTarget=null,所以执行完这个方法之后newTouchTarget的值仍为null。
分析4:dispatchTransformedTouchEvent(ev,false,child,idBitsToAssign)方法。看源码:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}
因为boolean cancel参数我们传进去的是false,child不为null,所以在该方法中,代码会执行child.dispatchTouchEvent(transformedEvent);
所以,这也就是将Down事件分发给了当前View的子View,上面分析View的OnTouch事件的监听的时候已经分析过了View的dispatchTouchEvent方法了。这里不再赘述,现在我们接着往下分析。
分析5:addTouchTarget(child,idBitsToAssign)方法源码如下:
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
这段代码将target赋值给了mFirstTouchTarget,所以newTouchTarget = mFirstTouchTarget不为null。
并且将alreadyDispatchedToNewTouchTarget设置为了true,之后便break掉了,所以当前for循环就会退出,这也就是将事件分发给了子View后当前View不会再执行的原因。
我们继续往下分析ViewGroup的dispatchTouchEvent方法中的其他代码:
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
} else {
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
// 分析1
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;
}
}
通过上面我们知道mFirstTouchTarget不为null,所以直接进入else。alreadyDispatchedToNewTouchTarget是为true的,
mFirstTouchTarget和newTouchTarget是相等的。所以将handled赋值为true,看系统源码可知最终是将此handled作为返回值return了。
因为while循环的判断条件是target不为null,target.next是为null的,而最后又将next赋值给了target,因此可见while循环只可进入一次。
至此,Down事件的分发流程我们就分析完了。
父View的拦截方法返回true时Down事件的流程
下面我们把ViewPager的onInterceptTouchEvent方法的返回值改为true,在分析一下Down事件。上面的分析1处的if条件不满足,所以整个if代码块不会执行。此时代码将执行如下的部分:
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
}
再来分析dispatchTransformedTouchEvent方法,由于child传的是null,所以将会执行下面的部分:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
// Perform any necessary transformations and dispatch.
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}
代码将会执行自身的dispatchTouchEvent方法,所以事件将会被自己处理。本例的运行效果就是只能有ViewPager的左右滑动的效果,而ListView的滑动得不到执行。
父View的拦截方法返回false时Move事件的流程
上面已经分析过Down事件的正常流程,也就是ViewPager的onInterceptTouchEvent方法返回false时的流程。现在分析此时Move事件的流程。首先dispatchTouchEvent方法代码执行:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
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;
}
}
}
由于是Move事件if条件中第一个不满足,但是Move事件是在Down事件之后才执行的,经上面的分析,Down完成之后mFirstTouchTarget不为null。所以此if代码块仍然可以进入,此时intercepted仍是false。接着往下看:
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
// canceled和intercepted都为false
if (!canceled && !intercepted) {
.....
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
.......
}
}
由于是Move事件,所以if(actionMasked == MotionEvent.ACTION_DOWN......)此代码块不能执行。
此时代码就又回到了如下的地方:
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
} else {
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
// 分析1
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
......
}
}
}
代码很熟悉,正是在分析Down事件时看过的,不过这里不同的是,alreadyDispatchedToNewTouchTarget=false,所以代码会走else。intercepted为false,所以cancelChild 也为false。再次进入dispatchTransformedTouchEvent方法。
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
// Perform any necessary transformations and dispatch.
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
// child不为null,执行此处代码
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}
在此方法中,cancel为false,child不为null,所以代码执行child.dispatchTouchEvent(transformedEvent);。至于View的dispatchTouchEvent方法上边分析OnTouch监听事件的时候已经分析过了。在此案例中,此种情形便是只有ListView的滑动效果,并没有ViewPager的滑动效果了。
通过内部拦截法对ListView进行改造
改造内部拦截法需要MyListView添加如下代码:
private int mLastX, mLastY;
// 内部拦截法
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(ev);
}
此时若父View的onInterceptTouchEvent返回true,运行结果是只能有ViewPager的左右滑动的效果并没有ListView的上下滑动的效果。
分析原因:看一下requestDisallowInterceptTouchEvent方法的代码。
@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);
}
}
MyListView的Down事件时,该方法传入了true,所以执行了mGroupFlags |= FLAG_DISALLOW_INTERCEPT。此时mGroupFlags一定不为0。此时再执行dispatchTouchEvent方法
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
mGroupFlags一个不为0的数&FLAG_DISALLOW_INTERCEPT(不为0得数),结果一定不为0,所以disallowIntercept为true。
因此:dispatchTouchEvent中的
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
}
将得不到执行,所以父View的onInterceptTouchEvent得不到执行。分析到这里肯定有人会觉得既然父View的onInterceptTouchEvent得不到执行,MyListView可以执行才对啊,也就是这样处理MyListView的滑动效果当是没有问题才对啊。这样想的同学大概是忽略了其中一点(其实我也是忽略了),现在我们就来把忽略掉的给补出来。在Down事件的时候父View的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();
}
接着看一下resetTouchState()方法。
/**
* Resets all touch state in preparation for a new cycle.
*/
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
原来是对mGroupFlags进行了处理。mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT。经此处理后,mGroupFlags一定为0,然后再执行dispatchTouchEvent方法中的
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
0&FLAG_DISALLOW_INTERCEPT结果一定为0,所以disallowIntercept一定为false,后续的流程又一定能正常进行了。因此我们可以知道Down事件的时候父View的onInterceptTouchEvent一定能够得到执行,而此时父View的onInterceptTouchEvent方法返回值为true,也就是ViewPager能够执行,而MyListView不能执行了。因此,我们在使用内部拦截法解决滑动冲突的时候父View的onInterceptTouchEvent方法需要分开来处理。
ViewPager的处理如下:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// return true;
// 内部拦截法
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
super.onInterceptTouchEvent(ev);
return false;
}
return true;
}
这样在Down事件的时候返回false,这样就可以保证MyListView的正常滑动效果了。
现在我们分析一下此时的Move事件的流程。继续看dispatchTouchEvent方法:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
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;
}
}
}
此时if代码块中mFirstTouchTarget不为null。经上面的分析disallowIntercept为false。但是intercepted为true。继续跟源码:
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
} else {
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
// 分析1
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
// 分析2
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
}
}
会直接进入else代码块,alreadyDispatchedToNewTouchTarget为false,所以会执行while循环里的else代码块,cancelChild为true。我们继续看dispatchTransformedTouchEvent方法:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
// 分析1
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
// Done.
transformedEvent.recycle();
return handled;
}
因为传入的cancel为true,所以在dispatchTransformedTouchEvent方法会执行我贴出来的部分。此段代码分析1我们可以看到,将此时的事件类型置为了ACTION_CANCEL。这也就是为什么事件被上层拦截时会触发ACTION_CANCEL事件的原因了。
此事child不为null,所以代码会执行child.dispatchTouchEvent(event);。但此时event已经是ACTION_CANCEL了。我们跟进此方法看一下源码:
// View 的dispatchTouchEvent方法
public boolean dispatchTouchEvent(MotionEvent event) {
......
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
我们可以看到如果是ACTION_CANCEL事件,会停止滑动事件。此时将会继续执行父View的dispatchTouchEvent方法,执行的代码是上述代码中的分析2。
// 分析2
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
此时cancelChild依然为true,所以将next赋值给了mFirstTouchTarget,上面分析过next是为null的,所以此时mFirstTouchTarget也为null了。此时跳出while循环。但是在一次事件序列中是有多个Move事件的,所以dispatchTouchEvent方法会再次进入,此时就直接进入mFirstTouchTarget==null的判断中了,代码将再次走到dispatchTransformedTouchEvent方法中,child为null的情况。代码如下:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
// Perform any necessary transformations and dispatch.
if (child == null) {
// child为null,执行此处代码
handled = super.dispatchTouchEvent(transformedEvent);
} else {
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}
此时事件就又从子View(MyListView)切换成父View(ViewPager)了。这样分析后,想必大家也能理解为什么Down事件的时候是MyLIstView处理,而Move事件又是ViewPager处理了。
对于外部拦截法这里我只贴一下代码好了。只需要对ViewPager做以下改变即可:
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
// 外部拦截法
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
mLastX = (int) event.getX();
mLastY = (int) event.getY();
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
return true;
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
return super.onInterceptTouchEvent(event);
}
这篇文章写到这里也就算是结束了。本文主要是对Down和Move事件在源码中一个流程的分析。至于具体事件分发从Activity到ViewGroup再到View以及它们各自的流程没有做介绍。这里我推荐大家阅读几篇文章,他们比我理解的更加透彻。在此谢过他们两位了。也希望大家能给我点个赞,有什么不足的也可以评论指出,谢谢大家了。
Android面试题精选:讲一讲 Android 的事件分发机制