本文已在公众号鸿洋原创发布。未经许可,不得以任何形式转载!java
NestedScrolling是Android5.0推出的嵌套滑动机制,可以让父View和子View在滑动时相互协调配合能够实现连贯的嵌套滑动,它基于原有的触摸事件分发机制上为ViewGroup和View增长处理滑动的方法提供调用,后来为了向前兼容到Android1.6,在Revision 22.1.0的android.support.v4兼容包中提供了从View、ViewGroup抽取出NestedScrollingChild、NestedScrollingParent两个接口和NestedScrollingChildHelper、NestedScrollingParentHelper两个辅助类来帮助控件实现嵌套滑动,CoordinatorLayout即是基于这个机制实现各类神奇的滑动效果。android
public class MyScrollView 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;
//调用ScrollView的onInterceptTouchEvent()初始化mActivePointerId
super.onInterceptTouchEvent(ev);
break;
case MotionEvent.ACTION_MOVE:
int detY = y - mLastY;
//这里要找到子ScrollView
View contentView = findViewById(R.id.my_scroll_inner);
if (contentView == null) {
return true;
}
//判断子ScrollView是否滑动到顶部或者顶部
boolean isChildScrolledTop = detY > 0 && !contentView.canScrollVertically(-1);
boolean isChildScrolledBottom = detY < 0 && !contentView.canScrollVertically(1);
if (isChildScrolledTop || isChildScrolledBottom) {
intercepted = true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
}
mLastY = y;
return intercepted;
}
}
复制代码
public class MyScrollView extends ScrollView {
private int mLastY = 0;
//此处省略构造方法
...
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int y = (int) ev.getY();
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
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) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
}
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;
}
}
复制代码
上面经过两种经典的解决方案,在内部View能够滑动时,外部View不拦截,当内部View滑动到底部或者顶部时,让外部消费滑动事件进行滑动。通常而言,外部拦截法和内部拦截法不能公用。 不然内部容器可能并无机会调用 requestDisallowInterceptTouchEvent方法。在传统的触摸事件分发中,若是不手动调用分发事件或者去发出事件,外部View最早拿到触摸事件,一旦它被外部View拦截消费了,内部View没法接收到触摸事件,同理,内部View消费了触摸事件,外部View也没有机会响应触摸事件。 而接下介绍的NestedScrolling机制,在一次滑动事件中外部View和内部View都有机会对滑动进行响应,这样处理滑动冲突就相对方便许多。数组
NestedScrollingChild(下图简称nc)、NestedScrollingParent(下图简称np)逻辑上分别对应以前内部View和外部View的角色,之因此称之为逻辑上是由于View能够同时扮演NestedScrollingChild和NestedScrollingParent,下面图片就是NestedScrolling的交互流程。 app
接下来详细说明一下上图的交互流程:ide
1.当NestedScrollingChild接收到触摸事件MotionEvent.ACTION_DOWN时,它会往外层布局遍历寻找最近的NestedScrollingParent请求配合处理滑动。因此它们之间层级不必定是直接上下级关系。函数
2.若是NestedScrollingParent不配合NestedScrollingChild处理滑动就没有接下来的流程,不然就会配合处理滑动。工具
3.NestedScrollingChild要滑动以前,它先拿到MotionEvent.ACTION_MOVE滑动的dx,dy并将一个有两个元素的数组(分别表明NestedScrollingParent要滑动的水平和垂直方向的距离)做为输出参数一同传给NestedScrollingParent。布局
4.NestedScrollingParent拿到上面【3】NestedScrollingChild传来的数据,将要消费的水平和垂直方向的距离传进数组,这样NestedScrollingChild就知道NestedScrollingParent要消费滑动值是多少了。post
5.NestedScrollingChild将【2】里拿到的dx、dy减去【4】NestedScrollingParent消费滑动值,计算出剩余的滑动值;若是剩余的滑动值为0说明NestedScrollingParent所有消费了NestedScrollingChild不该进行滑动;不然NestedScrollingChild根据剩余的滑动值进行消费,而后将本身消费了多少、还剩余多少汇报传递给NestedScrollingParent。动画
6.若是NestedScrollingChild在滑动期间发生的惯性滑动,它会将velocityX,velocityY传给NestedScrollingParent,并询问NestedScrollingParent是否要所有消费。
7.NestedScrollingParent收到【6】NestedScrollingChild传来的数据,告诉NestedScrollingChild是否所有消费惯性滑动。
8.若是在【7】NestedScrollingParent没有所有消费惯性滑动,NestedScrollingChild会将velocityX,velocityY、自身是否须要消费所有惯性滑动传给NestedScrollingParent,并询问NestedScrollingParent是否要所有消费。
9.NestedScrollingParent收到【8】NestedScrollingChild传来的数据,告诉NestedScrollingChild是否所有消费惯性滑动。
10.NestedScrollingChild中止滑动时通知NestedScrollingParent。
PS:
- A.上面的【消费】是指可滑动View调用自身的滑动方法进行滑动来消耗滑动数值,好比scrollBy()、scrollTo()、fling()、offsetLeftAndRight()、offsetTopAndBottom()、layout()、Scroller、LayoutParams等,View实现NestedScrollingParent、NestedScrollingChild只仅仅是能将数值进行传递,须要配合Touch事件根据需求去调用NestScrolling的接口和辅助类,而自己不支持滑动的View即便有嵌套滑动的相关方法也不能进行嵌套滑动。
- B.在【1】中外层实现NestedScrollingParent的View不应拦截NestedScrollingChild的MotionEvent.ACTION_DOWN;在【2】中若是NestedScrollingParent配合处理滑动时,实现NestedScrollingChild的View应该经过getParent().requestDisallowInterceptTouchEvent(true)往上递归关闭外层View的事件拦截机制,这样确保【3】中NestedScrollingChild先拿到MotionEvent.ACTION_MOVE。具体能够参考RecyclerView和NestedScrollView源码的触摸事件处理。
前面提到Android 5.0及以上的View、ViewGroup自身分别就有NestedScrollingChild和NestedScrollingParent的方法,而方法逻辑就是对应的NestedScrollingChildHelper和NestedScrollingParentHelper的具体方法实现,因此本小节不讲解View、ViewGroup的NestedScrolling机制相关内容,请自行查看源码。
public interface NestedScrollingChild {
/** * @param enabled 开启或关闭嵌套滑动 */
void setNestedScrollingEnabled(boolean enabled);
/** * @return 返回是否开启嵌套滑动 */
boolean isNestedScrollingEnabled();
/** * 沿着指定的方向开始滑动嵌套滑动 * @param axes 滑动方向 * @return 返回是否找到NestedScrollingParent配合滑动 */
boolean startNestedScroll(@ScrollAxis int axes);
/** * 中止嵌套滑动 */
void stopNestedScroll();
/** * @return 返回是否有配合滑动NestedScrollingParent */
boolean hasNestedScrollingParent();
/** * 滑动完成后,将已经消费、剩余的滑动值分发给NestedScrollingParent * @param dxConsumed 水平方向消费的距离 * @param dyConsumed 垂直方向消费的距离 * @param dxUnconsumed 水平方向剩余的距离 * @param dyUnconsumed 垂直方向剩余的距离 * @param offsetInWindow 含有View今后方法调用以前到调用完成后的屏幕坐标偏移量, * 可使用这个偏移量来调整预期的输入坐标(即上面4个消费、剩余的距离)跟踪,此参数可空。 * @return 返回该事件是否被成功分发 */
boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow);
/** * 在滑动以前,将滑动值分发给NestedScrollingParent * @param dx 水平方向消费的距离 * @param dy 垂直方向消费的距离 * @param consumed 输出坐标数组,consumed[0]为NestedScrollingParent消耗的水平距离、 * consumed[1]为NestedScrollingParent消耗的垂直距离,此参数可空。 * @param offsetInWindow 同上dispatchNestedScroll * @return 返回NestedScrollingParent是否消费部分或所有滑动值 */
boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow);
/** * 将惯性滑动的速度和NestedScrollingChild自身是否须要消费此惯性滑动分发给NestedScrollingParent * @param velocityX 水平方向的速度 * @param velocityY 垂直方向的速度 * @param consumed NestedScrollingChild自身是否须要消费此惯性滑动 * @return 返回NestedScrollingParent是否消费所有惯性滑动 */
boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
/** * 在惯性滑动以前,将惯性滑动值分发给NestedScrollingParent * @param velocityX 水平方向的速度 * @param velocityY 垂直方向的速度 * @return 返回NestedScrollingParent是否消费所有惯性滑动 */
boolean dispatchNestedPreFling(float velocityX, float velocityY);
}
复制代码
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);
/** * NestedScrollingParent配合处理嵌套滑动回调此方法 * @param child 同上 * @param target 同上 * @param axes 同上 */
void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes);
/** * 嵌套滑动结束 * @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);
/** * @return 返回当前嵌套滑动的方向 */
int getNestedScrollAxes();
}
复制代码
NestedScrollingChildHepler对NestedScrollingChild的接口方法作了代理,您能够结合实际状况借助它来实现,如:
public class MyScrollView extends View implements NestedScrollingChild{
...
@Override
public boolean startNestedScroll(int axes) {
return mChildHelper.startNestedScroll(axes);
}
}
复制代码
这里只分析关键的方法,具体代码请参考源码。
public boolean startNestedScroll(int axes) {
//判断是否找到配合处理滑动的NestedScrollingParent
if (hasNestedScrollingParent()) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {//判断是否开启滑动嵌套
ViewParent p = mView.getParent();
View child = mView;
//循环往上层寻找配合处理滑动的NestedScrollingParent
while (p != null) {
//ViewParentCompat.onStartNestedScroll()会判断p是否实现NestedScrollingParent,
//如果则将p转为NestedScrollingParent类型调用onStartNestedScroll()方法
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
mNestedScrollingParent = p;
//经过ViewParentCompat调用p的onNestedScrollAccepted()方法
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
复制代码
这个方法首先会判断是否已经找到了配合处理滑动的NestedScrollingParent、若找到了则返回true,不然会判断是否开启嵌套滑动,若开启了则经过构造函数注入的View来循环往上层寻找配合处理滑动的NestedScrollingParent,循环条件是经过ViewParentCompat这个兼容类判断p是否实现NestedScrollingParent,如果则将p转为NestedScrollingParent类型调用onStartNestedScroll()方法若是返回true则证实找配合处理滑动的NestedScrollingParent,因此接下来一样借助ViewParentCompat调用NestedScrollingParent的onNestedScrollAccepted()。
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {//若是开启嵌套滑动并找到配合处理滑动的NestedScrollingParent
if (dx != 0 || dy != 0) {//若是有水平或垂直方向滑动
int startX = 0;
int startY = 0;
if (offsetInWindow != null) {
//先记录View当前的在Window上的x、y坐标值
mView.getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}
//初始化输出数组consumed
if (consumed == null) {
if (mTempNestedScrollConsumed == null) {
mTempNestedScrollConsumed = new int[2];
}
consumed = mTempNestedScrollConsumed;
}
consumed[0] = 0;
consumed[1] = 0;
//经过ViewParentCompat调用NestedScrollingParent的onNestedPreScroll()方法
ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed);
if (offsetInWindow != null) {
//将以前记录好的x、y坐标减去调用NestedScrollingParent的onNestedPreScroll()后View的x、y坐标,计算得出偏移量并赋值进offsetInWindow数组
mView.getLocationInWindow(offsetInWindow);
offsetInWindow[0] -= startX;
offsetInWindow[1] -= startY;
}
//consumed数组的两个元素的值有其中一个不为0则说明NestedScrollingParent消耗的部分或者所有滑动值
return consumed[0] != 0 || consumed[1] != 0;
} else if (offsetInWindow != null) {
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}
复制代码
这个方法首先会判断是否开启嵌套滑动并找到配合处理滑动的NestedScrollingParent,若符合这两个条件则会根据参数dx、dy滑动值判断是否有水平或垂直方向滑动,如有滑动调用mView.getLocationInWindow()将View当前的在Window上的x、y坐标值赋值进offsetInWindow数组并以startX、startY记录,接下来初始化输出数组consumed、并经过ViewParentCompat调用NestedScrollingParent的onNestedPreScroll(),再次调用mView.getLocationInWindow()将调用NestedScrollingParent的onNestedPreScroll()后的View在Window上的x、y坐标值赋值进offsetInWindow数组并与以前记录好的startX、startY相减计算得出偏移量,接着以consumed数组的两个元素的值有其中一个不为0做为boolean值返回,若条件为true说明NestedScrollingParent消耗的部分或者所有滑动值。
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {//若是开启嵌套滑动并找到配合处理滑动的NestedScrollingParent
if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {//若是有消费滑动值或者有剩余滑动值
int startX = 0;
int startY = 0;
if (offsetInWindow != null) {
//先记录View当前的在Window上的x、y坐标值
mView.getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}
//经过ViewParentCompat调用NestedScrollingParent的onNestedScroll()方法
ViewParentCompat.onNestedScroll(mNestedScrollingParent, mView, dxConsumed,
dyConsumed, dxUnconsumed, dyUnconsumed);
if (offsetInWindow != null) {
//将以前记录好的x、y坐标减去调用NestedScrollingParent的onNestedScroll()后View的x、y坐标,计算得出偏移量并赋值进offsetInWindow数组
mView.getLocationInWindow(offsetInWindow);
offsetInWindow[0] -= startX;
offsetInWindow[1] -= startY;
}
//返回true代表NestedScrollingChild的dispatchNestedScroll事件成功分发NestedScrollingParent
return true;
} else if (offsetInWindow != null) {
// No motion, no dispatch. Keep offsetInWindow up to date.
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}
复制代码
这个方法与上面的dispatchNestedPreScroll()方法十分相似,这里就不细说了。
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
//经过ViewParentCompat调用NestedScrollingParent的onNestedPreFling()方法,返回值表示NestedScrollingParent是否消费所有惯性滑动
return ViewParentCompat.onNestedPreFling(mNestedScrollingParent, mView, velocityX,
velocityY);
}
return false;
}
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
//经过ViewParentCompat调用NestedScrollingParent的onNestedFling()方法,返回值表示NestedScrollingParent是否消费所有惯性滑动
return ViewParentCompat.onNestedFling(mNestedScrollingParent, mView, velocityX,
velocityY, consumed);
}
return false;
}
复制代码
这两方法都是经过ViewParentCompat调用NestedScrollingParent对应的fling方法来返回NestedScrollingParent是否消费所有惯性滑动。
public class NestedScrollingParentHelper {
private final ViewGroup mViewGroup;
private int mNestedScrollAxes;
public NestedScrollingParentHelper(ViewGroup viewGroup) {
mViewGroup = viewGroup;
}
public void onNestedScrollAccepted(View child, View target, int axes) {
mNestedScrollAxes = axes;
}
public int getNestedScrollAxes() {
return mNestedScrollAxes;
}
public void onStopNestedScroll(View target) {
mNestedScrollAxes = 0;
}
}
复制代码
NestedScrollingParentHelper只提供对应NestedScrollingParent相关的onNestedScrollAccepted()和onStopNestedScroll()方法,主要维护mNestedScrollAxes管理滑动的方向字段。
在使用以前NestedScrolling机制的 系统控件 嵌套滑动,当内部View快速滑动产生惯性滑动到边缘就中止,而不将惯性滑动传递给外部View继续消费惯性滑动,就会出现下图两个NestedScrollView嵌套滑动这种 惯性滑动不连续 的状况:
这里以com.android.support:appcompat-v7:22.1.0的NestedScrollView源码做为分析问题例子:
@Override
public boolean onTouchEvent(MotionEvent ev) {
...
switch (actionMasked) {
...
case MotionEvent.ACTION_UP:
if (mIsBeingDragged) {
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int initialVelocity = (int) VelocityTrackerCompat.getYVelocity(velocityTracker,
mActivePointerId);
if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
//分发惯性滑动
flingWithNestedDispatch(-initialVelocity);
}
mActivePointerId = INVALID_POINTER;
endDrag();
}
break;
}
...
}
private void flingWithNestedDispatch(int velocityY) {
final int scrollY = getScrollY();
final boolean canFling = (scrollY > 0 || velocityY > 0) &&
(scrollY < getScrollRange() || velocityY < 0);
if (!dispatchNestedPreFling(0, velocityY)) {//将惯性滑动分发给NestedScrollingParent,让它先对惯性滑动进行处理
dispatchNestedFling(0, velocityY, canFling);//若惯性滑动没被消费,再次将惯性滑动分发给NestedScrollingParent,并带上自身是否能消费fling的canFling参数让NestedScrollingParent根据状况处理决定canFling是true仍是false
if (canFling) {
//执行fling()消费惯性滑动
fling(velocityY);
}
}
}
public void fling(int velocityY) {
if (getChildCount() > 0) {
int height = getHeight() - getPaddingBottom() - getPaddingTop();
int bottom = getChildAt(0).getHeight();
//初始化fling的参数
mScroller.fling(getScrollX(), getScrollY(), 0, velocityY, 0, 0, 0,
Math.max(0, bottom - height), 0, height/2);
//重绘会触发computeScroll()进行滚动
ViewCompat.postInvalidateOnAnimation(this);
}
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
int oldX = getScrollX();
int oldY = getScrollY();
int x = mScroller.getCurrX();
int y = mScroller.getCurrY();
if (oldX != x || oldY != y) {
final int range = getScrollRange();
final int overscrollMode = ViewCompat.getOverScrollMode(this);
final boolean canOverscroll = overscrollMode == ViewCompat.OVER_SCROLL_ALWAYS ||
(overscrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
overScrollByCompat(x - oldX, y - oldY, oldX, oldY, 0, range,
0, 0, false);
if (canOverscroll) {
ensureGlows();
if (y <= 0 && oldY > 0) {
mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity());
} else if (y >= range && oldY < range) {
mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity());
}
}
}
}
}
复制代码
上面代码执行以下:
1.当快速滑动并抬起手指时onTouchEvent()方法会命中MotionEvent.ACTION_UP,执行关键flingWithNestedDispatch()方法将垂直方向的惯性滑动值分发。
2.flingWithNestedDispatch()方法先调用dispatchNestedPreFling()将惯性滑动分发给NestedScrollingParent,若NestedScrollingParent没有消费则调用dispatchNestedFling()并带上自身是否能消费fling的canFling参数让NestedScrollingParent能够根据状况处理决定canFling是true仍是false,若canFling值为true,执行fling()方法。
3.fling()方法执行mScroller.fling()初始化fling参数,而后 调用ViewCompat.postInvalidateOnAnimation()重绘触发computeScroll()方法进行滚动。
4.computeScroll()方法里面只让自身进行fling,并无在自身fling到边缘时将惯性滑动分发给NestedScrollingParent。
在Revision 26.1.0的android.support.v4兼容包添加了NestedScrollingChild二、NestedScrollingParent2两个接口:
public interface NestedScrollingChild2 extends NestedScrollingChild {
boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type);
void stopNestedScroll(@NestedScrollType int type);
boolean hasNestedScrollingParent(@NestedScrollType int type);
boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, @NestedScrollType int type);
boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow, @NestedScrollType int type);
}
public interface NestedScrollingParent2 extends NestedScrollingParent {
boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes, @NestedScrollType int type);
void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes, @NestedScrollType int type);
void onStopNestedScroll(@NonNull View target, @NestedScrollType int type);
void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @NestedScrollType int type);
void onNestedPreScroll(@NonNull View target, int dx, int dy, @Nullable int[] consumed, @NestedScrollType int type);
}
复制代码
它们分别继承NestedScrollingChild、NestedScrollingParent,都为滑动相关的方法添加了int类型参数type,这个参数有两个值:TYPE_TOUCH值为0表示滑动由用户手势滑动屏幕触发;TYPE_NON_TOUCH值为1表示滑动不是由用户手势滑动屏幕触发;同时View、ViewGroup、NestedScrollingChildHelper、NestedScrollingParentHelper一样根据参数type作了调整。
前面说到由于系统控件在computeScroll()方法里面只让自身进行fling,并无在自身fling到边缘时将惯性滑动分发给NestedScrollingParent致使惯性滑动不连贯,因此这里以com.android.support:appcompat-v7:26.1.0的NestedScrollView源码看看如何使用改进后的NestedScrolling机制:
public void fling(int velocityY) {
if (getChildCount() > 0) {
//发起滑动嵌套,注意ViewCompat.TYPE_NON_TOUCH参数表示不是由用户手势滑动屏幕触发
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL,ViewCompat.TYPE_NON_TOUCH);
mScroller.fling(getScrollX(), getScrollY(),
0, velocityY, 0, 0,Integer.MIN_VALUE, Integer.MAX_VALUE,0, 0);
mLastScrollerY = getScrollY();
ViewCompat.postInvalidateOnAnimation(this);
}
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
final int x = mScroller.getCurrX();
final int y = mScroller.getCurrY();
int dy = y - mLastScrollerY;
// Dispatch up to parent(将滑动值分发给NestedScrollingParent2)
if (dispatchNestedPreScroll(0, dy, mScrollConsumed, null,ViewCompat.TYPE_NON_TOUCH)) {
//计算NestedScrollingParent2消费后剩余的滑动值
dy -= mScrollConsumed[1];
}
if (dy != 0) {//若滑动值没有NestedScrollingParent2所有消费掉,则自身进行消费滚动
final int range = getScrollRange();
final int oldScrollY = getScrollY();
overScrollByCompat(0, dy, getScrollX(), oldScrollY, 0, range, 0, 0, false);
final int scrolledDeltaY = getScrollY() - oldScrollY;
final int unconsumedY = dy - scrolledDeltaY;
if (!dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, null,ViewCompat.TYPE_NON_TOUCH)) {//若滚动值没有分发成功给NestedScrollingParent2,则本身用EdgeEffect消费
final int mode = getOverScrollMode();
final boolean canOverscroll = mode == OVER_SCROLL_ALWAYS
|| (mode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
if (canOverscroll) {
ensureGlows();
if (y <= 0 && oldScrollY > 0) {
mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity());
} else if (y >= range && oldScrollY < range) {
mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity());
}
}
}
}
// Finally update the scroll positions and post an invalidation
mLastScrollerY = y;
ViewCompat.postInvalidateOnAnimation(this);
} else {
// We can't scroll any more, so stop any indirect scrolling
if (hasNestedScrollingParent(ViewCompat.TYPE_NON_TOUCH)) {
stopNestedScroll(ViewCompat.TYPE_NON_TOUCH);
}
// and reset the scroller y
mLastScrollerY = 0;
}
}
复制代码
代码分析以下:
1.与以前的NestedScrollView相比,fling()方法里面用到了NestedScrollingChild2的startNestedScroll方法发起滑动嵌套。
2.computeScroll()方法首先调用dispatchNestedPreScroll()将滑动值分发给NestedScrollingParent2,若滑动值没有被NestedScrollingParent2所有消费掉,则自身进行消费滚动,而后再调用dispatchNestedScroll()将自身消费、剩余的滑动值分发给NestedScrollingParent2,若分发失败则用EdgeEffect(这个用来滑动到顶部或者底部时会出现一个波浪形的边缘效果)消费掉,当mScroller滚动完成后调用stopNestedScroll()方法结束嵌套滑动。
在使用以前NestedScrolling机制的 系统控件 嵌套滑动,当子、父View都在顶部时,首先快速下滑子View并抬起手指制造惯性滑动,而后立刻滑动父View,这时就会出现上图的两个NestedScrollView嵌套滑动现象,你手指往上滑视图内容往下滚一段距离,视图内容马上就会自动往上回滚。
这里仍是以com.android.support:appcompat-v7:26.1.0的NestedScrollView源码做为分析问题例子:
private void flingWithNestedDispatch(int velocityY) {
final int scrollY = getScrollY();
final boolean canFling = (scrollY > 0 || velocityY > 0)
&& (scrollY < getScrollRange() || velocityY < 0);
if (!dispatchNestedPreFling(0, velocityY)) {
dispatchNestedFling(0, velocityY, canFling);
fling(velocityY);
}
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (actionMasked) {
...
case MotionEvent.ACTION_DOWN: {
...
//中止mScroller滚动
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
}
...
}
...
}
复制代码
代码执行以下:
1.这里分析场景是两个NestedScrollView嵌套滑动,因此dispatchNestedPreFling()返回值为false,子View执行就会fling()方法,前面分析过fling()方法调用mScroller.fling()触发computeScroll()进行实际的滚动。
2.在子View调用computeScroll()方法期间,若是此时子View不命中MotionEvent.ACTION_DOWN,mScroller是不会中止滚动,只能等待它完成,因而就子View就不停调用dispatchNestedPreScroll()和dispatchNestedScroll()分发滑动值给父View,就出现了上图的场景。
在androidx.core 1.1.0-alpha01开始引入NestedScrollingChild三、NestedScrollingParent3,它们在androidx.core:core:1.1.0正式被添加:
public interface NestedScrollingChild3 extends NestedScrollingChild2 {
void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, @ViewCompat.NestedScrollType int type, @NonNull int[] consumed);
}
public interface NestedScrollingParent3 extends NestedScrollingParent2 {
void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @ViewCompat.NestedScrollType int type, @NonNull int[] consumed);
}
复制代码
NestedScrollingChild3继承NestedScrollingChild2重载dispatchNestedScroll()方法,从返回值类型boolean改成void类型,添加了一个int数组consumed参数做为输出参数记录NestedScrollingParent3消费的滑动值,同理,NestedScrollingParent3继承NestedScrollingParent2重载onNestedScroll添加了一个int数组consumed参数来对应NestedScrollingChild3,NestedScrollingChildHepler、NestedScrollingParentHelper一样根据变化作了适配调整。
下面是androidx.appcompat:appcompat:1.1.0的NestedScrollView源码看看如何使用改进后的NestedScrolling机制:
@Override
public void computeScroll() {
if (mScroller.isFinished()) {
return;
}
mScroller.computeScrollOffset();
final int y = mScroller.getCurrY();
int unconsumed = y - mLastScrollerY;
mLastScrollerY = y;
// Nested Scrolling Pre Pass(分发滑动值给NestedScrollingParent3)
mScrollConsumed[1] = 0;
dispatchNestedPreScroll(0, unconsumed, mScrollConsumed, null,
ViewCompat.TYPE_NON_TOUCH);
//计算剩余的滑动值
unconsumed -= mScrollConsumed[1];
final int range = getScrollRange();
if (unconsumed != 0) {
// Internal Scroll(自身滚动消费滑动值)
final int oldScrollY = getScrollY();
overScrollByCompat(0, unconsumed, getScrollX(), oldScrollY, 0, range, 0, 0, false);
final int scrolledByMe = getScrollY() - oldScrollY;
//计算剩余的滑动值
unconsumed -= scrolledByMe;
// Nested Scrolling Post Pass(分发滑动值给NestedScrollingParent3)
mScrollConsumed[1] = 0;
dispatchNestedScroll(0, scrolledByMe, 0, unconsumed, mScrollOffset,
ViewCompat.TYPE_NON_TOUCH, mScrollConsumed);
//计算剩余的滑动值
unconsumed -= mScrollConsumed[1];
}
if (unconsumed != 0) {
//EdgeEffect消费剩余滑动值
final int mode = getOverScrollMode();
final boolean canOverscroll = mode == OVER_SCROLL_ALWAYS
|| (mode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
if (canOverscroll) {
ensureGlows();
if (unconsumed < 0) {
if (mEdgeGlowTop.isFinished()) {
mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity());
}
} else {
if (mEdgeGlowBottom.isFinished()) {
mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity());
}
}
}
//中止mScroller滚动动画并结束滑动嵌套
abortAnimatedScroll();
}
if (!mScroller.isFinished()) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
private void abortAnimatedScroll() {
mScroller.abortAnimation();
stopNestedScroll(ViewCompat.TYPE_NON_TOUCH);
}
复制代码
代码分析以下:
1.首先调用dispatchNestedPreScroll()将滑动值分发给NestedScrollingParent3并附带mScrollConsumed数组做为输出参数记录其具体消费多少滑动值,变量unconsumed表示剩余的滑动值,在调用dispatchNestedPreScroll()后,unconsumed减去以前的mScrollConsumed数组的元素从新赋值;
2.此时unconsumed值不为0,说明NestedScrollingParent3没有消费掉所有滑动值,则自身掉用overScrollByCompat()进行滚动消费滑动值,unconsumed减去记录本次消费的滑动值scrolledByMe从新赋值;而后调用dispatchNestedScroll()相似于【1】将滑动值分发给NestedScrollingParent3的操做而后计算unconsumed;
3.若unconsumed值还不为0,说明滑动值没有彻底消费掉,此时实现NestedScrollingParent三、NestedScrollingChild3对应的父View、子View在同一方向都滑动到了边缘尽头,此时自身用EdgeEffect消费剩余滑动值并调用abortAnimatedScroll()来 中止mScroller滚动并结束嵌套滑动;
若是你最低支持android版本是5.0及其以上,你可使用View、ViewGroup自己对应的NestedScrollingChild、NestedScrollingParent接口;若是你使用AndroidX那么你就须要使用NestedScrollingChild三、NestedScrollingParent3;若是你兼容Android5.0以前版本请使用NestedScrollingChild二、NestedScrollingParent2。下面的例子是伪代码,由于下面的自定义View没有实现相似Scroller的方式来消费滑动值,所以它运行也不能实现嵌套滑动进行滑动,只是提供给你们处理触摸事件调用NestedScrolling机制的思路。
若是要兼容NestedScrollingParent则覆写其接口便可,能够借助NestedScrollingParentHelper结合需求做方法代理,你能够根据具体业务在onStartNestedScroll()选择在嵌套滑动的方向、在onNestedPreScroll()要不要消费NestedScrollingChild2的滑动值等等。
若是要兼容NestedScrollingChild则覆写其接口便可,能够借助NestedScrollingChildHelper结合需求做方法代理。
public class NSChildView extends FrameLayout implements NestedScrollingChild2 {
private int mLastMotionY;
private final int[] mScrollOffset = new int[2];
private final int[] mScrollConsumed = new int[2];
...
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN: {
//关闭外层触摸事件拦截,确保能拿到MotionEvent.ACTION_MOVE
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
mLastMotionY = (int) ev.getY();
//开始嵌套滑动
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
break;
}
case MotionEvent.ACTION_MOVE:
final int y = (int) ev.getY();
int deltaY = mLastMotionY - y;
//开始滑动以前,分发滑动值给NestedScrollingParent2
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset,
ViewCompat.TYPE_TOUCH)) {
deltaY -= mScrollConsumed[1];
}
//模拟Scroller消费剩余滑动值
final int oldY = getScrollY();
scrollBy(0,deltaY);
//计算自身消费的滑动值,汇报给NestedScrollingParent2
final int scrolledDeltaY = getScrollY() - oldY;
final int unconsumedY = deltaY - scrolledDeltaY;
if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset,
ViewCompat.TYPE_TOUCH)) {
mLastMotionY -= mScrollOffset[1];
}else {
//能够选择EdgeEffectCompat消费剩余的滑动值
}
break;
case MotionEvent.ACTION_UP:
//能够用VelocityTracker计算velocityY
int velocityY=0;
//根据需求判断是否能Fling
boolean canFling=true;
if (!dispatchNestedPreFling(0, velocityY)) {
dispatchNestedFling(0, velocityY, canFling);
//模拟执行惯性滑动,若是你但愿惯性滑动也能传递给NestedScrollingParent2,对于每次消费滑动距离,
// 与MOVE事件中处理滑动同样,按照dispatchNestedPreScroll() -> 本身消费 -> dispatchNestedScroll() -> 本身消费的顺序进行消费滑动值
fling(velocityY);
}
//中止嵌套滑动
stopNestedScroll(ViewCompat.TYPE_TOUCH);
break;
case MotionEvent.ACTION_CANCEL:
//中止嵌套滑动
stopNestedScroll(ViewCompat.TYPE_TOUCH);
break;
}
return true;
}
复制代码
这种状况一般是ViewGroup支持布局嵌套如:
<android.support.v4.widget.NestedScrollView android:tag="我是爷爷">
<android.support.v4.widget.NestedScrollView android:tag="我是爸爸">
<android.support.v4.widget.NestedScrollView android:tag="我是儿子">
</android.support.v4.widget.NestedScrollView >
</android.support.v4.widget.NestedScrollView >
</android.support.v4.widget.NestedScrollView >
复制代码
举个例子:当儿子NestedScrollView调用stopNestedScroll()中止嵌套滑动时,就会回调爸爸NestedScrollView的onStopNestedScroll(),这时爸爸NestedScrollView也该中止嵌套滑动而且爷爷NestedScrollView也应该收到爸爸NestedScrollView的中止嵌套滑动,故在NestedScrollingParent2的onStopNestedScroll()应该这么写达到嵌套滑动事件往外分发的效果:
//NestedScrollingParent2
@Override
public void onStopNestedScroll(@NonNull View target, int type) {
mParentHelper.onStopNestedScroll(target, type);
//往外分发
stopNestedScroll(type);
}
//NestedScrollingChild2
@Override
public void stopNestedScroll(int type) {
mChildHelper.stopNestedScroll(type);
}
复制代码
除了下面的饿了么商家详情页外其余的效果能够用 CoordinatorLayout+AppBarLayout+CollapsingToolbarLayout 实现折叠悬停效果,其实它们底层Behavior也是基于NestedScrolling机制来实现的,而像饿了么这样的效果若是使用自定View的话要么用NestedScrolling机制来实现,要能基于传统的触摸事件分发实现。
本文偏向概念性内容,不免有些枯燥,但若遇到稍微有点挑战要解决的问题,没有现成的工具能够利用,只能靠本身思考和分析或者借鉴其余现成的工具的原理,就离不开这些看不起眼的“细节知识”;因为本人水平有限仅给各位提供参考,但愿可以抛砖引玉,若是有什么能够讨论的问题能够在评论区留言或联系本人,下篇将带你们实战基于NestedScrolling机制自定义View实现饿了么商家详情页效果。