Activity:
public boolean dispatchTouchEvent(MotionEvent event)
public boolean onTouchEvent(MotionEvent event)
ViewGroup:
public boolean dispatchTouchEvent(MotionEvent event)
public boolean onInterceptTouchEvent(MotionEvent ev)
public boolean onTouchEvent(MotionEvent event)
View:
public boolean dispatchTouchEvent(MotionEvent event)
public boolean onTouchEvent(MotionEvent event)
伪代码表示dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent三者之间的关系:
public boolean dispatchTouchEvent (MotionEvent ev){
boolean consume = false;
if (onInterceptTouchEvent(ev)) {
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
用图来表示事件传递的过程:
来看ViewGroup的分发(PS:本文中的源码是基于Android API 24分析的~):
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
------other code------
// 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;
}
------other code------
handled = super.dispatchTouchEvent(transformedEvent);
}
当ViewGroup不拦截事件并由ViewGroup的子View处理事件时,mFirstTouchTarget会被赋值并指向子View,上面的代码可以分成下面几种情况:
1、如果MotionEvent.ACTION_DOWN事件被ViewGroup拦截,那么mFirstTouchTarget==null,那么后续的ACTION_MOVE、ACTION_UP事件都不会往子View中传递了,而会走ViewGroup的onTouchEvent方法。
2、当mFirstTouchTarget != null时,即子View处理后续ACTION_MOVE、ACTION_UP事件时,子View中可以通过设置getParent().requestDisallowInterceptTouchEvent(true)
,该设置会影响上述代码中的disallowIntercept
变量,从而使ViewGroup不拦截事件,将事件传递到子View中去
下面分析一下当遇到滑动冲突的时候一般的解决方法:
外部拦截法即在父ViewGroup的onInterceptTouchEvent中去做处理,伪代码如下:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean isIntercept = false;
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
isIntercept = false;
break;
case MotionEvent.ACTION_MOVE:
if (ViewGroup拦截ACTION_MOVE条件) {
isIntercept = true;
} else {
isIntercept = false;
}
break;
case MotionEvent.ACTION_UP:
isIntercept = false;
break;
}
return isIntercept;
}
这里注意除非父ViewGroup要处理所有事件,否则一定不能拦截ACTION_DOWN
,因为一旦拦截了ACTION_DOWN
事件,后续的MOVE、UP事件将都不能传递到子View了
内部拦截法是指父ViewGroup不拦截任何事件,所有事件都传递到子View中,通过getParent().requestDisallowInterceptTouchEvent(boolean)
来控制后续事件让不让父ViewGroup去拦截。来看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);
}
}
主要是修改标志位mGroupFlags
,这个标志位也是在父ViewGroup的dispatchTouchEvent中控制是否走onInterceptTouchEvent()拦截方法的,disallowIntercept为true时,父ViewGroup不会再走onInterceptTouchEvent()拦截事件;反之会走onInterceptTouchEvent()方法,默认是false。
内部拦截法的使用举例:
在子View的dispatchTouchEvent中:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if (父ViewGroup需要拦截并处理事件) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
}
return super.dispatchTouchEvent(event);
}
在父ViewGroup的onInterceptTouchEvent中:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return ev.getAction() != MotionEvent.ACTION_DOWN;
}
内部拦截法同样不能拦截ACTION_DOWN事件,否则事件不能传递到子View中。
在开始介绍滑动冲突之前,先介绍一下测量规格MeasureSpec
,MeasureSpec
参与了View的测量过程,子View的MeasureSpec的创建是由父View的MeasureSpec和子View自身的LayoutParams共同决定的,MeasureSpec
的组成:
MeasureSpec是一个32位的int值,高2位是specMode
,低30位是specSize
,如下:
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size, @MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
SpecMode是测量模式,SpecSize是在某种测量模式下的测量大小,SpecMode有三个值:
USPECIFIED
: 父View对子View没有任何限制。
EXACTLY
: 父View已经检测出View的精确大小,View的最终大小就是SpecSize指定的值。它对应于LayoutParams中的match_parent和具体数值这两种模式。
AT_MOST
:父View指定了一个可用大小SpecSize,View的大小不能大于这个值,具体是什么值要看不同View的具体实现。它对应于LayoutParams中的wrap_content。
默认ListView中的onMeasure方法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// Sets up mListPadding
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-----其他代码-----
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (heightMode == MeasureSpec.UNSPECIFIED) {
heightSize = mListPadding.top + mListPadding.bottom + childHeight + getVerticalFadingEdgeLength() * 2;
}
if (heightMode == MeasureSpec.AT_MOST) {
heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
}
}
通过源码我们知道当测量模式是UNSPECIFIED时,高度只是一个item的高度(包括上下的padding);当测量模式是AT_MOST时,高度是所有item的高度,即整个listview的高度。通过测试发现ScrollView和ListView嵌套使用时,传给ListView的测量模式是UNSPECIFIED,所以只能显示一个Item的高度,那怎么显示整个ListView的高度呢?通过上面的分析我们已经知道答案了:重写ListView的onMeasure方法!
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int heightSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, heightSpec);
}
放弃通过父View及自身LayoutParams生成的MeasureSpec(specMode为UNSPECIFIED),重新生成一个specMode为AT_MOST的MeasureSpec即可。
来看ViewPager的onMeasure源码:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// For simple implementation, our internal size is always 0.
// We depend on the container to specify the layout size of
// our view. We can't really know what it is since we will be
// adding and removing different arbitrary views and do not
// want the layout to change as this happens.
setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
getDefaultSize(0, heightMeasureSpec));
}
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
通过分析源码我们知道:
测量模式specMode是UNSPECIFIED
时,ViewPager的高度是0;
测量模式specMode是AT_MOST
或EXACTLY
时,ViewPager的高度直接取的父View传入的值。
通过测试发现当ScrollView和ViewPager嵌套使用时,测量模式specMode是UNSPECIFIED
,所以默认高度是0.
从Android5.0开始引入了NestedScrolling机制(5.0之前可以用Support V4包向前兼容),用来处理子View和父View嵌套滑动时的交互机制。子View一般是可以滑动的View并且需要实现 NestedScrollingChild 接口,父View需要实现NestedScrollingParent接口。
public interface NestedScrollingChild {
public void setNestedScrollingEnabled(boolean enabled);
public boolean isNestedScrollingEnabled();
public boolean startNestedScroll(int axes);
public void stopNestedScroll();
public boolean hasNestedScrollingParent();
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
public boolean dispatchNestedPreFling(float velocityX, float velocityY);
}
public interface NestedScrollingParent {
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
public void onStopNestedScroll(View target);
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed);
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
public boolean onNestedPreFling(View target, float velocityX, float velocityY);
public int getNestedScrollAxes();
}
一、startNestedScroll
首先子view需要开启整个流程(内部主要是找到合适的能接受nestedScroll的parent),通知父View,我要和你配合处理TouchEvent
二、dispatchNestedPreScroll
在子View的onInterceptTouchEvent或者onTouch中(一般在 MontionEvent.ACTION_MOVE事件里),调用该方法通知父View滑动的距离。该方法的第三第四个参数返回父view消费掉的 scroll长度和子View的窗体偏移量。如果这个scroll没有被消费完,则子view进行处理剩下的一些距离,由于窗体进行了移动,如果你记录了手指最后的位置,需要根据第四个参数offsetInWindow计算偏移量,才能保证下一次的touch事件的计算是正确的。
如果父view接受了它的滚动参数,进行了部分消费,则这个函数返回true,否则为false。
这个函数一般在子view处理scroll前调用。
三、dispatchNestedScroll
向父view汇报滚动情况,包括子view消费的部分和子view没有消费的部分。
如果父view接受了它的滚动参数,进行了部分消费,则这个函数返回true,否则为false。
这个函数一般在子view处理scroll后调用。
四、stopNestedScroll
结束整个流程。
更详细见:
http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0603/2990.html
直接见github吧:
https://github.com/crazyqiang/AndroidStudy
或者见鸿神的博客:
https://blog.csdn.net/lmj623565791/article/details/52204039