简介
NestedScrolling,包含在android.support.v4包中,由 22.10 版本开始引入,支持 5.0 及 5.0 以上的系统。
NestedScrolling,简称嵌套滑动,可主要分为NestedScrollingParen和NestedScrollingChild两部分,使用它可以实现一些非常绚丽的效果。
Google 帮我们封装好了一些相应的空间,比如 RecyclerView 实现了 NestedScrollingChild 接口,CoordinatorLayout 实现了 NestedScrollingParent 接口,NestedScrollingView,SwipeRefreshLayout 实现了 NestedScrollingChild,NestedScrollingParent 接口等。
那么相比较于传统的事件分发机制,NetstedScroll 机制有什么特点呢?
在传统的事件分发机制 中,一旦某个 View 或者 ViewGroup 消费了事件,就很难将事件交给父 View 进行共同处理。而 NestedScrolling 机制很好地帮助我们解决了这一问题。我们只需要按照规范实现相应的接口即可,子 View 实现 NestedScrollingChild,父 View 实现 NestedScrollingParent ,通过 NestedScrollingChildHelper 或者 NestedScrollingParentHelper 完成交互。
NestedScrolling机制的原理
NestedScrolling 整体主要包含四个类:
- NestedScrollingParent
在嵌套滑动中,如果父View 想实现 嵌套滑动,要实现这个 NestedScrollingParent 借口,与 NestedScrollingChild 大概有一一对应的关系。
- NestedScrollingChild
在嵌套滑动中,如果scrolling child 想实现嵌套滑动,必须实现这个借口
- NestedScrollingChildHelper
实现 Child 和 Parent 交互的逻辑
- NestedScrollingParentHelper
实现 Child 和 Parent 交互的逻辑
它的处理流程大致如下:
img- scrolling child 在滑动之前,会通过 NestedScrollingChildHelper 查找是否有响应的 scrolling parent,如果有的话,会先询问scrolling parent 是否需要先于scrolling child 滑动,如果需要的话,scrolling parent 进行相应的滑动,并消费一定的距离;
- 接着scrolling child 进行相应的滑动,并消耗一定的距离值 dx,dy;
- scrolling child 滑动完之后,询问scrolling parent 是否还需要继续进行滑动,需要的话,进行相应的处理;
- 滑动结束之后,Scrolling child 会停止滑动,并通过 NestedScrollingChildHelper 通知相应的 Scrolling Parent 停止滑动。
NestedScrollingChild 相关方法
目前已实现改接口的类包括: HorizontalGridView, NestedScrollView, RecyclerView, SwipeRefreshLayout, VerticalGridView
- boolean startNestedScroll(int axes)
在开始滑动的时候会调用这个方法,axes 代表滑动的方向:ViewCompat.SCROLL_AXIS_HORIZONTAL 代表水平滑动,ViewCompat.SCROLL_AXIS_VERTICAL 代表垂直滑动。返回值是布尔类型的,根据返回值,我们可以判断是否找到支持嵌套滑动的父View ,返回 true,表示在scrolling parent (需要注意的是这里不一定是直接scrolling parent ,间接scrolling parent 也可会返回 true) 中找到支持嵌套滑动的。反之,则找不到。
- boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow)
在scrolling child 滑动之前,提供机会让scrolling parent 先于scrolling child滑动。
dx,dy 是输入参数,表示scrolling child 传递给 scrolling parent 水平方向,垂直方向上的偏移量,consumed 是输出参数,consumed[0] 表示父 View 在水平方向上消费的值,,consumed[1 表示父 View 在垂直方向上消费的值。
返回值也是布尔类型的,根据这个值 ,我们可以判断scrolling parent 是都消费了相应距离 。
- boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow)
在scrolling child 滑动之后,调用这个方法,提供机会给scrolling parent 滑动,dxConsumed,dyConsumed 是输入参数,表示scrolling child 在水平方向,垂直方向消耗的值,dxUnconsumed,dyUnconsumed 也是输入参数,表示scrolling child 在水平方向,垂直方向未消耗的值。
- boolean dispatchNestedPreFling(float velocityX, float velocityY, boolean consumed)
调用这个方法,在scrolling child 处理 fling 动作之前,提供机会scrolling parent 先于scrolling child 处理 fling 动作。
三个参数都是输入参数,velocityX 表示水平方向的速度,velocityY 表示垂直方向感的速度,consumed 表示scrolling child 是否消费 fling 动作 。返回值也是布尔类型的,表示scrolling parent 是否有消费了fling 动作或者对 fling 动作做出相应的 处理。true 表示有,false 表示没有。
- boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed)
在 Scrolling child 处理 fling 动作之后,提供机会给 Scrolling Parent 处理 fling 动作。各个参数的意义这里就不再意义阐述了,跟 dispatchNestedFling 参数的意义是一样的。
- void stopNestedScroll
当滑动取消或停止的时候,会调用这个方法。例如在 RecyclerView 中,当 ACTION_UP 或者 ACTION_CANCEL 或者 item 消费了 Touch 事件的时候,会调用这个方法。
NestedScrollingParent主要方法
目前已实现改接口的类包括: CoordinatorLayout, NestedScrollView, SwipeRefreshLayout。它通常是配合 NestedScrollingChild 进行嵌套滑动的。
- boolean onStartNestedScroll(View child, View target, int nestedScrollAxes)
在 Scrolling Child 开始滑动的时候会调用这个方法
当 Scrolling Child 调用 onStartNestedScroll 方法的时候,通过 NestedScrollingChildHelper 会回调 Scrolling parent 的 onStartNestedScroll 方法,如果返回 true, Scrolling parent 的 onNestedScrollAccepted(View child, View target, int nestedScrollAxes) 方法会被回调。
target 表示发起滑动事件的 View,Child 是 ViewParent 的直接子View,包含 target,nestedScrollAxes 表示滑动方向。
- void onNestedScrollAccepted(View child, View target, int nestedScrollAxes)
如果 Scrolling Parent 的onStartNestedScroll 返回 true, Scrolling parent 的 onNestedScrollAccepted(View child, View target, int nestedScrollAxes) 方法会被回调。
- boolean onNestedPreScroll(View target, int dx, int dy, int[] consumed)
在 Scrolling Child 进行滑动之前,Scrolling Parent 可以先于Scrolling Child 进行相应的处理
如果 Scrolling Child 调用 dispatchNestedPreFling(float velocityX, float velocityY) ,通过 NestedScrollingChildHelper 会回调 Scrolling parent 的 onNestedPreScroll 方法
接下来的几个方法,我们不一一介绍了。与 Scrolling Child 方法几乎是一一对应的。
NetsedScrollingChildHelper相关方法
RecyclerView实现了NestedScrollingChild接口,因此我们以RecyclerView为例,详细探究NetsedScrollingChildHelper的具体应用
public RecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
...
mScrollingChildHelper = new NestedScrollingChildHelper(this);
setNestedScrollingEnabled(true);
}
...
@Override
public void setNestedScrollingEnabled(boolean enabled) {
mScrollingChildHelper.setNestedScrollingEnabled(enabled);
}
@Override
public boolean isNestedScrollingEnabled() {
return mScrollingChildHelper.isNestedScrollingEnabled();
}
@Override
public boolean startNestedScroll(int axes) {
return mScrollingChildHelper.startNestedScroll(axes);
}
@Override
public void stopNestedScroll() {
mScrollingChildHelper.stopNestedScroll();
}
@Override
public boolean hasNestedScrollingParent() {
return mScrollingChildHelper.hasNestedScrollingParent();
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow) {
return mScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, offsetInWindow);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return mScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return mScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
}
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return mScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY);
}
...
从代码中可以看到,RecyclerView充当了一个代理的角色,它的很多逻辑其实是交给 NestedScrollingChildHelper 去帮助其完成的,下面我们一起来看一下 NestedScrollingChildHelper 里的方法
/**
* Start a new nested scroll for this view.
*
* <p>This is a delegate method. Call it from your {@link android.view.View View} subclass
* method/{@link NestedScrollingChild} interface method with the same signature to implement
* the standard policy.</p>
*
* @param axes Supported nested scroll axes.
* See {@link NestedScrollingChild#startNestedScroll(int)}.
* @return true if a cooperating parent view was found and nested scrolling started successfully
*/
public boolean startNestedScroll(int axes) {
if (hasNestedScrollingParent()) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
while (p != null) {
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
mNestedScrollingParent = p;
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
- 第一步,判断 P 是否为空,不为空, 从 P (初始值是RecyclerView 的直接父 View) 开始找起,判断其是否支持嵌套滑动,若支持,返回true;
- 第二步:若 P 不支持嵌套滑动,再将 p 指向 p.getParent(),循环第一步;
- 第三步:若循环了所有的 P ,都找不到支持嵌套滑动的 View,返回 false。
/**
* Dispatch one step of a nested scrolling operation to the current nested scrolling parent.
*
* <p>This is a delegate method. Call it from your {@link android.view.View View} subclass
* method/{@link NestedScrollingChild} interface method with the same signature to implement
* the standard policy.</p>
*
* @return true if the parent consumed any of the nested scroll
*/
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
int startX = 0;
int startY = 0;
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}
ViewParentCompat.onNestedScroll(mNestedScrollingParent, mView, dxConsumed,
dyConsumed, dxUnconsumed, dyUnconsumed);
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
offsetInWindow[0] -= startX;
offsetInWindow[1] -= startY;
}
return true;
} else if (offsetInWindow != null) {
// No motion, no dispatch. Keep offsetInWindow up to date.
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}
当childView已发生滑动时,首先获取childView在屏幕上的位置并记录X, Y坐标,因为上一步在startNestedScroll 方法中已完成对 mNestedScrollingParent的初始化,在这里调用 ViewParentCompat.onNestedScroll(mNestedScrollingParent, mView, dxConsumed,
dyConsumed, dxUnconsumed,dyUnconsumed),最后重新获取滑动后的childView在屏幕上的位置,并将childView左上角的X,Y轴坐标重新赋值为当前位置与初始位置之差;当childView未发生滑动时,直接将childView左上角的X,Y轴坐标赋值为0。
看完了上面的两个主要方法,我们可以得出这样的一个结论:当我们调用 Scrolling Child 的 onStartNested 方法的时候,会通过 ChildHelper 去寻找是否有相应的 Scrolling Parent,如果有的话,会 回调相应的方法。同理 dispatchNestedPreScroll,dispatchNestedScroll,dispatchNestedPreFling 同样如此。
public boolean onTouchEvent(MotionEvent e) {
...
// 如果 Item 处理了 Touch 事件,直接返回 true ,在在处理
if (dispatchOnItemTouch(e)) {
cancelTouch();
return true;
}
if (mLayout == null) {
return false;
}
...
switch (action) {
case MotionEvent.ACTION_DOWN: {
mScrollPointerId = e.getPointerId(0);
mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontally) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertically) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
// 在 Action_Down 的时候 调用 startNestedScroll
startNestedScroll(nestedScrollAxis);
} break;
case MotionEvent.ACTION_MOVE: {
...
// 在 Action_move 的时候,回调 dispatchNestedPreScroll 方法
if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
// 减去 Scrolling Parent 的消费的值
dx -= mScrollConsumed[0];
dy -= mScrollConsumed[1];
vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
// Updated the nested offsets
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
}
...
if (mScrollState == SCROLL_STATE_DRAGGING) {
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];
// 在 scrollByInternal 方法里面会回调 onNestedScroll 方法
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
vtev)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
if (mGapWorker != null && (dx != 0 || dy != 0)) {
mGapWorker.postFromTraversal(this, dx, dy);
}
}
}
break;
case MotionEvent.ACTION_UP: {
...
// 在 fling 方法里面会回调 onNestedPreFling dispatchNestedFling 等方法
if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
setScrollState(SCROLL_STATE_IDLE);
}
// 在resetTouch方法中调用 onStopScroll 方法
resetTouch();
} break;
case MotionEvent.ACTION_CANCEL: {
// 在 cancelTouch中通过调用 resetTouch 调用 onStopScroll 方法
cancelTouch();
} break;
}
if (!eventAddedToVelocityTracker) {
mVelocityTracker.addMovement(vtev);
}
vtev.recycle();
return true;
}
private void resetTouch() {
if (mVelocityTracker != null) {
mVelocityTracker.clear();
}
stopNestedScroll();
releaseGlows();
}
private void cancelTouch() {
resetTouch();
setScrollState(SCROLL_STATE_IDLE);
}
执行流程
- 在 ACTION_DOWN 时,Scrolling Child 会调用 startNestedScroll 方法,通过 childHelper 回调 Scrolling Parent 的 startNestedScroll 方法;
- 在 AACTION_MOVE 时,Scrolling Child 要开始滑动的时候,会调用dispatchNestedPreScroll 方法,通过 NestedScrollingChildHelper 询问 Scrolling Parent 是否要先于 Child 进行 滑动,若需要的话,会调用 Parent 的 onNestedPreScroll 方法,协同 ChildView 一起进行滑动;
- 当 Scrolling Child 滑动完成的时候,会调用 dispatchNestedScroll 方法,通过 ChildHelper 询问 Scrolling Parent 是否需要进行滑动,需要的话,会 调用 Scrolling Parent 的 onNestedScroll 方法
- 在 ACTION_DOWN,ACTION_MOVE 的时候,会调用 Scrolling Child 的stopNestedScroll ,通过 NestedScrollingChildHelper 调用 Scrolling parent 的 stopNestedScroll 方法。
- 如果需要处理 Fling 动作,我们可以通过 VelocityTrackerCompat 获得相应的速度,并在 ACTION_UP 的时候,调用 dispatchNestedPreFling 方法,通过 NestedScrollingChildHelper 询问 Parent 是否需要先于 Child 进行 Fling 动作
- 在 Child 处理完 Fling 动作时候,如果 Scrolling Parent 还需要处理 Fling 动作,我们可以调用 dispatchNestedFling 方法,通过 ChildHelper ,调用 Parent 的 onNestedFling 方法
子View | 父View | 方法描述 |
---|---|---|
startNestedScroll | onStartNestedScroll、onNestedScrollAccepted | Scrolling Child 开始滑动的时候,通知 Scrolling Parent 要开始滑动了,通常是在 Action_down 动作 的时候调用这个方法 |
dispatchNestedPreScroll | onNestedPreScroll | 在 Scrolling Child 要开始滑动的时候,询问 Scrolling Parent 是否先于 Scrolling Child 进行相应的处理,同时是在 Action_move 的时候调用 |
dispatchNestedScroll | onNestedScroll | 在 Scrolling Child 滑动后会询问 Scrolling Parent 是否需要继续滑动 |
dispatchNestedPreFling | onNestedPreFling | 在 Scrolling Child 开始处理 Fling 动作的时候,询问 Scrolling Parent 是否需要先处理 Fling 动作 |
dispatchNestedFling | onNestedFling | 在 Scrolling Child 处理 Fling 动作完毕的时候,询问 Scrolling Parent 是都还需要进行相应的处理 |
stopNestedScroll | onStopNestedScroll | 在 Scrolling Child 停止滑动的时候,会调用 Scrolling Parent 的这个方法。通常是在 Action_up 或者 Action_cancel 或者被别的 View 消费 Touch 事件的时候调用的 |
网友评论