嵌套滑动的简单应用

您所在的位置:网站首页 安卓滑动惯性怎么调整 嵌套滑动的简单应用

嵌套滑动的简单应用

2023-06-18 18:20| 来源: 网络整理| 查看: 265

今天要实现一个购物 app 首页的嵌套滑动效果,像京东、淘宝、闲鱼的首页都采用了类似的效果,如图:

请添加图片描述

其布局结构大致如下:

请添加图片描述 我们要实现的主要功能有:

嵌套滑动与惯性滑动时,嵌套滑动的父视图先滑动,然后子视图再滑向上滑动时,TabLayout 会固定在屏幕顶部

请添加图片描述 实际上这种效果直接用 CoordinatorLayout 那一套就可以轻松实现,但是抱着了解嵌套滑动机制的目的,我们自己动手来实现一个简单的 Demo。

1、布局结构

详细的布局结构示意图:

请添加图片描述 解释一下部分布局:

NestedScrollView:注意 NestedScrollView 与 ScrollView 的区别,ScrollView 是 FrameLayout 的子类,它不具备嵌套滑动的基础条件 —— 实现 NestedScrollingParent 或 NestedScrollingChild 接口族的接口之一;而同样是 FrameLayout 子类的 NestedScrollView 同时实现了 NestedScrollingParent3 与 NestedScrollingChild3 接口,既可以作为嵌套滑动中的“父亲”,也可以作为嵌套滑动中的“孩子”。因此这个位置用不了 ScrollView,而是要用 NestedScrollViewHeaderView:理解为包含 Banner 在内的、自己内部不进行上下滑动的 ViewRecyclerView: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; // 非拖拽滑动,先省略... if (mScrollState != 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 ... 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) 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); } }

因此我们无须自己实现惯性滑动,因此系统已经帮我们实现了。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3