嵌套滑动冲突的原因:
嵌套滑动
:一个可滑动的父View包裹了可滑动的子View,由上篇的事件分发原理分析我们得知:
父View会执行dispatchTouchEvent()决定是否拦截?如果拦截,则不传递给子类事件,如果不拦截,则遍历子类查看是否拦截。而嵌套滑动的父子View都需要拦截事件,但默认给父View拦截了,子View就滑动不了。
那么解决的方法就是,只能根据滑动的临界条件,动态的给父子View分配事件。
大致三种解决方案:
- 使用有NestedScrolling机制滑动控件:根据接口实现,动态分配事件。
- 外部拦截法:父View滑动控件,动态决定是否拦截。
- 内部拦截法:子View滑动控件,动态决定是否拦截
外部拦截法
原理:控制父View的onInterceptTouchEvent()方法,决定在什么时候拦截。
拦截时机:先判断手势的上下,然后根据滑动的子View是否已经在顶部或底部来决定是否拦截。
public class MyScrollerView extends ScrollView {
private int mLastY = 0;
、、、省略构造函数
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//默认不拦截
boolean intercepted = false;
int y = (int) ev.getY();
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
intercepted = false;
super.onInterceptTouchEvent(ev);
break;
case MotionEvent.ACTION_MOVE:
int detY = y - mLastY;
// 还要自己找子View
View childView = findViewById(R.id.child);
if (childView == null) {
return true; //拦截
}
//根据手势判断子scrollView是否在顶部或底部
boolean isChildScrolledTop = detY > 0 && !childView.canScrollVertically(-1);
boolean isChildScrolledBottom = detY < 0 && !childView.canScrollVertically(1);
if (isChildScrolledTop || isChildScrolledBottom) {
intercepted = true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
}
mLastY = y;
return intercepted;
}
}
内部拦截法
原理:由子滑动View调用requestDisallowInterceptTouchEvent()决定父View是否可拦截
public class MyScrollerView extends ScrollView {
private int mLastY = 0;
public MyScrollerView(Context context) {
super(context);
}
public MyScrollerView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyScrollerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int y = (int) ev.getY();
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
//不让父View拦截
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int detY = y - mLastY;
boolean isScrolledTop = detY > 0 && !canScrollVertically(-1);
boolean isScrolledBottom = detY < 0 && !canScrollVertically(1);
//根据自身是否滑动到顶部或者顶部来判断让父View拦截触摸事件
if (isScrolledTop || isScrolledBottom) {
//让父View拦截
getParent().requestDisallowInterceptTouchEvent(false);
}
mLastY = y;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
super.onInterceptTouchEvent(ev);
return false;
}
return true;
}
}
NestedScrolling机制
实现了NestedScrolling的常用控件有:RecyclerView、NestedScrollView、CoordinatorLayout
NestedScrolling介绍:
- NestedScrolling的分为child和parent两种,它解决嵌套滑动的主要思路是
让child在onTouchEvent中总是拦截事件,然后通过实现NestedScrollingChild和NestedScrollingParent接口来让两者相互的通信。子View总会先让父View先消耗事件,然后再自己消耗。
- 系统提供了
NestedScrollingParentHelper和NestedScrollingChildHelper
两个类中写好了parent和child的接口实例类
,我们只要实列化他们,就可以调用他们已经写好的通信方式。 - 但实例类并不是万能,在
MOVE
手势时的dispatchNestedPreScroll和dispatchNestedScroll,onNestedPreScroll和onNestedScroll。UP
手势时的dispatchNestedPreFling和dispatchNestedFling,onNestedPreFling和onNestFling,实现不同的需求逻辑。
NestedScrollingChild接口类API:
public interface NestedScrollingChild {
void setNestedScrollingEnabled(boolean enabled); //开启或关闭嵌套滑动
boolean isNestedScrollingEnabled(); //返回是否开启嵌套滑动
boolean startNestedScroll(@ScrollAxis int axes); //axes为滑动方向, 返回是否找到NestedScrollingParent配合滑动
void stopNestedScroll(); //停止嵌套滑动
boolean hasNestedScrollingParent(); //返回是否有配合滑动NestedScrollingParent
boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow); //滑动完成后,将已经消费、剩余的滑动值分发给NestedScrollingParent
boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
@Nullable int[] offsetInWindow); //在滑动之前,将滑动值分发给NestedScrollingParent
boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);//将惯性滑动的速度和NestedScrollingChild自身是否需要消费此惯性滑动分发给NestedScrollingParent
boolean dispatchNestedPreFling(float velocityX, float velocityY); //在惯性滑动之前,将惯性滑动值分发给NestedScrollingParent
}
NestedScrollingParent接口类API:
public interface NestedScrollingParent {
/**
* 对NestedScrollingChild发起嵌套滑动作出应答
* @param child 布局中包含下面target的直接父View
* @param target 发起嵌套滑动的NestedScrollingChild的View
* @param axes 滑动方向
* @return 返回NestedScrollingParent是否配合处理嵌套滑动
*/
boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes); //对NestedScrollingChild发起嵌套滑动作出应答
/**
* NestedScrollingParent配合处理嵌套滑动回调此方法
* @param child 同上
* @param target 同上
* @param axes 同上
*/
void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes);//NestedScrollingParent配合处理嵌套滑动回调此方法
/**
* 嵌套滑动结束
* @param target 同上
*/
void onStopNestedScroll(@NonNull View target);
/**
* NestedScrollingChild滑动完成后将滑动值分发给NestedScrollingParent回调此方法
* @param target 同上
* @param dxConsumed 水平方向消费的距离
* @param dyConsumed 垂直方向消费的距离
* @param dxUnconsumed 水平方向剩余的距离
* @param dyUnconsumed 垂直方向剩余的距离
*/
void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed);
/**
* NestedScrollingChild滑动完之前将滑动值分发给NestedScrollingParent回调此方法
* @param target 同上
* @param dx 水平方向的距离
* @param dy 水平方向的距离
* @param consumed 返回NestedScrollingParent是否消费部分或全部滑动值
*/
void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed);
/**
* NestedScrollingChild在惯性滑动之前,将惯性滑动的速度和NestedScrollingChild自身是否需要消费此惯性滑动分
* 发给NestedScrollingParent回调此方法
* @param target 同上
* @param velocityX 水平方向的速度
* @param velocityY 垂直方向的速度
* @param consumed NestedScrollingChild自身是否需要消费此惯性滑动
* @return 返回NestedScrollingParent是否消费全部惯性滑动
*/
boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, boolean consumed);
/**
* NestedScrollingChild在惯性滑动之前,将惯性滑动的速度分发给NestedScrollingParent
* @param target 同上
* @param velocityX 同上
* @param velocityY 同上
* @return 返回NestedScrollingParent是否消费全部惯性滑动
*/
boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY);
@ScrollAxis
int getNestedScrollAxes(); //返回当前嵌套滑动的方向
}
NestedScrollView实例分析
NestedScrollView
是一个实现了Parent、Child
的NestedScrolling的接口,接下看看它是如何通过实例帮助类,实现嵌套滑动的。
DOWN手势
- 子View滑动之前询问父View是否配合滑动
- 子:
startNestedScroll
- 父:
onStartNestedScroll,onNestedScrollAccepted
NestedScrollView:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
case MotionEvent.ACTION_DOWN: {
// ....
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
break;
}
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
case MotionEvent.ACTION_DOWN: {
// ....
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
break;
}
}
@Override
public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes,
int type) {
//如果是垂直方向就接受
return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
@Override
public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes,
int type) {
mParentHelper.onNestedScrollAccepted(child, target, axes, type);
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, type);
}
NestedScrollingChildHelper:
public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
//如果有可配合的Parent则返回true
if (hasNestedScrollingParent(type)) {
// Already in progress
return true;
}
//自己是否可滑动
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
while (p != null) {
//遍历父View的onStartNestedScroll()询问是否配合滑动
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
//保存配合的父View
setNestedScrollingParentForType(type, p);
//执行父View的onNestedScrollAccepted方法
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
//返回ture
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
//没有父View配合滑动则返回false
return false;
}
MOVE手势
- 如果有父View配合。则子View在滑动之前会把滑动值先交给父View消费,父View也将消费的值存起来返回给子View,子View则可以根据父View消耗的值来计算自己的滑动距离。子View消耗后将剩余的交给父View进行处理
- 子:
dispatchNestedPreScroll,dispatchNestedScroll
- 父:
onNestedPreScroll,onNestedScroll
NestedScrollView:
@Override
public boolean onTouchEvent(MotionEvent ev) {
// ....
case MotionEvent.ACTION_MOVE:
// ....
//滑动的距离
int deltaY = mLastMotionY - y;
// ....
if (mIsBeingDragged) {
// Start with nested pre scrolling
//滑动之前先给父View消耗此次的滑动距离
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset,
ViewCompat.TYPE_TOUCH)) {
//父View消耗后剩余的距离
deltaY -= mScrollConsumed[1];
mNestedYOffset += mScrollOffset[1];
}
// ....
//调用overScrollByCompat将调用onOverScrolled,//如果适用,则调用onScrollChanged。
if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0,
0, true) && !hasNestedScrollingParent(ViewCompat.TYPE_TOUCH)) {
// Break our velocity if we hit a scroll barrier.
mVelocityTracker.clear();
}
final int scrolledDeltaY = getScrollY() - oldY;
final int unconsumedY = deltaY - scrolledDeltaY;
mScrollConsumed[1] = 0;
//子View消耗后将剩余的距离给父View处理
dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset,
ViewCompat.TYPE_TOUCH, mScrollConsumed);
// ....
break;
return true;
}
// 消费子View滑动之前的距离
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
int type) {
dispatchNestedPreScroll(dx, dy, consumed, null, type);
}
// 消费子View滑动之后的距离
@Override
public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int type) {
onNestedScrollInternal(dyUnconsumed, type, null);
}
NestedScrollingChildHelper:
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
@Nullable int[] offsetInWindow, @NestedScrollType int type) {
if (isNestedScrollingEnabled()) {
//拿到配合滑动的父View
final ViewParent parent = getNestedScrollingParentForType(type);
if (parent == null) {
return false;
}
if (dx != 0 || dy != 0) {
int startX = 0;
int startY = 0;
if (offsetInWindow != null) {
//滑动子VIew当前的坐上坐标
mView.getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}
//给父View消耗的数组
if (consumed == null) {
consumed = getTempNestedScrollConsumed();
}
consumed[0] = 0;
consumed[1] = 0;
//调用父View的onNestedPreScroll()
ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);
if (offsetInWindow != null) {
//计算子view滑动的距离
mView.getLocationInWindow(offsetInWindow);
offsetInWindow[0] -= startX;
offsetInWindow[1] -= startY;
}
//返回父View是否消耗了距离
return consumed[0] != 0 || consumed[1] != 0;
} else if (offsetInWindow != null) {
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow) {
return dispatchNestedScrollInternal(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
offsetInWindow, TYPE_TOUCH, null);
}
private boolean dispatchNestedScrollInternal(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow,
@NestedScrollType int type, @Nullable int[] consumed) {
//找到配合滑动的父View
if (isNestedScrollingEnabled()) {
final ViewParent parent = getNestedScrollingParentForType(type);
if (parent == null) {
return false;
}
if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
int startX = 0;
int startY = 0;
//子View开始的左上位置
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}
if (consumed == null) {
consumed = getTempNestedScrollConsumed();
consumed[0] = 0;
consumed[1] = 0;
}
//执行父View的onNestedScroll()进行剩余事件消耗
ViewParentCompat.onNestedScroll(parent, mView,
dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, consumed);
// 父view消耗后子View的滑动距离
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
offsetInWindow[0] -= startX;
offsetInWindow[1] -= startY;
}
//返回true
return true;
} else if (offsetInWindow != null) {
// No motion, no dispatch. Keep offsetInWindow up to date.
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}
UP手势
- 当手指抬起时,子View则将配合的父View置空,父View也将点击类型转化为NONE。
如果手指抬起时有fling值,则先将Fling值给父View消耗,然后子View计算父View消耗的距离再决定自己的消耗。等子View消耗完之后,则再讲剩余的给父View消耗。
- 子:
stopNestScroll(),dispatchNestedPreFling(),dispatchNestedFling()
- 父:
onStopNestedScroll(),onNestedPreFling(),onNestedFling()
NestedScrollView:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
case MotionEvent.ACTION_UP:
// ....
stopNestedScroll(ViewCompat.TYPE_TOUCH);
break;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
case MotionEvent.ACTION_UP:
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
if ((Math.abs(initialVelocity) >= mMinimumVelocity)) {
if (!dispatchNestedPreFling(0, -initialVelocity)) {
dispatchNestedFling(0, -initialVelocity, true);
fling(-initialVelocity);
}
} else if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0,
getScrollRange())) {
ViewCompat.postInvalidateOnAnimation(this);
}
mActivePointerId = INVALID_POINTER;
endDrag();
break;
case MotionEvent.ACTION_CANCEL:
if (mIsBeingDragged && getChildCount() > 0) {
if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0,
getScrollRange())) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
mActivePointerId = INVALID_POINTER;
endDrag();
break;
}
private void endDrag() {
// ....
stopNestedScroll(ViewCompat.TYPE_TOUCH);
// ....
}
@Override
public void onStopNestedScroll(@NonNull View target, int type) {
mParentHelper.onStopNestedScroll(target, type);
stopNestedScroll(type);
}
@Override
public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) {
return dispatchNestedPreFling(velocityX, velocityY);
}
@Override
public boolean onNestedFling(
@NonNull View target, float velocityX, float velocityY, boolean consumed) {
if (!consumed) {
dispatchNestedFling(0, velocityY, true);
fling((int) velocityY);
return true;
}
return false;
}
NestedScrollingChildHelper:-----------------------
public void stopNestedScroll(@NestedScrollType int type) {
ViewParent parent = getNestedScrollingParentForType(type);
if (parent != null) {
//调用父View的onStopNestedScroll()方法
ViewParentCompat.onStopNestedScroll(parent, mView, type);
//设置父View为null
setNestedScrollingParentForType(type, null);
}
}
NestedScrollingParentHelper:-----------------------------
public void onStopNestedScroll(@NonNull View target, @NestedScrollType int type) {
if (type == ViewCompat.TYPE_NON_TOUCH) {
mNestedScrollAxesNonTouch = ViewGroup.SCROLL_AXIS_NONE;
} else {
mNestedScrollAxesTouch = ViewGroup.SCROLL_AXIS_NONE;
}
}
NestedScrollingChildHelper:-----------------------
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
if (isNestedScrollingEnabled()) {
ViewParent parent = getNestedScrollingParentForType(TYPE_TOUCH);
if (parent != null) {
//调用父View的onNestedPreFling()方法
return ViewParentCompat.onNestedPreFling(parent, mView, velocityX,
velocityY);
}
}
return false;
}
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
if (isNestedScrollingEnabled()) {
ViewParent parent = getNestedScrollingParentForType(TYPE_TOUCH);
if (parent != null) {
return ViewParentCompat.onNestedFling(parent, mView, velocityX,
velocityY, consumed);
}
}
return false;
}
流程图:
image.png
自定义简易版NestedScrollingParent类
public class NestedScrollLayout extends NestedScrollView {
private View topView;
private ViewGroup contentView;
private static final String TAG = "NestedScrollLayout";
public NestedScrollLayout(Context context) {
this(context, null);
init();
}
public NestedScrollLayout(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
init();
}
public NestedScrollLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
init();
}
public NestedScrollLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr);
init();
}
private FlingHelper mFlingHelper;
int totalDy = 0;
/**
* 用于判断RecyclerView是否在fling
*/
boolean isStartFling = false;
/**
* 记录当前滑动的y轴加速度
*/
private int velocityY = 0;
private void init() {
mFlingHelper = new FlingHelper(getContext());
setOnScrollChangeListener(new View.OnScrollChangeListener() {
@Override
public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
if (isStartFling) {
totalDy = 0;
isStartFling = false;
}
if (scrollY == 0) {
Log.i(TAG, "TOP SCROLL");
// refreshLayout.setEnabled(true);
}
if (scrollY == (getChildAt(0).getMeasuredHeight() - v.getMeasuredHeight())) {
Log.i(TAG, "BOTTOM SCROLL");
dispatchChildFling();
}
//在RecyclerView fling情况下,记录当前RecyclerView在y轴的偏移
totalDy += scrollY - oldScrollY;
}
});
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
topView = ((ViewGroup) getChildAt(0)).getChildAt(0);
contentView = (ViewGroup) ((ViewGroup) getChildAt(0)).getChildAt(1);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 调整contentView的高度为父容器高度,使之填充布局,避免父容器滚动后出现空白
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
ViewGroup.LayoutParams lp = contentView.getLayoutParams();
lp.height = getMeasuredHeight();
contentView.setLayoutParams(lp);
}
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
Log.i("NestedScrollLayout", getScrollY()+"::onNestedPreScroll::"+topView.getMeasuredHeight());
// 向上滑动。若当前topview可见,需要将topview滑动至不可见
boolean hideTop = dy > 0 && getScrollY() < topView.getMeasuredHeight();
if (hideTop) {
scrollBy(0, dy);
consumed[1] = dy;
}
}
private void dispatchChildFling() {
if (velocityY != 0) {
Double splineFlingDistance = mFlingHelper.getSplineFlingDistance(velocityY);
if (splineFlingDistance > totalDy) {
childFling(mFlingHelper.getVelocityByDistance(splineFlingDistance - Double.valueOf(totalDy)));
}
}
totalDy = 0;
velocityY = 0;
}
private void childFling(int velY) {
RecyclerView childRecyclerView = getChildRecyclerView(contentView);
if (childRecyclerView != null) {
childRecyclerView.fling(0, velY);
}
}
@Override
public void fling(int velocityY) {
super.fling(velocityY);
//记录速度
if (velocityY <= 0) {
this.velocityY = 0;
} else {
isStartFling = true;
this.velocityY = velocityY;
}
}
private RecyclerView getChildRecyclerView(ViewGroup viewGroup) {
for (int i = 0; i < viewGroup.getChildCount(); i++) {
View view = viewGroup.getChildAt(i);
if (view instanceof RecyclerView && view.getClass() == NestedLogRecyclerView.class) {
return (RecyclerView) viewGroup.getChildAt(i);
} else if (viewGroup.getChildAt(i) instanceof ViewGroup) {
ViewGroup childRecyclerView = getChildRecyclerView((ViewGroup) viewGroup.getChildAt(i));
if (childRecyclerView instanceof RecyclerView) {
return (RecyclerView) childRecyclerView;
}
}
continue;
}
return null;
}
}
网友评论