10分钟带你入门NestedScrolling机制
Posted 低调小一
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了10分钟带你入门NestedScrolling机制相关的知识,希望对你有一定的参考价值。
一、从一个简单的DEMO看什么是嵌套滚动##
我们先来看一下DEMO的效果,直观的感受一下什么是嵌套滚动:
在解释上图涉及到哪些嵌套滑动操作之前,我先贴一下嵌套布局的xml结构:
<com.wzy.nesteddetail.view.NestedWebViewRecyclerViewGroup>
<com.wzy.nesteddetail.view.NestedScrollWebView/>
<TextView />
<android.support.v7.widget.RecyclerView />
</com.wzy.nesteddetail.view.NestedWebViewRecyclerViewGroup>
其中:
- NestedWebViewRecyclerViewGroup为最外层滑动容器;
- com.wzy.nesteddetail.view.NestedScrollWebView为布局顶部可嵌套滑动的View;
- TextView为布局中部不可滑动的View;
- android.support.v7.widget.RecyclerView为布局底部可滑动的View;
现在我们来说明一下,简单的DEMO效果中包含了哪些嵌套滑动操作:
- 向上滑动顶部WebView时,首先滑动WebView的内容,WebView的内容滑动到底后再滑动外层容器。外层容器滑动到RecyclerView完全露出后,再将滑动距离或者剩余速度传递给RecyclerView继续滑动.
- 滑动底部RecyclerView时,首先滑动RecyclerView的内容,RecyclerView的内容滑动到顶后再滑动外层容器。外层容器也滑动到顶后,再将滑动距离或者剩余速度传递给WebView继续滑动.
- 触摸本身不可滑动的TextView时,滑动事件被外层容器拦截。外层容器根据滑动方向和是否滑动到相应阈值,再将相应的滑动距离或者速度传递给WebView或者RecyclerView.
再不知道NestedScrolling机制之前,我相信大部分人想实现上面的滑动效果都是比较头大的,特别是滑动距离和速度要从WebView->外层容器->RecyclerView并且还要支持反向传递。
有了Google提供的牛逼嵌套滑动机制,再加上这篇文章粗浅的科普,我相信大部分人都能够实现这种滑动效果。这种效果最常见的应用场景就是各种新闻客户端的详情页。
二、NestedScrolling接口简介
Android在support.v4包中提供了用于View支持嵌套滑动的两个接口:
- NestedScrollingParent
- NestedScrollingChild
我先用比较白话的语言介绍一下NestedScrolling的工作原理:
- Google从逻辑上区分了滑动的两个角色:NestedScrollingParent简称ns parent,NestedScrollingChild简称ns child。对应了滑动布局中的外层滑动容器和内部滑动容器。
- ns child在收到DOWN事件时,找到离自己最近的ns parent,与它进行绑定并关闭它的事件拦截机制。
- ns child会在接下来的MOVE事件中判定出用户触发了滑动手势,并把事件拦截下来给自己消费。
- 消费MOVE事件流时,对于每一个MOVE事件增加的滑动距离:
4.1. ns child并不是直接自己消费,而是先将它交给ns parent,让ns parent可以在ns child滑动前进行消费。
4.2. 如果ns parent没有消费或者滑动没消费完,ns child再消费剩下的滑动。
4.3. 如果ns child消费后滑动还是有剩余,会把剩下的滑动距离再交给ns parent消费。
4.4. 最后如果ns parent消费滑动后还有剩余,ns child可以做最终处理。 - ns child在收到UP事件时,可以计算出需要滚动的速度,ns child对于速度的消费流程是:
5.1 ns child在进行flying操作前,先询问ns parent是否需要消费该速度。如果ns parent消费该速度,后续就由ns parent带飞,自己就不消费该速度了。如果ns parent不消费,则ns child进行自己的flying操作。
5.2 ns child在flying过程中,如果已经滚动到阈值速度仍没有消费完,会再次将速度分发给ns parent,将ns parent进行消费。
NestedScrollingParent和NestedScrollingChild的源码定义也是为了配合滑动实现定义出来的:
NestedScrollingChild
void setNestedScrollingEnabled(boolean enabled); // 设置是否开启嵌套滑动
boolean isNestedScrollingEnabled(); // 获得设置开启了嵌套滑动
boolean startNestedScroll(@ScrollAxis int axes); // 沿给定的轴线开始嵌套滚动
void stopNestedScroll(); // 停止当前嵌套滚动
boolean hasNestedScrollingParent(); // 如果有ns parent,返回true
boolean dispatchNestedPreScroll(int dx
, int dy
, @Nullable int[] consumed
, @Nullable int[] offsetInWindow); // 消费滑动时间前,先让ns parent消费
boolean dispatchNestedScroll(int dxConsumed
, int dyConsumed
, int dxUnconsumed
, int dyUnconsumed
, @Nullable int[] offsetInWindow); // ns parent消费ns child剩余滚动后是否还有剩余。return true代表还有剩余
boolean dispatchNestedPreFling(float velocityX
, float velocityY); // 消费fly速度前,先让ns parent消费
boolean dispatchNestedFling(float velocityX
, float velocityY
, boolean consumed); // ns parent消费ns child消费后的速度之后是否还有剩余。return true代表还有剩余
NestedScrollingParent
boolean onStartNestedScroll(@NonNull View var1
, @NonNull View var2
, int var3); // 决定是否接收子View的滚动事件
void onNestedScrollAccepted(@NonNull View var1
, @NonNull View var2
, int var3); // 响应子View的滚动
void onStopNestedScroll(@NonNull View var1); // 滚动结束的回调
void onNestedPreScroll(@NonNull View var1
, int var2
, int var3
, @NonNull int[] var4); // ns child滚动前回调
void onNestedScroll(@NonNull View var1
, int var2
, int var3
, int var4
, int var5); // ns child滚动后回调
boolean onNestedPreFling(@NonNull View var1
, float var2
, float var3); // ns child flying前回调
boolean onNestedFling(@NonNull View var1
, float var2
, float var3
, boolean var4); // ns child flying后回调
int getNestedScrollAxes(); // 返回当前布局嵌套滚动的坐标轴
Google为了让开发者更加方便的实现这两个接口,提供了NestedScrollingParentHelper和NestedScrollingChildHelper这两个辅助。所以实现NestedScrolling这两个接口的常用写法是:
ns child:
public class NestedScrollingWebView extends WebView implements NestedScrollingChild
private NestedScrollingChildHelper mChildHelper;
private NestedScrollingChildHelper getNestedScrollingHelper()
if (mChildHelper == null)
mChildHelper = new NestedScrollingChildHelper(this);
return mChildHelper;
@Override
public void setNestedScrollingEnabled(boolean enabled)
getNestedScrollingHelper().setNestedScrollingEnabled(enabled);
@Override
public boolean isNestedScrollingEnabled()
return getNestedScrollingHelper().isNestedScrollingEnabled();
@Override
public boolean startNestedScroll(int axes)
return getNestedScrollingHelper().startNestedScroll(axes);
@Override
public void stopNestedScroll()
getNestedScrollingHelper().stopNestedScroll();
@Override
public boolean hasNestedScrollingParent()
return getNestedScrollingHelper().hasNestedScrollingParent();
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow)
return getNestedScrollingHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow)
return getNestedScrollingHelper().dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed)
return getNestedScrollingHelper().dispatchNestedFling(velocityX, velocityY, consumed);
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY)
return getNestedScrollingHelper().dispatchNestedPreFling(velocityX, velocityY);
@Override
public boolean startNestedScroll(int axes, int type)
return getNestedScrollingHelper().startNestedScroll(axes, type);
@Override
public void stopNestedScroll(int type)
getNestedScrollingHelper().stopNestedScroll(type);
@Override
public boolean hasNestedScrollingParent(int type)
return getNestedScrollingHelper().hasNestedScrollingParent(type);
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, int type)
return getNestedScrollingHelper().dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, type);
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow, int type)
return getNestedScrollingHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
ns parent:
public class NestedScrollingDetailContainer extends ViewGroup implements NestedScrollingParent
private NestedScrollingParentHelper mParentHelper;
private NestedScrollingParentHelper getNestedScrollingHelper()
if (mParentHelper == null)
mParentHelper = new NestedScrollingParentHelper(this);
return mParentHelper;
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes)
return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
@Override
public int getNestedScrollAxes()
return getNestedScrollingHelper().getNestedScrollAxes();
@Override
public void onNestedScrollAccepted(View child, View target, int axes)
getNestedScrollingHelper().onNestedScrollAccepted(child, target, axes);
@Override
public void onStopNestedScroll(View child)
getNestedScrollingHelper().onStopNestedScroll(child);
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY)
// 处理预先flying事件
return false;
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed)
// 处理后续flying事件
return false;
@Override
public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed)
// 处理后续scroll事件
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @Nullable int[] consumed)
// 处理预先滑动scroll事件
三、效果实现
只知道原理大家肯定是不满足的,结合原理进行实操才是关键。这里以DEMO的效果为例,想要实现DEMO的效果,需要自定义两个嵌套滑动容器:
- 自定义一个支持嵌套WebView和RecyclerView滑动的外部容器。
- 自定义一个实现NestedScrollingChild接口的WebView。
外部滑动容器
在实现外部滑动的容器的时候,我们首先需要考虑这个外部滑动容器的滑动阈值是什么?
答: 外部滑动的滑动阈值=外部容器中所有子View的高度-外部容器的高度。同理类似WebView的滑动阈值=WebView的内容高度-WebView的容器高度。
对应代码实现:
private int mInnerScrollHeight; // 可滑动的最大距离
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width;
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
if (widthMode == MeasureSpec.EXACTLY)
width = measureWidth;
else
width = mScreenWidth;
int left = getPaddingLeft();
int right = getPaddingRight();
int top = getPaddingTop();
int bottom = getPaddingBottom();
int count = getChildCount();
for (int i = 0; i < count; i++)
View child = getChildAt(i);
LayoutParams params = child.getLayoutParams();
int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, left + right, params.width);
int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, top + bottom, params.height);
measureChild(child, childWidthMeasureSpec, childHeightMeasureSpec);
setMeasuredDimension(width, measureHeight);
findWebView(this);
findRecyclerView(this);
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b)
int childTotalHeight = 0;
mInnerScrollHeight = 0;
for (int i = 0; i < getChildCount(); i++)
View child = getChildAt(i);
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
child.layout(0, childTotalHeight, childWidth, childHeight + childTotalHeight);
childTotalHeight += childHeight;
mInnerScrollHeight += childHeight;
mInnerScrollHeight -= getMeasuredHeight();
其次,需要考虑当WebView传递上滑事件和RecyclerView传递下滑事件时如何处理:
- 向上滑动时,如果WebView内容还没有到底,该事件交给WebView处理;如果WebView内容已经滑动到底,但是滑动距离没有超过外部容器的最大滑动距离,该事件由滑动容器自身处理;如果WebView内容已经滑动到底,并且滑动距离超过了外部容器的最大滑动距离,这时将滑动事件传递给底部的 RecyclerView,让RecyclerView处理;
- 向下滑动时,如果RecyclerView没有到顶部,该事件交给RecyclerView处理;如果RecyclerView已经到顶部,并且外部容器的滑动距离不为0,该事件由外部容器处理;如果RecyclerView已经到顶部,并且外部容器的滑动距离已经为0,则该事件交给WebView处理;
对应的WebView传递上滑速度和RecyclerView传递下滑速度,处理和Scroll传递类似。
对应代码实现:
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @Nullable int[] consumed, int type)
boolean isWebViewBottom = !canWebViewScrollDown();
boolean isCenter = isParentCenter();
if (dy > 0 && isWebViewBottom && getScrollY() < getInnerScrollHeight())
//为了WebView滑动到底部,继续向下滑动父控件
scrollBy(0, dy);
if (consumed != null)
consumed[1] = dy;
else if (dy < 0 && isCenter)
//为了RecyclerView滑动到顶部时,继续向上滑动父控件
scrollBy(0, dy);
if (consumed != null)
consumed[1] = dy;
if (isCenter && !isWebViewBottom)
//异常情况的处理
scrollToWebViewBottom();
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY)
if (target instanceof NestedScrollingWebView)
//WebView滑到底部时,继续向下滑动父控件和RV
mCurFlyingType = FLYING_FROM_WEBVIEW_TO_PARENT;
parentFling(velocityY);
else if (target instanceof RecyclerView && velocityY < 0 && getScrollY() >= getInnerScrollHeight())
//RV滑动到顶部时,继续向上滑动父控件和WebView,这里用于计算到达父控件的顶部时RV的速度
mCurFlyingType = FLYING_FROM_RVLIST_TO_PARENT;
parentFling((int) velocityY);
else if (target instanceof RecyclerView && velocityY > 0)
mIsRvFlyingDown = true;
return false;
@Override
public void computeScroll()
if (mScroller.computeScrollOffset())
int currY = mScroller.getCurrY();
switch (mCurFlyingType)
case FLYING_FROM_WEBVIEW_TO_PARENT://WebView向父控件滑动
if (mIsRvFlyingDown)
//RecyclerView的区域的fling由自己完成
break;
scrollTo(0, currY);
invalidate();
checkRvTop();
if (getScrollY() == getInnerScrollHeight() && !mIsSetFlying)
//滚动到父控件底部,滚动事件交给RecyclerView
mIsSetFlying = true;
recyclerViewFling((int) mScroller.getCurrVelocity());
br以上是关于10分钟带你入门NestedScrolling机制的主要内容,如果未能解决你的问题,请参考以下文章
都9102年了,还不会Docker?10分钟带你从入门操作到实战上手