今天要实现一个购物 app 首页的嵌套滑动效果,像京东、淘宝、闲鱼的首页都采用了类似的效果,如图:
其布局结构大致如下:
我们要实现的主要功能有:
- 嵌套滑动与惯性滑动时,嵌套滑动的父视图先滑动,然后子视图再滑
- 向上滑动时,TabLayout 会固定在屏幕顶部
实际上这种效果直接用 CoordinatorLayout 那一套就可以轻松实现,但是抱着了解嵌套滑动机制的目的,我们自己动手来实现一个简单的 Demo。
1、布局结构
详细的布局结构示意图:
解释一下部分布局:
- NestedScrollView:注意 NestedScrollView 与 ScrollView 的区别,ScrollView 是 FrameLayout 的子类,它不具备嵌套滑动的基础条件 —— 实现 NestedScrollingParent 或 NestedScrollingChild 接口族的接口之一;而同样是 FrameLayout 子类的 NestedScrollView 同时实现了 NestedScrollingParent3 与 NestedScrollingChild3 接口,既可以作为嵌套滑动中的“父亲”,也可以作为嵌套滑动中的“孩子”。因此这个位置用不了 ScrollView,而是要用 NestedScrollView
- HeaderView:理解为包含 Banner 在内的、自己内部不进行上下滑动的 View
- RecyclerView:RecyclerView 实际上是 ViewPager 每个 Fragment 的根布局,它实现了 NestedScrollingChild2、NestedScrollingChild3 接口,在嵌套滑动中扮演“孩子”的角色
相信你已经从上述解释中看出,嵌套滑动中有两个角色——“孩子”与“父亲”,分别表示嵌套滑动的子视图与父视图。子视图需实现 NestedScrollingChild、NestedScrollingChild2 或 NestedScrollingChild3 接口之一(后者继承前者),父视图需要实现 NestedScrollingParent、NestedScrollingParent2 或 NestedScrollingParent3 接口之一(后者继承前者),这是实现嵌套滑动的先决条件。
如果真的使用了 ScrollView,由于其没有实现 NestedScrollingParent 接口,不会对 RecyclerView 传递过来的嵌套滑动事件进行处理,会导致嵌套滑动完全由 RecyclerView 消费,无法将整个视图向上滑动:
而使用 NestedScrollView 能避免以上问题:
2、实现 TabLayout 顶置
接下来再想如何让 TabLayout 在滑动到顶部时被顶置,一种实现方案是,自定义一个 NestedScrollView 的子类 NestedScrollLayout,在测量时强行让 TabLayout 与 ViewPager 所在的 LinearLayout 的高度占满屏幕,这样当 TabLayout 滑动到屏幕顶部时,LinearLayout 完全展现出来,再向上滑动时,由于嵌套滑动的父视图 NestedScrollLayout 由于已经滑到底,因此它不再继续消费嵌套滑动事件,而是由 RecyclerView 消费,使得其向上滑动,从而造成 TabLayout “吸顶”的假象。
参考代码:
class NestedScrollLayout(context: Context, attrs: AttributeSet?, defStyleAttr: Int) :
NestedScrollView(context, attrs, defStyleAttr) {
private lateinit var mTabAndViewPagerLayout: LinearLayout
private lateinit var mHeaderView: View
constructor(context: Context) : this(context, null, 0)
constructor(context: Context, attributeSet: AttributeSet) : this(context, attributeSet, 0)
override fun onFinishInflate() {
super.onFinishInflate()
// 根据布局文件,目标 LinearLayout 是 NestedScrollLayout 的第 0 个孩子的第 1 个孩子
// 或者通过 findViewById() 通过 id 直接找这些 View 也可以
mTabAndViewPagerLayout = (getChildAt(0) as ViewGroup).getChildAt(1) as LinearLayout
mHeaderView = (getChildAt(0) as ViewGroup).getChildAt(0)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// 设置 TabLayout + ViewPager 所在的 LinearLayout 的高度为页面可显示区域高度
val layoutParams = mTabAndViewPagerLayout.layoutParams
layoutParams?.let {
// 由于布局中 SwipeRefreshLayout 和 NestedScrollLayout 的高度都是 match_parent,
// 所以 getMeasuredHeight() 拿到的就是整个 activity 去掉 ActionBar 后的高度
layoutParams.height = measuredHeight
mTabAndViewPagerLayout.layoutParams = layoutParams
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
}
效果图:
能看到,虽然吸顶效果实现了,但是由于滑动事件都被 RecyclerView 消费,使得只有在滑动 RecyclerView 以外的部分时,整个 NestedScrollLayout 才会向上滑动。发生问题的原因是,孩子作为嵌套滑动中主动的一方,将滑动事件传递给父亲,但是父亲并没有处理该嵌套滑动事件,而是将其继续再向上层分发。
3、嵌套滑动原理
这一节我们一边梳理嵌套滑动的过程,一边实现我们想要的功能。
上面我们提到,嵌套滑动的父亲没有处理孩子传来的嵌套滑动事件,导致滑动冲突。为了了解嵌套滑动的完整过程,我们先来看时序图:
在我们的例子中,NestedScrollLayout 就是上图的 NestedScrollingParent,而 RecyclerView 既是接收事件的 View 也是嵌套滑动的孩子 NestedScrollingChild。
从时序图中也不难看出,嵌套滑动实际上没有改变事件分发的流程,嵌套滑动的子视图在接收到触摸事件时,会在 ACTION_DOWN、ACTION_MOVE 和 ACTION_UP 中分别触发不同的嵌套滑动事件,并且都是优先交给嵌套滑动父视图处理。只有在父视图不处理或没有完全处理的情况下,子视图才进行处理。在这个过程中,时序图没有体现出的两个角色 —— NestedScrollingChildHelper 与 NestedScrollingParentHelper 分别提供了与子视图和父视图同名的方法来实现嵌套滑动的功能。
下面进入源码,结合源码来找出问题的解决方案。
源码版本:
androidx.recyclerview:recyclerview:1.1.0
androidx.core:core:1.7.0 (NestedScrollView)
3.1 初始化过程
首先,RecyclerView 在构造方法中要设置是否开启嵌套滑动:
// #1 RecyclerView 构造方法,通过 android.R.attr.nestedScrollingEnabled 属性设置嵌套滑动开启
public RecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
...
boolean nestedScrollingEnabled = true;
if (Build.VERSION.SDK_INT >= 21) {
a = context.obtainStyledAttributes(attrs, NESTED_SCROLLING_ATTRS,
defStyleAttr, 0);
if (Build.VERSION.SDK_INT >= 29) {
saveAttributeDataForStyleable(
context, NESTED_SCROLLING_ATTRS, attrs, a, defStyleAttr, 0);
}
nestedScrollingEnabled = a.getBoolean(0, true);
a.recycle();
}
setNestedScrollingEnabled(nestedScrollingEnabled);
}
setNestedScrollingEnabled() 是 NestedScrollingChild 的接口方法,它通过 NestedScrollingChildHelper 设置是否开启嵌套滑动的标记位:
#RecyclerView:
@Override
public void setNestedScrollingEnabled(boolean enabled) {
getScrollingChildHelper().setNestedScrollingEnabled(enabled);
}
#NestedScrollingChildHelper:
// #2 设置是否开启嵌套滑动
public void setNestedScrollingEnabled(boolean enabled) {
if (mIsNestedScrollingEnabled) {
ViewCompat.stopNestedScroll(mView);
}
mIsNestedScrollingEnabled = enabled;
}
3.2 ACTION_DOWN
然后,滑动过程开始,当嵌套滑动的子视图 RecyclerView 接收到 ACTION_DOWN 事件时:
@Override
public boolean onTouchEvent(MotionEvent e) {
...
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;
}
// 交由 NestedScrollingChild 处理,滑动类型为 TYPE_TOUCH,即触摸滚动
startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
} break;
}
}
RecyclerView 通过 startNestedScroll() 将滑动事件的起始事件 ACTION_DOWN 交给 NestedScrollingChild,实际上它是 NestedScrollingChild2 定义的接口方法之一:
/**
* #4 根据给定的 type 沿着 axes 轴开始一个可嵌套的滚动操作,需遵守以下协议:
* 视图在启动滚动操作时应调用 startNestedScroll()。对于触摸滚动类型,就是在初始的
* MotionEvent.ACTION_DOWN 事件中调用该方法。
* 触摸滚动将以与 ViewParent.requestDisallowInterceptTouchEvent() 相同的方式自动
* 终止;而程序化滚动必须显式调用 stopNestedScroll() 来指定嵌套滚动的结束。
*
* 如果 startNestedScroll() 返回 true,表示已经找到合作的父视图;否则,调用者可以忽略
* 接下来的协议,直到下一次滚动。在嵌套滚动正在进行时调用 startNestedScroll() 将返回 true
*
* 在滚动的每个增量步骤中,调用者应在计算出请求的滚动增量后调用 dispatchNestedPreScroll(),
* 如果该方法返回 true,则表示嵌套滚动的父视图已经部分消耗了该滚动,并且调用者应相应地调整滚动量。
* 在应用剩余的滚动增量后,调用者应调用 dispatchNestedScroll(),将已消耗和未消耗的滚动增量都传递
* 给该方法。嵌套滚动的父视图可能会以不同的方式处理这些值,具体参见 NestedScrollingParent2 的
* onNestedScroll()
*
* 返回值是 true 表示找到合作的父视图并已启用当前手势的嵌套滚动
*/
@Override
public boolean startNestedScroll(int axes, int type) {
return getScrollingChildHelper().startNestedScroll(axes, type);
}
startNestedScroll() 会寻找能响应本次嵌套滑动的父视图,并通过 onNestedScrollAccepted() 将嵌套滑动事件交由父视图处理:
public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
// 若已经有了嵌套滑动的父视图直接返回 true
if (hasNestedScrollingParent(type)) {
// Already in progress
return true;
}
// 如果开启了嵌套滑动,就递归寻找支持嵌套滑动的父视图,注意可能不是直接父视图
if (isNestedScrollingEnabled()) {
// mView 就是嵌套滑动的子视图,本例中就是 RecyclerView
ViewParent p = mView.getParent();
View child = mView;
while (p != null) {
// 寻找可以响应本次滑动的父视图
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
setNestedScrollingParentForType(type, p);
// 如果找到了,就将滑动交由父视图处理
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
ViewParentCompat 是 ViewParent 的兼容处理类,最终都会调用 NestedScrollingParent 的对应方法。先是通过 onStartNestedScroll() 寻找能接管嵌套滑动的父视图:
/**
* 响应子视图的嵌套滑动操作,条件满足时会接管嵌套滚动操作。
* 每个父视图都将有机会响应并声明嵌套滚动操作,通过返回 true 实现。
* ViewParent 的实现可以覆盖此方法,以指示视图何时愿意支持即将开始的嵌套滚动操作。
* 如果返回 true,则此 ViewParent 将成为目标视图正在进行中的滚动操作的嵌套滚动父级。
* 嵌套滚动完成时,此 ViewParent 将接收到 onStopNestedScroll(ViewParent, View, int)
* 的调用。
* child 是此 ViewParent 的直接子视图,包含目标视图;target 是启动嵌套滚动的视图
*/
public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
int nestedScrollAxes, int type) {
// 调用父亲接口的 onStartNestedScroll()
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;
}
不论怎样,都是要回调 NestedScrollingParent 的 onStartNestedScroll()。在本例中,只有 NestedScrollLayout 有可能作为嵌套滑动的父视图,由于其未重写该方法,因此会调用其父类 NestedScrollView 的:
// #5 NestedScrollView 只有在纵向滑动时才会接收嵌套滑动事件
@Override
public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes,
int type) {
return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
这样看,NestedScrollLayout 可以作为嵌套滑动的父视图。那么接下来,在 startNestedScroll() 中就会再执行 ViewParentCompat.onNestedScrollAccepted():
public static void onNestedScrollAccepted(ViewParent parent, View child, View target,
int nestedScrollAxes, int type) {
if (parent instanceof NestedScrollingParent2) {
// First try the NestedScrollingParent2 API
((NestedScrollingParent2) parent).onNestedScrollAccepted(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 {
parent.onNestedScrollAccepted(child, target, nestedScrollAxes);
} catch (AbstractMethodError e) {
Log.e(TAG, "ViewParent " + parent + " does not implement interface "
+ "method onNestedScrollAccepted", e);
}
} else if (parent instanceof NestedScrollingParent) {
((NestedScrollingParent) parent).onNestedScrollAccepted(child, target,
nestedScrollAxes);
}
}
}
可以看出是类似的处理方式,回调 NestedScrollingParent 的 onNestedScrollAccepted(),还是要看 NestedScrollView:
/**
* #6 响应嵌套滑动,在 onStartNestedScroll() 返回 true 之后调用,为视图及其超类提供了执
* 行嵌套滚动的初始配置的机会。如果父类有实现此方法,则此方法的实现应始终调用其父类的实现
*/
@Override
public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes,
int type) {
mParentHelper.onNestedScrollAccepted(child, target, axes, type);
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, type);
}
onNestedScrollAccepted() 做了两件事:
-
NestedScrollingParentHelper 在 onNestedScrollAccepted() 中初始化:
/** * 当由子视图初始化的嵌套滑动操作被此 ViewGroup 接收时调用此方法 */ public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes, @NestedScrollType int type) { if (type == ViewCompat.TYPE_NON_TOUCH) { mNestedScrollAxesNonTouch = axes; } else { mNestedScrollAxesTouch = axes; } }
-
startNestedScroll() 内,自己作为嵌套滑动子视图,将嵌套滑动继续向父视图传递:
NestedScrollView: @Override public boolean startNestedScroll(int axes, int type) { return mChildHelper.startNestedScroll(axes, type); }
可以看到 NestedScrollingView 虽然作为嵌套滑动父视图接收了嵌套滑动事件,但是它在 onNestedScrollAccepted() 内做完所需的处理后,又转身作为子视图,通过 startNestedScroll() 将该事件向更高级的嵌套滑动父视图传递,即重复时序图中 4 ~6 步的动作。
3.3 ACTION_MOVE
ACTION_DOWN 事件处理完,下一步看如何处理 ACTION_MOVE。
RecyclerView 处理 ACTION_MOVE:
@Override
public boolean onTouchEvent(MotionEvent e) {
...
switch (action) {
...
case MotionEvent.ACTION_MOVE: {
...
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;
// mScrollState 有三个可选值:SCROLL_STATE_IDLE 表示还未开始
// 滑动,SCROLL_STATE_DRAGGING 表示 RecyclerView 处于手指拖拽
// 状态,SCROLL_STATE_SETTLING 表示未受外力作用但还在向终点滑动
// 的状态,比如惯性滑动。这里是未开始滑动,为滑动做准备工作
if (mScrollState != SCROLL_STATE_DRAGGING) {
final int dx = x - mInitialTouchX;
final int dy = y - mInitialTouchY;
boolean startScroll = false;
if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
mLastTouchX = x;
startScroll = true;
}
if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
mLastTouchY = y;
startScroll = true;
}
if (startScroll) {
// 设置 mScrollState = SCROLL_STATE_DRAGGING
setScrollState(SCROLL_STATE_DRAGGING);
}
}
// 拖拽滑动
if (mScrollState == SCROLL_STATE_DRAGGING) {
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
if (dispatchNestedPreScroll(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
mReusableIntPair, mScrollOffset, TYPE_TOUCH
)) {
dx -= mReusableIntPair[0];
dy -= mReusableIntPair[1];
// Updated the nested offsets
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
// Scroll has initiated, prevent parents from intercepting
getParent().requestDisallowInterceptTouchEvent(true);
}
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
e)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
if (mGapWorker != null && (dx != 0 || dy != 0)) {
mGapWorker.postFromTraversal(this, dx, dy);
}
}
} break;
}
}
主要看拖拽状态处理,会调用 dispatchNestedPreScroll() 分发嵌套滑动事件:
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,
int type) {
return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow,
type);
}
还是借助 NestedScrollingChildHelper 中转给父视图:
// #7
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
@Nullable int[] offsetInWindow, @NestedScrollType int type) {
if (isNestedScrollingEnabled()) {
// 没有找到可以处理 type 滑动类型的父视图则直接返回 false
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[0] = 0;
consumed[1] = 0;
// 回调嵌套滑动父视图
ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
offsetInWindow[0] -= startX;
offsetInWindow[1] -= startY;
}
// 只要父视图消费了(无须完全消费)位移距离就返回 true
return consumed[0] != 0 || consumed[1] != 0;
} else if (offsetInWindow != null) {
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}
仍是 ViewParentCompat 做兼容处理,回调 NestedScrollingParent 的 onNestedPreScroll():
public static void onNestedPreScroll(ViewParent parent, View target, int dx, int dy,
int[] consumed, int type) {
if (parent instanceof NestedScrollingParent2) {
// First try the NestedScrollingParent2 API
((NestedScrollingParent2) parent).onNestedPreScroll(target, dx, dy, consumed, 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 {
parent.onNestedPreScroll(target, dx, dy, consumed);
} catch (AbstractMethodError e) {
Log.e(TAG, "ViewParent " + parent + " does not implement interface "
+ "method onNestedPreScroll", e);
}
} else if (parent instanceof NestedScrollingParent) {
((NestedScrollingParent) parent).onNestedPreScroll(target, dx, dy, consumed);
}
}
}
回调到 NestedScrollView 的 onNestedPreScroll(),发现并没有进行相应的滑动,而是作为嵌套滑动子视图将嵌套滑动事件向父视图传递,询问父视图是否进行处理:
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
int type) {
// 没有进行滑动处理,而是向外分发了
dispatchNestedPreScroll(dx, dy, consumed, null, type);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,
int type) {
// 借助 Helper 把滑动事件向上层容器分发,自己没处理滑动
return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
}
其实这就是问题所在,NestedScrollLayout 应该重写 onNestedPreScroll(),在向上滑动并且 Header 部分可见时,消费 Y 轴的滑动距离,而不是将滑动事件向自己的父视图传递:
override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
// 向上滑动,并且当前NestedScrollLayout可显示区域的顶部纵坐标小于Header高度,就消费
if (dy > 0 && scrollY <= headerView.measuredHeight) {
scrollBy(0, dy)
consumed[1] = dy
}
}
RecyclerView 接收到滑动事件,要先问嵌套滑动的父视图 NestedScrollLayout 是否可以滑动,若父视图可以滑动,则应由父视图消费该滑动事件。只有当父视图无法滑动时,反馈给子视图,然后才由子视图 RecyclerView 进行滑动。
3.4 ACTION_UP
ACTION_UP 这部分我们主要看惯性滑动相关的代码。
惯性滑动也是先给嵌套滑动的父视图滑动,父视图滑完子视图才滑。在子视图 RecyclerView 的 onTouchEvent() 内:
@Override
public boolean onTouchEvent(MotionEvent e) {
...
switch (action) {
...
case MotionEvent.ACTION_UP: {
mVelocityTracker.addMovement(vtev);
eventAddedToVelocityTracker = true;
mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
final float xvel = canScrollHorizontally
? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;
final float yvel = canScrollVertically
? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
// fling() 处理惯性滑动
if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
setScrollState(SCROLL_STATE_IDLE);
}
resetScroll();
} break;
}
}
只要在 x 轴或 y 轴有速度才有机会执行到 fling():
public boolean fling(int velocityX, int velocityY) {
...
// canScrollHorizontal 与 canScrollVertical 默认实现返回的都是 false,后续详解
final boolean canScrollHorizontal = mLayout.canScrollHorizontally();
final boolean canScrollVertical = mLayout.canScrollVertically();
if (!canScrollHorizontal || Math.abs(velocityX) < mMinFlingVelocity) {
velocityX = 0;
}
if (!canScrollVertical || Math.abs(velocityY) < mMinFlingVelocity) {
velocityY = 0;
}
if (velocityX == 0 && velocityY == 0) {
// If we don't have any velocity, return false
return false;
}
// 在 RecyclerView 处理惯性滑动之前,先问嵌套滚动的父视图是否处理惯性滑动
if (!dispatchNestedPreFling(velocityX, velocityY)) {
// 如果父视图完全不处理,或者处理之后还有滑动余量,RecyclerView 才处理
final boolean canScroll = canScrollHorizontal || canScrollVertical;
dispatchNestedFling(velocityX, velocityY, canScroll);
...
}
return false;
}
dispatchNestedPreFling() 在接口中的注释写道:
/**
* 在当前这个视图处理之前向嵌套滚动父视图分发一个 fling
* 嵌套的 pre-fling 事件对于嵌套的 fling,就像嵌套的 pre-scroll 之于 scroll,intercept
* 之于 touch。dispatchNestedPreFling() 为父视图在子视图消费 fling 之前完全消费掉 fling
* 提供了机会。如果此方法返回 true,则嵌套父级视图已经消耗了 fling,因此此视图不应滚动。
*
* 为了更好的用户体验,嵌套滚动链中只有一个视图应该消耗 fling。自定义 View 应以两种方式
* 考虑此问题:
* 1.如果自定义视图是分页的并且需要安定到固定页面点,请勿调用 dispatchNestedPreFling
* 2.如果嵌套父级确实消耗了 fling,则此视图根本不应滚动,即使要回到一个有效的空闲位置也不行
*
* 两个参数分别表示水平和垂直两个方向的 fling 速度(每秒像素数),返回值为 true 表示嵌套
* 滚动的父视图消耗了 fling
*/
boolean dispatchNestedPreFling(float velocityX, float velocityY);
RecyclerView 的 fling() 也正是按照以上原则处理的,当 dispatchNestedPreFling() 返回 false 时才调用 dispatchNestedFling(),仍是通过 NestedScrollingChildHelper 分发,回调父视图 NestedScrollingView 的 onNestedFling():
@Override
public boolean onNestedFling(
@NonNull View target, float velocityX, float velocityY, boolean consumed) {
if (!consumed) {
// 这里在向父视图分发时,consumed 传了 true 表示子视图已经消费,这样一般
// 情况下父视图就不会再做惯性滑动处理
dispatchNestedFling(0, velocityY, true);
// 自己处理惯性滑动
fling((int) velocityY);
return true;
}
return false;
}
consumed 参数,是在 RecyclerView 的 fling() 中是由 canScroll 变量决定的:
public boolean fling(int velocityX, int velocityY) {
...
final boolean canScrollHorizontal = mLayout.canScrollHorizontally();
final boolean canScrollVertical = mLayout.canScrollVertically();
...
// 嵌套滑动父视图没有处理完惯性滑动才需要子视图处理
if (!dispatchNestedPreFling(velocityX, velocityY)) {
final boolean canScroll = canScrollHorizontal || canScrollVertical;
// 子视图处理惯性滑动时,还是会先让父视图做处理
dispatchNestedFling(velocityX, velocityY, canScroll);
...
}
return false;
}
mLayout 是一个 LayoutManager,canScrollHorizontally() 的默认实现都返回 false。RecyclerView 内部设置 mLayout 的只有 setLayoutManager(),而该方法又只被 createLayoutManager() 调用。createLayoutManager() 会根据用户提供的 LayoutManager 的全类名以反射的方式创建出一个 LayoutManager 实例,这个全类名是在 RecyclerView 的构造方法中通过属性解析获取到的:
public RecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
...
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RecyclerView,
defStyleAttr, 0);
...
String layoutManagerName = a.getString(R.styleable.RecyclerView_layoutManager);
...
createLayoutManager(context, layoutManagerName, attrs, defStyleAttr, 0);
...
}
也就是说,如果你没有在布局文件中为 RecyclerView 配置 layoutManager 这个属性,那么父视图 NestedScrollingView 的 onNestedFling() 接收的 consumed 就是 false,进而执行 if 语句,先 dispatchNestedFling() 将惯性滑动分发给嵌套滑动父视图,再 fling() 自己执行惯性滑动并返回 true。由于 NestedScrollingLayout 的父视图不会处理惯性滑动,因此所有的滑动距离都由其 fling() 消费:
public void fling(int velocityY) {
if (getChildCount() > 0) {
mScroller.fling(getScrollX(), getScrollY(), // start
0, velocityY, // velocities
0, 0, // x
Integer.MIN_VALUE, Integer.MAX_VALUE, // y
0, 0); // overscroll
runAnimatedScroll(true);
}
}
因此我们无须自己实现惯性滑动,因此系统已经帮我们实现了。