我们知道两个同一方向的可以滑动的View,如果不做任何处理,会出现滑动冲突,处理滑动冲突我们有内部和外部拦截法。
如果有朋友还不知道事件分发的原理可以看包括滑动冲突的拦截事件分发源码的学习分享
处理滑动冲突之后还是存在缺陷,比如一次滑动操作父View消费完之后子View没有办法继续消费,滑动惯性也无法传递,所有就有了:
NestedScrollingParent和NestedScrollingChild
而我们使用的时候是通过NestedScrollingParentHelper和NestedScrollingChildHelper
可滑动的ViewGroup
NestedScrollingParent 充当父View
NestedScrollingChild 充当子View
一个嵌套滑动子View和父View的简单交互流程。
事件的消费是从子View开始的
case MotionEvent.ACTION_DOWN:{
mLastTouchY = (int)(event.getRawY() + .5f);
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
//上面判断是Y轴滑动还是X轴滑动,把对应的轴传递进去
startNestedScroll(nestedScrollAxis);
break;
}
@Override
public boolean startNestedScroll(int axes) {
//使用对应的helper来帮助我们调用
return helper.startNestedScroll(axes);
}
@Override
public boolean startNestedScroll(int axes, int type) {
//type 做了兼容处理
return mChildHelper.startNestedScroll(axes, type);
}
public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
//如果有了能嵌套滑动的父View,直接返回
if (hasNestedScrollingParent(type)) {
// Already in progress
return true;
}
//当前是否支持嵌套滑动
if (isNestedScrollingEnabled()) {
//拿当前View的父View
ViewParent p = mView.getParent();
//把当前View赋值为child
View child = mView;
//循环遍历往上找,直到找到支持嵌套滑动的父View
while (p != null) {
//如果你是父View:NestedScrollingParent,
//onStartNestedScroll方法返回true的话进入if
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
//将父Viewt的type存储起来,你是直接touch还是Fing
setNestedScrollingParentForType(type, p);
//将父View嵌套滑动的Y轴还是X轴存储起来
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
return true;
}
//继续往上查找,支持多层嵌套
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
上面的onStartNestedScroll调用到ViewParentCompat,就是一个兼容处理,后续不再贴出了
public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
int nestedScrollAxes, int type) {
if (parent instanceof NestedScrollingParent2) {
// First try the NestedScrollingParent2 API
return ((NestedScrollingParent2) parent).onStartNestedScroll(child, target,
nestedScrollAxes, type);
} else if (type == ViewCompat.TYPE_TOUCH) {
// Else if the type is the default (touch), try the NestedScrollingParent API
if (Build.VERSION.SDK_INT >= 21) {
try {
return parent.onStartNestedScroll(child, target, nestedScrollAxes);
} catch (AbstractMethodError e) {
Log.e(TAG, "ViewParent " + parent + " does not implement interface "
+ "method onStartNestedScroll", e);
}
} else if (parent instanceof NestedScrollingParent) {
return ((NestedScrollingParent) parent).onStartNestedScroll(child, target,
nestedScrollAxes);
}
}
return false;
}
找到可支持滑动嵌套的父View之后下一步是在move事件中
case MotionEvent.ACTION_MOVE:{
int x = (int)(event.getRawX() + .5f);
int y = (int)(event.getRawY() + .5f);
int dx = mLastTouchX -x;
int dy = mLastTouchY -y;
mLastTouchX = x;
mLastTouchY = y;
//将滑动情况传给父View,让父View先决定是否需要消费
//consumed是数组,0==x轴,1==y轴
if(dispatchNestedPreScroll(dx,dy,consumed,null)){
Log.i("onMeasure","dy: " + dy + ", cosumed: " + consumed[1]);
//父View消费了Y轴多少需要减掉
dy -= consumed[1];
if(dy == 0){
Log.i("onMeasure","dy: " + dy);
return true;
}
}else{
Log.i("onMeasure","scrollBy: " + dy);
scrollBy(0,dy);
//如果子View没消费完可以再次传给父View dispatchNestedScroll
}
break;
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
Log.i(Tag, "dispatchNestedPreScroll:dx" + dx + ",dy:" + dy + ",
consumed:" + consumed[1] + ",offsetInWindow:" + offsetInWindow);
return helper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
@Nullable int[] offsetInWindow, @NestedScrollType int type) {
//再次验证判断是否支持嵌套滑动
if (isNestedScrollingEnabled()) {
final ViewParent parent = getNestedScrollingParentForType(type);
if (parent == null) {
return false;
}
if (dx != 0 || dy != 0) {
int startX = 0;
int startY = 0;
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}
//上面主要是做一个视差效果处理
if (consumed == null) {
consumed = getTempNestedScrollConsumed();
}
//确保consumed不是null
consumed[0] = 0;
consumed[1] = 0;
//将参数传给父View
ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
offsetInWindow[0] -= startX;
offsetInWindow[1] -= startY;
}
return consumed[0] != 0 || consumed[1] != 0;
} else if (offsetInWindow != null) {
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}
父View处理
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
boolean show = showImg(dy);
boolean hide = hideImg(dy);
// Log.i("onMeasure","show: " + show + ", hide: " + hide);
if(show||hide){//1.作业 消费过头
consumed[1] = dy;//全部消费
scrollBy(0, dy);
Log.i(Tag,"Parent滑动:"+dy);
}
Log.i(Tag, "onNestedPreScroll--getScrollY():" + getScrollY() + ",dx:" + dx + ",dy:" + dy + ",consumed:" + consumed[1]);
}
最后:
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
stopNestedScroll();
从子View的down事件开始我们需要调用startNestedScroll(传入需要嵌套滑动的轴)–》
内部代码找到支持嵌套滑动的父View…–》onNestedPreScroll(父View处理)返回处理结果
其实还有Fing的传递,后面讲,大方向就是类似上面这样。
我们常用的滑动列表的控件有recyclerView
public class RecyclerView extends ViewGroup implements ScrollingView,
NestedScrollingChild2, NestedScrollingChild3
可以看出它实现了嵌套滑动的子View
当页面再稍微复杂点,我们还需要一个NestedScrollView
public class NestedScrollView extends FrameLayout implements NestedScrollingParent3,
NestedScrollingChild3, ScrollingView
即可以当父View也可以当子View
下面我们来看下NestedScrollView包裹着recyclerView的情况下嵌套滑动如何处理
当两者嵌套滑动的时候,不需要我们去处理滑动冲突,谷歌已经帮我们处理好了,那么很明显,down事件被recyclerView获取到。
recyclerView的ACTION_DOWN–》 startNestedScroll(nestedScrollAxis, TYPE_TOUCH);–》onStartNestedScroll(找到支持嵌套滑动的父View)–》onNestedScrollAccepted(记录相关信息)
recyclerView的ACTION_MOVE–》dispatchNestedPreScroll(滑动之前先问一下父View你滑不滑动)下面会触发父View
NestedScrollLayout 的onNestedPreScroll方法
按照我们上面一开始的分析,接下来就在父View的onNestedPreScroll方法里处理滑动逻辑就OK了,但是NestedScrollLayout即是父View也子View,所以它又调用了dispatchNestedPreScroll方法,它没有滑动。
当我们需要它先滑动一部分的时候怎么办?
继承,重写对应的方法即可:
topView是recyclerView上面NestedScrollLayout里面的一块布局
@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);
//告诉子View我滑了多少
consumed[1] = dy;
}
}
当我们设置(默认)是惯性滑动的时候会调用
@Override
public void fling(int velocityY) {
super.fling(velocityY);
if (velocityY <= 0) {
this.velocityY = 0;
} else {
isStartFling = true;
this.velocityY = velocityY;
}
}
根据自己判断,当我滑完了还剩下的多少没滑完传递给子View
private void dispatchChildFling() {
if (velocityY != 0) {
//velocityY滑动速度转换成距离
Double splineFlingDistance = mFlingHelper.getSplineFlingDistance(velocityY);
if (splineFlingDistance > totalDy) {
//将距离减去我滑去剩下再转成速度,因为对方只支持接收速度的变量
childFling(mFlingHelper.getVelocityByDistance(splineFlingDistance -
Double.valueOf(totalDy)));
}
}
totalDy = 0;
velocityY = 0;
}
private void childFling(int velY) {
//找到我的子View中的recyclerView
RecyclerView childRecyclerView = getChildRecyclerView(contentView);
if (childRecyclerView != null) {
//把速度传给它
childRecyclerView.fling(0, velY);
}
}
可能很多朋友都知道 CoordinatorLayout天然就能支持这种实现,为什么还要花时间去研究这个呢,其实 CoordinatorLayout内部的滑动原理核心就是这个,下一篇我们就来研究 CoordinatorLayout的behavior