概述
NestedScrolling是Android 5.0之后为我们提供的新特性,降低了使用传统事件分发机制处理嵌套滑动的难度,用于给子view与父view提供更好的交互。
嵌套滚动,顾名思义,就是有至少两个可以滚动的view或者viewgroup,也就说父view和子view都可以滚动,而子view要滚动的时候就要通知父view,我要开始滚动了,由父view决定是否要帮助子view滚动一段距离,父view帮助子view滚动了多少,子view就少移动多少距离。
传统的嵌套事件,我们滑动子view的内容区域,而移动却是外部的ViewGroup,所以传统的方式肯定是外部的ViewGroup拦截了内部的Child的事件,但是在Parent滑动到一定程度时,Chlid又开始滑动,中间的过程没有间断。从正常的事件分发(不手动分发)是不能完成的,因为Parent拦截后,就没有办法再把事件交给Child的(拦截的是一个事件序列)
主要类
需要知道几个关键的接口或类,如下:
NestedScrollingChild
:支持滚动的子View需要实现一套接口。
NestedScrollingChildHelper
:将子View的滑动事件转发到相应的父View,让父View来处理事件。
NestedScrollingParent
:包括滚动子View的父View需要实现的接口。这玩意就是我们上面说的父View必须具有的特性,也就是说父View必须要实现这个接口,稍后的源代码中会看到解释的。
NestedScrollingParentHelper
:父View中会使用的辅助类。此类只有3个方法,基本没干啥事。
1、NestedScrollingChild
public interface NestedScrollingChild {
/**
* 设置嵌套滑动是否能用
*
* @param enabled
*/
void setNestedScrollingEnabled(boolean enabled);
/**
* 判断嵌套滑动是否可用
*
* @return
*/
boolean isNestedScrollingEnabled();
/**
* 开始嵌套滑动
*
* @param axes 表示方向轴,有横向和竖向
* @return
*/
boolean startNestedScroll(int axes);
/**
* 停止嵌套滑动
*/
void stopNestedScroll();
/**
* 判断是否有父View 支持嵌套滑动
*
* @return
*/
boolean hasNestedScrollingParent();
/**
* 子view处理scroll后调用
*
* @param dxConsumed x轴上被消费的距离(横向)
* @param dyConsumed y轴上被消费的距离(竖向)
* @param dxUnconsumed x轴上未被消费的距离
* @param dyUnconsumed y轴上未被消费的距离
* @param offsetInWindow 子View的窗体偏移量
* @return true if the event was dispatched, false if it could not be dispatched.
*/
boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow);
/**
* 在子View的onInterceptTouchEvent或者onTouch中,调用该方法通知父View滑动的距离
*
* @param dx x轴上滑动的距离
* @param dy y轴上滑动的距离
* @param consumed 父view消费掉的scroll长度
* @param offsetInWindow 子View的窗体偏移量
* @return 支持的嵌套的父View 是否处理了 滑动事件
*/
boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
@Nullable int[] offsetInWindow);
/**
* 滑行时调用
*
* @param velocityX x 轴上的滑动速率
* @param velocityY y 轴上的滑动速率
* @param consumed 是否被消费
* @return true if the nested scrolling parent consumed or otherwise reacted to the fling
*/
boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
/**
* 进行滑行前调用
*
* @param velocityX x 轴上的滑动速率
* @param velocityY y 轴上的滑动速率
* @return true if a nested scrolling parent consumed the fling
*/
boolean dispatchNestedPreFling(float velocityX, float velocityY);
}
2、NestedScrollingParent
public interface NestedScrollingParent {
/**
* 当 Scrolling Child 调用 onStartNestedScroll 方法的时候,通过 NestedScrollingChildHelper
* 会回调 Scrolling parent 的 onStartNestedScroll 方法,如果返回 true,
* Scrolling parent 的 onNestedScrollAccepted(View child, View target, int nestedScrollAxes)
* 方法会被回调。
*
* @param child 包含此目标的ViewParent的直接view
* @param target 初始化嵌套滚动的view
* @param nestedScrollAxes 滚动方向:ViewCompat#SCROLL_AXIS_HORIZONTAL,ViewCompat#SCROLL_AXIS_VERTICAL
* @return
*/
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
/**
* 如果 Scrolling Parent 的onStartNestedScroll 返回 true,
* Scrolling parent 的 onNestedScrollAccepted(View child, View target,
* int nestedScrollAxes) 方法会被回调。
* @param child
* @param target
* @param nestedScrollAxes
*/
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
/**
* 嵌套滚动结束后调用
* @param target
*/
public void onStopNestedScroll(View target);
/**
* 正在嵌套滚动
* 要接收对此方法的调用,ViewParent必须先前在调用onStartNestedScroll时返回true
* @param target 嵌套滚动的child view
* @param dxConsumed 目标已消耗的水平滚动距离
* @param dyConsumed 目标已消耗的垂直滚动距离
* @param dxUnconsumed 目标未消耗的水平滚动距离
* @param dyUnconsumed 目标未消耗的垂直滚动距离
*/
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed);
/**
* 当 Scrolling Child 调用 dispatchNestedPreScroll 方法的时候调用此方法
* @param target Scrolling Child
* @param dx 水平滚动距离(像素)
* @param dy 垂直滚动距离(像素)
* @param consumed Output. The horizontal and vertical scroll distance consumed by this parent
*/
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();
}
NestedScrollingChild
和NestedScrollingParent
分别定义了嵌套子View和嵌套父View需要实现的接口。另外这些方法基本都是通过NestedScrollingChildHelper
和NestedScrollingParentHelper
来实现,一般并不需要手动编写多少逻辑。
通过方法名可以看出,NestedScrollingChild
的方法均为主动方法,而NestedScrollingParent
的方法基本都是回调方法。这也是NestedScrolling机制的一个体现,子View作为NestedScrolling事件传递的主动方,父View作为被动方。
NestedScrolling机制生效的前提条件是子View作为Touch事件的消费者,在消费过程中向父View发送NestedScrolling事件(注意这里不是Touch事件,而是NestedScrolling事件。
NestedScrolling事件传递
NestedScrolling机制中,NestedScrolling事件使用dx, dy表示,分别表示子View Touch事件处理方法中判定的x和y方向上的滚动偏移量。
NestedScrolling事件的传递:
- 由子View产生NestedScrolling事件;
- 发送给父View进行处理,父View处理之后,返回消费的偏移量;
- 子View根据父View消费的偏移量计算NestedScrolling事件剩余偏移量;
- 根据剩余偏移量判断是否能处理滚动事件;如果处理滚动事件,同时将自身滚动情况通知父View;
- 处理结束,事件传递完成。
这里只说明了一层嵌套的情况,事实上NestedScrolling很可能出现在多重嵌套的场景。对于多重嵌套,步骤2、3、4将事件自底向上进行传递,步骤2中消费的偏移量将记录所有嵌套父View消费偏移量的总和。这里不再重复。
下面以RecyclerView
为例,看一下NestedScrolling事件的传递过程
1、初始阶段
确认开启NestedScrolling,关联父View和子View。
//NestedScrollingChild
void setNestedScrollingEnabled(boolean enabled);
boolean startNestedScroll(int axes);
//NestedScrollingParent
boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
RecyclerView
实现了NestedScrollingChild
,那它就是事件的源头。
直接去看RecyclerView
的onTouchEvent
方法:
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;
}
startNestedScroll(nestedScrollAxis);
} break;
在MotionEvent.ACTION_DOWN中,获取到RecyclerView滚动的方向。记录初始位置。然后调用startNestedScroll(nestedScrollAxis);代码如下:
public boolean startNestedScroll(int axes) {
return getScrollingChildHelper().startNestedScroll(axes);
}
getScrollingChildHelper()返回的就是NestedScrollingChildHelper。NestedScrollingChildHelper的startNestedScroll方法是真正将事件传递到父View的地方
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;
}
在NestedScrollingChildHelper的startNestedScroll方法中会去递归的寻找有特征的父View,此处调用了ViewParentCompat.onStartNestedScroll(ViewParent parent, View child, View target,int nestedScrollAxes)方法。若找到父View,则将父View记录到变量mNestedScrollingParent 中,在接下来的事件中直接使用。如果有找到父View,并且父View的onStartNestedScroll方法返回true(代码父View接受滑动事件,比如父View只接受垂直滑动事件,就可以根据坐标轴进行方向判断是否是垂直方向,并返回true),还会调用ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes)方法。
2、预滚动阶段
子View将事件分发到父View
// NestedScrollingChild
boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
// NestedScrollingParent
void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
接下来看看RecyclerView
中onTouchEvent
的ACTION_MOVE
事件的实现,代码如下:
case MotionEvent.ACTION_MOVE: {
final int index = e.findPointerIndex(mScrollPointerId);
final int x = (int) (e.getX(index) + 0.5f);
final int y = (int) (e.getY(index) + 0.5f);
int dx = mLastTouchX - x;
int dy = mLastTouchY - y;
if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
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];
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;
首先计算出当前滑动的距离dx和dy。然后调用dispatchNestedPreScroll方法。dispatchNestedPreScroll方法的实现最终也是调用NestedScrollingChildHelper的dispatchNestedPreScroll方法。代码如下:
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
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) {
if (mTempNestedScrollConsumed == null) {
mTempNestedScrollConsumed = new int[2];
}
consumed = mTempNestedScrollConsumed;
}
consumed[0] = 0;
consumed[1] = 0;
ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed);
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;
}
通过ViewParentCompat.onNestedPreScroll方法,并调用父View的onNestedPreScroll方法。主要就是想知道父View是否消费了某个方向的距离。如果父View有消费某个方向上的距离,整个方法就返回true
3、滚动阶段
子View处理滚动事件。
在RecyclerView
的ACTION_MOVE
中还有一段代码很重要,那就是当子View当前处于拖拽状态时(mScrollState == SCROLL_STATE_DRAGGING)
会执行的方法,那就是scrollByInternal
方法。该方法的代码如下:
boolean scrollByInternal(int x, int y, MotionEvent ev) {
int unconsumedX = 0, unconsumedY = 0;
int consumedX = 0, consumedY = 0;
consumePendingUpdateOperations();
if (mAdapter != null) {
eatRequestLayout();
onEnterLayoutOrScroll();
TraceCompat.beginSection(TRACE_SCROLL_TAG);
if (x != 0) {
consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
unconsumedX = x - consumedX;
}
if (y != 0) {
consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
unconsumedY = y - consumedY;
}
TraceCompat.endSection();
repositionShadowingViews();
onExitLayoutOrScroll();
resumeRequestLayout(false);
}
if (!mItemDecorations.isEmpty()) {
invalidate();
}
if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset)) {
// Update the last touch co-ords, taking any scroll offset into account
mLastTouchX -= mScrollOffset[0];
mLastTouchY -= mScrollOffset[1];
if (ev != null) {
ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
}
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
} else if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
if (ev != null) {
pullGlows(ev.getX(), unconsumedX, ev.getY(), unconsumedY);
}
considerReleasingGlowsOnScroll(x, y);
}
if (consumedX != 0 || consumedY != 0) {
dispatchOnScrolled(consumedX, consumedY);
}
if (!awakenScrollBars()) {
invalidate();
}
return consumedX != 0 || consumedY != 0;
}
该方法内部,主要做了3件事:
- 让子View沿着水平或者垂直方向,将剩下的dx和dy滚动完。
- 计算出子View当前以及滚动的距离和未滚动的距离。
- 根据子View已经滚动的距离和未滚动的距离调用
dispatchNestedScroll
方法。当然这里和上面的dispatchNestedPreScroll
方法类似,最终也是会调用到父View的onNestedScroll
方法的。
4、结束阶段
// NestedScrollingChild
void stopNestedScroll();
// NestedScrollingParent
void onStopNestedScroll(View target);
最后再看一下RecyclerView
的ACTION_UP
事件:
case MotionEvent.ACTION_UP: {
mVelocityTracker.addMovement(vtev);
eventAddedToVelocityTracker = true;
mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
final float xvel = canScrollHorizontally ?
-VelocityTrackerCompat.getXVelocity(mVelocityTracker, mScrollPointerId) : 0;
final float yvel = canScrollVertically ?
-VelocityTrackerCompat.getYVelocity(mVelocityTracker, mScrollPointerId) : 0;
if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
setScrollState(SCROLL_STATE_IDLE);
}
resetTouch();
} break;
private void resetTouch() {
if (mVelocityTracker != null) {
mVelocityTracker.clear();
}
stopNestedScroll();
releaseGlows();
}
在ACTION_UP
中会调用resetTouch
方法。在此方法中,最重要的就是调用了stopNestedScroll()方法,该方法的目的就是通知父View滚动停止了。会调用父View的onStopNestedScroll()方法
当子View调用startNestedScroll方法时,开始嵌套滚动流程;之后不断循环pre-scroll和scroll两个过程(一般在子View的onTouchEvent的MOVE分支调用);直到手指抬起,子View调用stopNestedScroll方法结束滚动(在结束之前可能进入Fling状态)。
参考:
1、NestedScrolling嵌套滚动原理
2、NestedScrolling事件机制源码解析
3、NestedScrolling 机制深入解析
4、Android Nested Scrolling