1.NestedScrollingParent和NestedScrollingChild的作用
其实NestedScrolling就是父View和子View之间的一套滑动交互机制。简单点来说就是要求子View在准备滑动之前将滑动的细节信息传递给父View,父View可以决定是否部分或者全部消耗掉这次滑动,并使用消耗掉的值在子View滑动之前做自己想做的事情,子View会在父View处理完后收到剩余的没有被父View消耗掉的值,然后再根据这个值进行滑动。滑动完成之后如果子View没有完全消耗掉这个剩余的值就再告知一下父View,我滑完了,但是还有剩余的值你还要不要?
NestedScrollingChild的话,Lollipop及以上版本可滑动的View如ScrollView、ListView已经按此交互流程为我们处理好了一切,如果你需要自定义View或者要兼容Lollipop以下版本就需要自己实现上述所有逻辑,当然更好的办法是使用support包里为我们提供的,比如NestedScrollView,RecyclerView等。 NestedScrollingParent就需要我们自己去实现了,毕竟父View要实现什么酷炫的效果还是需要我们去定义的,当然,support包中也有一系列为我们准备好的Parent,就是design包中的CoordinatorLayout。
2.分析

图中的onXXX开头的方法都是父View的方法。
下面分析嵌套滚动的实现方法:
1)子View的实现
(1)创建一个final的NestedScrollingChildHelper
如果允许子View和父View进行嵌套滑动交互,那么不要忘了调用setNestedScrollingEnabled方法,设置为true,如果没有设置为true,那么子View调用startNestedScroll方法的时候,父View是不会有任何反应的。下面的代码中,在子View的构造方法里调用了setNestedScrollingEnabled(true),其实在子View创建实例对象后再调用setNestedScrollingEnabled(true)也没有问题。
//1(步骤一)创建一个final的NestedScrollingChildHelper
private final NestedScrollingChildHelper mChildHelper;
public ChildView(Context context) {
this(context, null);
}
public ChildView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public ChildView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mChildHelper = new NestedScrollingChildHelper(this);
setNestedScrollingEnabled(true);
}
(2)调用NestedScrollingChildHelper对象的同名方法
在子View里实现NestedScrollingChild的接口,并调用NestedScrollingChildHelper同名方法。如下方法:
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);
操作如下:
@Override
public void setNestedScrollingEnabled(boolean enabled) {
mChildHelper.setNestedScrollingEnabled(enabled);
showLog("----------------setNestedScrollingEnabled");
}
@Override
public boolean isNestedScrollingEnabled() {
showLog("----------------isNestedScrollingEnabled");
return mChildHelper.isNestedScrollingEnabled();
}
@Override
public boolean startNestedScroll(int axes) {
showLog("----------------startNestedScroll");
return mChildHelper.startNestedScroll(axes);
}
/**
* 当抬起手势的时候,默认会调用stopNestedScroll
*/
@Override
public void stopNestedScroll() {
mChildHelper.stopNestedScroll();
showLog("----------------stopNestedScroll");
}
@Override
public boolean hasNestedScrollingParent() {
showLog("----------------hasNestedScrollingParent");
return mChildHelper.hasNestedScrollingParent();
}
/**
* 当子类调用dispatchNestedScroll分发消费距离给父View的时候,
* 会触发父View的onNestedScroll方法
* @param dxConsumed
* @param dyConsumed
* @param dxUnconsumed
* @param dyUnconsumed
* @param offsetInWindow
* @return
*/
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
showLog("----------------dispatchNestedScroll");
return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
}
/**
* 一般在子类里的onTouchEvent方法里调用,当子类调用dispatchNestedPreScroll分发消费距离给父View的时候,
* 会触发父View的onNestedPreScroll方法,如果父View要消费,那么子View则不进行任何滚动操作,
* 如果父View不消费,子View自己消费滚动操作
* @param dx
* @param dy
* @param consumed
* @param offsetInWindow
* @return
*/
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
showLog("----------------dispatchNestedPreScroll");
return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
/**
* 当子View ACTION_UP时可能伴随着fling的产生,如果产生了fling,就需要子View在stopNestedScroll
* 前调用public boolean dispatchNestedPreFling(View target, float velocityX, float velocityY)和
* public boolean dispatchNestedFling(View target, float velocityX, float velocityY, boolean consumed),
* 父View对应的会被回调public boolean onNestedPreFling(View target, float velocityX, float velocityY)和
* public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed)
* @param velocityX
* @param velocityY
* @param consumed
* @return
*/
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
showLog("----------------dispatchNestedFling");
return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
}
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
showLog("----------------dispatchNestedPreFling");
return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
}
(3)在onTouchEvent里定义手势操作,决定谁消费滚动距离
float moveYs;//移动手势其点
boolean isStartNestedScrloll = false;
@Override
public boolean onTouchEvent(MotionEvent event) {
float downY;
float downX;
float moveY;
// float moveYs = 0;//移动手势其点
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
downX = event.getRawX();
//moveYs定义必须放到onTouchEvent外边,否则moveYs拿到的是默认值,
// 因为MotionEvent.ACTION_DOWN和MotionEvent.ACTION_MOVE手势的触发都是会重新调用onTouchEvent
//这就导致moveYs = 0
moveYs = downY = event.getRawY();
isStartNestedScrloll = startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
showLog( "ACTION_DOWN x:"+downX+" y:" + downY);
break;
case MotionEvent.ACTION_MOVE:
moveY = event.getRawY();
float dy = moveY - moveYs;
moveYs = moveY;
//如果开始嵌套滚动且分发给父View的距离被父View消耗的话,那么子View不做任何操作,否则子View自己进行消化,执行scrollBy
if (!(isStartNestedScrloll&&dispatchNestedPreScroll(0,(int)dy, consumed, offset))) {
showLog( "ACTION_DOWN dy:"+dy);
scrollBy(0, -(int)dy);
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
super.onTouchEvent(event);
return true;
}
2)父View的实现
(1)创建一个final的NestedScrollingParentHelper
//1(步骤一)创建一个final的NestedScrollingParentHelper
private final NestedScrollingParentHelper mParentHelper;
public ParentLayout(Context context) {
this(context, null);
}
public ParentLayout(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public ParentLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mParentHelper = new NestedScrollingParentHelper(this);
}
(2)调用NestedScrollingParentHelper对象的同名方法
在父View里实现NestedScrollingParent的接口,并调用NestedScrollingParentHelper同名方法。如下方法:
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();
其中,onNestedScrollAccepted和onStopNestedScroll方法需要调用NestedScrollingParentHelper的同名方法。
/*
* 如果父View的onStartNestedScroll方法返回值是true,那么onNestedScrollAccepted方法将会被调用
*
*/
@Override
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
showLog("----------------onNestedScrollAccepted");
}
/**
* 当允许和父View进行嵌套滚动交互,且onStartNestedScroll返回true的时候,
* 当子View抬起手势的时候,默认会调用onStartNestedScroll
* 当子View抬起手势的时候的日志:
* 05-07 11:05:30.480 16320-16320/com.tan.lgy.realizeas E/ParentLayout: ----------------onStartNestedScroll
* 05-07 11:05:30.480 16320-16320/com.tan.lgy.realizeas E/ParentLayout: ----------------onNestedScrollAccepted
* 05-07 11:05:30.530 16320-16320/com.tan.lgy.realizeas E/ParentLayout: ----------------onStopNestedScroll
* @param target
*/
@Override
public void onStopNestedScroll(View target) {
mParentHelper.onStopNestedScroll(target);
showLog("----------------onStopNestedScroll");
}
(3)复写onStartNestedScroll方法
//(步骤二)调用NestedScrollingParentHelper对象的同名方法。
/*
* 当子View调用startNestedScroll方法的时候,如果子View调用setNestedScrollingEnabled(true),
* 允许和父View进行嵌套滚动交互(默认是false),那么父View会回调onStartNestedScroll方法
*/
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
showLog("----------------onStartNestedScroll");
if (target instanceof ChildView) {
return true;
}
return false;
}
(4)复写onNestedPreScroll,onNestedScroll,onNestedFling或onNestedPreFling方法
这里需要复写onNestedPreScroll,onNestedScroll,onNestedFling或onNestedPreFling里的哪个方法,完全是根据需求来决定:
如果子View调用了dispatchNestedPreScroll方法,那么父View对应的就需要实现onNestedPreScroll方法;
如果子View调用了dispatchNestedScroll方法,那么父View对应的就需要实现onNestedScroll方法;
如果子View调用了dispatchNestedFling方法,那么父View对应的就需要实现onNestedFling方法;
如果子View调用了dispatchNestedPreFling方法,那么父View对应的就需要实现onNestedPreFling方法。
如下,我再子View里调用了dispatchNestedPreScroll方法,那么在父View里就需要复写onNestedPreScroll。在这个方法里,决定了父View要不要消费子View的滚动距离,通过consumed数组来告诉子View, consumed[0]表示x轴上的距离,consumed[1]表示y轴上的距离。
/**
* 当子类调用dispatchNestedPreScroll分发消费距离给父View的时候,
* 会触发父View的onNestedPreScroll方法,如果父View要消费,
* 那么子View则不进行任何滚动操作,如果父View不消费,子View自己消费滚动操作
* @param target
* @param dx
* @param dy
* @param consumed
*/
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
showLog("----------------onNestedPreScroll");
if (showImg(dy) || hideImg(dy)) {//如果需要显示或隐藏图片,即需要自己(parent)滚动
scrollBy(0, -dy);//滚动
consumed[1] = dy;//告诉child我消费了多少
}
}
(5)限制父View的活动范围
如下,会将父View的y轴方向滚动范围限制在0 – imgHeight里,
//scrollBy内部会调用scrollTo
//限制父View滚动范围
@Override
public void scrollTo(int x, int y) {
if (y < 0) {
y = 0;
}
if (y > imgHeight) {
y = imgHeight;
}
super.scrollTo(x, y);
}
3.总结
1) Lollipop及以上版本的所有View都已经支持了NestedScrolling,Lollipop之前版本可以通过Support包进行向前兼容,NestedScrollingParent和NestedScrollingChild都是接口,分别对应ViewGroup和View中新增的方法,在Lollipop以下版本中,我们需要手动添加这两个接口的实现。
2) 当子View调用startNestedScroll方法的时候,如果子View调用setNestedScrollingEnabled(true), 允许和父View进行嵌套滚动交互(默认是false),那么父View会回调onStartNestedScroll方法。如果父View的onStartNestedScroll方法返回值是true,那么父类的onNestedScrollAccepted方法将会被调用。
3) 如果子View调用了dispatchNestedPreScroll方法,那么父View对应的就需要实现onNestedPreScroll方法。
4) 如果子View调用了dispatchNestedScroll方法,那么父View对应的就需要实现onNestedScroll方法。
5) 如果子View调用了dispatchNestedFling方法,那么父View对应的就需要实现onNestedFling方法。
6) 如果子View调用了dispatchNestedPreFling方法,那么父View对应的就需要实现onNestedPreFling方法。
网友评论