我开源了一个方便RecyclerView吸顶的Android库,欢迎您访问github.com/lizijin/Sti…,如果您使用本库,请提出您的宝贵意见。
它目前支持以下功能:
- 支持单类型吸顶功能
- 支持多类型吸顶功能
- 支持开启和关闭吸顶功能
- 支持指定位置吸顶功能
- 支持设置吸顶偏移量
- 支持自定义RecyclerView上Item吸顶边界自定义
- 可以无缝配合AppBarLayout
AppBarLayout设计的主要目的,我个人认为有以下几个:
- 配合ScrollableView如NestedScrollView、RecyclerView等完成嵌套滑动功能。制造滑动的主动权是在ScrollableView上。
- 制定了ScrollableView依赖于AppBarLayout的规则,当AppBarLayout主动滑动时,ScollableView能够根据AppBarLayout的位置,调整自身的位置。与1相对应,制造滑动的主动权在AppBarLayout上。
- AppBarLayout继承于LinearLayout,它通过对子View设置scroll相关的标志,来控制子View是否跟随滑动、上滑的时候是否吸顶、下滑的时候是否优先跟随滑动。
- 对外暴露了OnOffsetChangedListener,以便更灵活地实现AppBarLayout本身不能做到的一些功能。
本文主要会围绕这几个点,结合源码讲解AppBarLayout。
1. 从LinearLayout的子类角度讲解看AppBarLayout
我们都知道AppBarLayout类是继承了LinearLayout类的,并且设置为垂直方向的。对于LinearLayout,大家都很熟悉了。在AppBarLayout的篇幅中,我觉得还是需要强调几个知识点的,虽然很简单,但是依然有一些很重要的细节会被忽略掉。
- 假设AppBarLayout有四个子View,view1、view2、view3、view4。view4是绘制在最上面的,view1是绘制在最下面的。我们可能都知道,对子view设置app:layout_scrollFlags="scroll"可以让子view滑出屏幕。我们可以设置view1的属性app:layout_scrollFlags="scroll"。但是如果我们只设置view2的layout_scrollFlags="scroll",那么view2的该属性相当于没有设置。原因在于,假设view2可以滑动出屏幕,那么它势必会与view1产生交集,而且会绘制在view1上面,这样的效果很丑,google大神在设计的时候规避掉了这种不好的用户体验。如果子View没有设置scroll标志,那么它后面的兄弟,即使设置了scroll标志,也是无效的。getTotalScrollRange方法是计算AppBarLayout可以滑动出屏幕的距离。 我们可以看到如果不满足(flags & LayoutParams.SCROLL_FLAG_SCROLL) != 0条件,循环是会被break掉的,后面的子view根本都不参与计算,系统代码如下:
public final int getTotalScrollRange() {
if (totalScrollRange != INVALID_SCROLL_RANGE) {
return totalScrollRange;
}
int range = 0;
for (int i = 0, z = getChildCount(); i < z; i++) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int childHeight = child.getMeasuredHeight();
final int flags = lp.scrollFlags;
if ((flags & LayoutParams.SCROLL_FLAG_SCROLL) != 0) {
// We're set to scroll so add the child's height
range += childHeight + lp.topMargin + lp.bottomMargin;
if (i == 0 && ViewCompat.getFitsSystemWindows(child)) {
// If this is the first child and it wants to handle system windows, we need to make
// sure we don't scroll it past the inset
range -= getTopInset();
}
if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) {
// For a collapsing scroll, we to take the collapsed height into account.
// We also break straight away since later views can't scroll beneath
// us
range -= ViewCompat.getMinimumHeight(child);
break;
}
} else {
// As soon as a view doesn't have the scroll flag, we end the range calculation.
// This is because views below can not scroll under a fixed view.
break;
}
}
return totalScrollRange = Math.max(0, range);
}
-
AppBarLayout的onMeasure方法,比较普通说白了就是沿用了LinearLayout的测量思路。但是为什么要在这里提它呢,因为与它对应的ScrollableView对应的ScrollingViewBehavior的测量方法还是比较重要的,后面我们会讲
-
AppBarLayout的onLayout方法,比较普通,说白了就是沿用了LinearLayout的layout思路。在这里提它的原因同2。
2. AppBarLayout对事件的处理
AppBarLayout有一个默认的Behavior,AppBarLayout$BaseBehavior,继承自com.google.android.material.appbar.HeaderBehavior,该类的主要作用就是处理触摸事件的。
//com.google.android.material.appbar.HeaderBehavior
@Override
public boolean onTouchEvent(
@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull MotionEvent ev) {
if (touchSlop < 0) {
touchSlop = ViewConfiguration.get(parent.getContext()).getScaledTouchSlop();
}
switch (ev.getActionMasked()) {
//省略其他事件
case MotionEvent.ACTION_MOVE:
{
final int activePointerIndex = ev.findPointerIndex(activePointerId);
if (activePointerIndex == -1) {
return false;
}
final int y = (int) ev.getY(activePointerIndex);
int dy = lastMotionY - y;
if (!isBeingDragged && Math.abs(dy) > touchSlop) {
isBeingDragged = true;
if (dy > 0) {
dy -= touchSlop;
} else {
dy += touchSlop;
}
}
if (isBeingDragged) {
lastMotionY = y;
// We're being dragged so scroll the ABL
scroll(parent, child, dy, getMaxDragOffset(child), 0);
}
break;
}
return true;
}
我们可以看到处理Move事件时,会调用scroll方法,顾名思义就是让AppBarLayout滑出屏幕或者滑入屏幕。
final int scroll(
CoordinatorLayout coordinatorLayout, V header, int dy, int minOffset, int maxOffset) {
return setHeaderTopBottomOffset(
coordinatorLayout,
header,
getTopBottomOffsetForScrollingSibling() - dy,
minOffset,
maxOffset);
}
scroll方法主要是通过offsetTopAndBottom来实现偏移的。而且该方法,会有返回值,主要是处理ScrollableView发起的嵌套滑动用的,但是在这里,没有嵌套滑动的逻辑需要处理。
一般情况下,我们使用AppBarLayout和RecyclerView的时候,它们的布局总是前者在后者的上面。那么问题来了,AppBarLayout滑出了屏幕,如果RecyclerView不作出相应的改变,那么它们中间势必会有一段空白,这显然是不合理的,那么AppBarlayout是如何规避这个问题的。答案是通过CoordinatorLayout的依赖关系和AppBarLayout$ScrollingViewBehavior。
3. ScrollingViewBehavior为RecyclerView测量、Layout、跟随ABL滑动
ScrollingViewBehavior主要作用就是三个
- 跟随APL滑动
//ScrollingViewBehavior.java
@Override
public boolean onDependentViewChanged(
@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
offsetChildAsNeeded(child, dependency);
updateLiftedStateIfNeeded(child, dependency);
return false;
}
//根据APL的位置移动ScrollableView
private void offsetChildAsNeeded(@NonNull View child, @NonNull View dependency) {
final CoordinatorLayout.Behavior behavior =
((CoordinatorLayout.LayoutParams) dependency.getLayoutParams()).getBehavior();
if (behavior instanceof BaseBehavior) {
// Offset the child, pinning it to the bottom the header-dependency, maintaining
// any vertical gap and overlap
final BaseBehavior ablBehavior = (BaseBehavior) behavior;
ViewCompat.offsetTopAndBottom(
child,
(dependency.getBottom() - child.getTop())
+ ablBehavior.offsetDelta
+ getVerticalLayoutGap()
- getOverlapPixelsForOffset(dependency));
}
}
- 为ScrollableView测量高度,由父类HeaderScrollingViewBehavior实现,主要的算法是,ScrollableView本身测量的高度-APL的高度+APL可滑动的距离,这个细节还是蛮重要的,想想为什么要这么设计。因为必须要加上APL可滑动的距离,否则,往上滑的时候,ScrollableView的高度不够,会出现白色的真空地带,影响用户体验。
//HeaderScrollingViewBehavior
@Override
public boolean onMeasureChild(
@NonNull CoordinatorLayout parent,
@NonNull View child,
int parentWidthMeasureSpec,
int widthUsed,
int parentHeightMeasureSpec,
int heightUsed) {
final int childLpHeight = child.getLayoutParams().height;
if (childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT
|| childLpHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
// If the menu's height is set to match_parent/wrap_content then measure it
// with the maximum visible height
final List<View> dependencies = parent.getDependencies(child);
final View header = findFirstDependency(dependencies);
if (header != null) {
int availableHeight = View.MeasureSpec.getSize(parentHeightMeasureSpec);
if (availableHeight > 0) {
if (ViewCompat.getFitsSystemWindows(header)) {
final WindowInsetsCompat parentInsets = parent.getLastWindowInsets();
if (parentInsets != null) {
availableHeight += parentInsets.getSystemWindowInsetTop()
+ parentInsets.getSystemWindowInsetBottom();
}
}
} else {
// If the measure spec doesn't specify a size, use the current height
availableHeight = parent.getHeight();
}
int height = availableHeight + getScrollRange(header);
int headerHeight = header.getMeasuredHeight();
if (shouldHeaderOverlapScrollingChild()) {
child.setTranslationY(-headerHeight);
} else {
height -= headerHeight;
}
final int heightMeasureSpec =
View.MeasureSpec.makeMeasureSpec(
height,
childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT
? View.MeasureSpec.EXACTLY
: View.MeasureSpec.AT_MOST);
// Now measure the scrolling view with the correct height
parent.onMeasureChild(
child, parentWidthMeasureSpec, widthUsed, heightMeasureSpec, heightUsed);
return true;
}
}
return false;
}
3.把ScrollableView布局在APL下方,代码比较简单,主要是计算位置。
//HeaderScrollingViewBehavior
@Override
protected void layoutChild(
@NonNull final CoordinatorLayout parent,
@NonNull final View child,
final int layoutDirection) {
final List<View> dependencies = parent.getDependencies(child);
final View header = findFirstDependency(dependencies);
if (header != null) {
final CoordinatorLayout.LayoutParams lp =
(CoordinatorLayout.LayoutParams) child.getLayoutParams();
final Rect available = tempRect1;
available.set(
parent.getPaddingLeft() + lp.leftMargin,
header.getBottom() + lp.topMargin,
parent.getWidth() - parent.getPaddingRight() - lp.rightMargin,
parent.getHeight() + header.getBottom() - parent.getPaddingBottom() - lp.bottomMargin);
final WindowInsetsCompat parentInsets = parent.getLastWindowInsets();
if (parentInsets != null
&& ViewCompat.getFitsSystemWindows(parent)
&& !ViewCompat.getFitsSystemWindows(child)) {
// If we're set to handle insets but this child isn't, then it has been measured as
// if there are no insets. We need to lay it out to match horizontally.
// Top and bottom and already handled in the logic above
available.left += parentInsets.getSystemWindowInsetLeft();
available.right -= parentInsets.getSystemWindowInsetRight();
}
final Rect out = tempRect2;
GravityCompat.apply(
resolveGravity(lp.gravity),
child.getMeasuredWidth(),
child.getMeasuredHeight(),
available,
out,
layoutDirection);
final int overlap = getOverlapPixelsForOffset(header);
child.layout(out.left, out.top - overlap, out.right, out.bottom - overlap);
verticalLayoutGap = out.top - header.getBottom();
} else {
// If we don't have a dependency, let super handle it
super.layoutChild(parent, child, layoutDirection);
verticalLayoutGap = 0;
}
}
4. AppBarLayout的嵌套滑动
AppBarLayout嵌套滑动,指的是当滚动下方ScrollableView时,AppBarLayout会跟随滑动。主要有三种情况:
- ScrollableView向上滑动时,ABL跟随滑动
- ScrollableView向下滑动时,ABL跟随滑动
- ScrollableView在顶部,向下滑动时,ABL处理ScrollableView无法处理的滑动
以上Case1、Case2 对应的方法是AppBarLayoutBaseBehavior#onNestedPreScroll,Case3对应的方法是AppBarLayoutBaseBehavior#onNestedScroll。
public void onNestedPreScroll(
CoordinatorLayout coordinatorLayout,
@NonNull T child,
View target,
int dx,
int dy,
int[] consumed,
int type) {
if (dy != 0) {
int min;
int max;
if (dy < 0) {
// We're scrolling down
min = -child.getTotalScrollRange();
max = min + child.getDownNestedPreScrollRange();
} else {
// We're scrolling up
min = -child.getUpNestedPreScrollRange();
max = 0;
}
if (min != max) {
consumed[1] = scroll(coordinatorLayout, child, dy, min, max);
}
}
if (child.isLiftOnScroll()) {
child.setLiftedState(child.shouldLift(target));
}
}
- 当ScrollableView向上滑动时,ABL滑动距离在[-child.getUpNestedPreScrollRange(),0]范围内,0表示恢复原状,-child.getUpNestedPreScrollRange()表示滑出屏幕的距离,getUpNestedPreScrollRange()的值等于getTotalScrollRange()的值
int getUpNestedPreScrollRange() {
return getTotalScrollRange();
}
- 当ScrollableView向下滑动时,ABL滑动距离在[-child.getTotalScrollRange(),-child.getTotalScrollRange()+child.getDownNestedPreScrollRange()]范围内。
int getDownNestedPreScrollRange() {
if (downPreScrollRange != INVALID_SCROLL_RANGE) {
// If we already have a valid value, return it
return downPreScrollRange;
}
int range = 0;
for (int i = getChildCount() - 1; i >= 0; i--) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int childHeight = child.getMeasuredHeight();
final int flags = lp.scrollFlags;
if ((flags & LayoutParams.FLAG_QUICK_RETURN) == LayoutParams.FLAG_QUICK_RETURN) {
// First take the margin into account
int childRange = lp.topMargin + lp.bottomMargin;
// The view has the quick return flag combination...
if ((flags & LayoutParams.SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED) != 0) {
// If they're set to enter collapsed, use the minimum height
childRange += ViewCompat.getMinimumHeight(child);
} else if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) {
// Only enter by the amount of the collapsed height
childRange += childHeight - ViewCompat.getMinimumHeight(child);
} else {
// Else use the full height
childRange += childHeight;
}
if (i == 0 && ViewCompat.getFitsSystemWindows(child)) {
// If this is the first child and it wants to handle system windows, we need to make
// sure we don't scroll past the inset
childRange = Math.min(childRange, childHeight - getTopInset());
}
range += childRange;
} else if (range > 0) {
// If we've hit an non-quick return scrollable view, and we've already hit a
// quick return view, return now
break;
}
}
return downPreScrollRange = Math.max(0, range);
}
}
getUpNestedPreScrollRange和getDownNestedScrollRange的区别是,getUpNestedPreScrollRange是从第一个View开始遍历,getDownNestedScrollRange是从第最后一个View开始遍历计算距离。
- ABL处理ScrollableView无法处理的滑动在onNestedScroll方法中,只在ScrollableView向下滑动时会触发。
public void onNestedScroll(
CoordinatorLayout coordinatorLayout,
@NonNull T child,
View target,
int dxConsumed,
int dyConsumed,
int dxUnconsumed,
int dyUnconsumed,
int type,
int[] consumed) {
if (dyUnconsumed < 0) {
// If the scrolling view is scrolling down but not consuming, it's probably be at
// the top of it's content
consumed[1] =
scroll(coordinatorLayout, child, dyUnconsumed, -child.getDownNestedScrollRange(), 0);
}
}
int getDownNestedScrollRange() {
if (downScrollRange != INVALID_SCROLL_RANGE) {
// If we already have a valid value, return it
return downScrollRange;
}
int range = 0;
for (int i = 0, z = getChildCount(); i < z; i++) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
int childHeight = child.getMeasuredHeight();
childHeight += lp.topMargin + lp.bottomMargin;
final int flags = lp.scrollFlags;
if ((flags & LayoutParams.SCROLL_FLAG_SCROLL) != 0) {
// We're set to scroll so add the child's height
range += childHeight;
if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) {
// For a collapsing exit scroll, we to take the collapsed height into account.
// We also break the range straight away since later views can't scroll
// beneath us
range -= ViewCompat.getMinimumHeight(child);
break;
}
} else {
// As soon as a view doesn't have the scroll flag, we end the range calculation.
// This is because views below can not scroll under a fixed view.
break;
}
}
return downScrollRange = Math.max(0, range);
}
5. AppBarLayout Scroll相关的flag详解
前文我们看到在getDownNestedPreScrollRange等方法中,通过遍历子view,判断lp.scrollFlags等标志来计算偏移量。那么下面具体讲讲这些flag的作用
| flag | 值 | 含义 |
|---|---|---|
| SCROLL_FLAG_NO_SCROLL | 0x0 | 子View不允许滑动,默认值 |
| SCROLL_FLAG_SCROLL | 0x1 | 子View允许滑动,如果该子View前面的兄弟View没有设置该flag,标志位失效 |
| SCROLL_FLAG_EXIT_UNTIL_COLLAPSED | 1 << 1 | 子View向上滑动出屏幕时,会有mininumHeight的高度吸顶,它后面的子View的flag失效 |
| SCROLL_FLAG_ENTER_ALWAYS | 1 << 2 | |
| SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED | 1 << 3 | |
| SCROLL_FLAG_SNAP | 1 << 4 | |
| SCROLL_FLAG_SNAP_MARGINS | 1 << 5 |
相关组合
| 组合 | 值 | |
|---|---|---|
| FLAG_QUICK_RETURN | SCROLL_FLAG_SCROLL ` | ` SCROLL_FLAG_ENTER_ALWAYS |
| FLAG_SNAP | SCROLL_FLAG_SCROLL ` | ` SCROLL_FLAG_SNAP |
| COLLAPSIBLE_FLAGS | SCROLL_FLAG_EXIT_UNTIL_COLLAPSED ` | ` SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED |
源码使用场景
场景一、 向上滑动,ABL跟随滑动时,会判断SCROLL_FLAG_SCROLL和SCROLL_FLAG_EXIT_UNTIL_COLLAPSED。children从前往后遍历,如果没有设置SCROLL_FLAG_SCROLL,会中断遍历,如果设置了SCROLL_FLAG_SCROLL,可滑动距离+child.getMeasureheight,如果同时设置了SCROLL_FLAG_EXIT_UNTIL_COLLAPSED,也会中断遍历,滑动距离-child.getMinimumHeight,child.getMinimumHeight会一直停留在屏幕中。
代码片段如下
public final int getTotalScrollRange() {
if (totalScrollRange != INVALID_SCROLL_RANGE) {
return totalScrollRange;
}
int range = 0;
for (int i = 0, z = getChildCount(); i < z; i++) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int childHeight = child.getMeasuredHeight();
final int flags = lp.scrollFlags;
if ((flags & LayoutParams.SCROLL_FLAG_SCROLL) != 0) {
// We're set to scroll so add the child's height
range += childHeight + lp.topMargin + lp.bottomMargin;
if (i == 0 && ViewCompat.getFitsSystemWindows(child)) {
// If this is the first child and it wants to handle system windows, we need to make
// sure we don't scroll it past the inset
range -= getTopInset();
}
if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) {
// For a collapsing scroll, we to take the collapsed height into account.
// We also break straight away since later views can't scroll beneath
// us
range -= ViewCompat.getMinimumHeight(child);
break;
}
} else {
// As soon as a view doesn't have the scroll flag, we end the range calculation.
// This is because views below can not scroll under a fixed view.
break;
}
}
return totalScrollRange = Math.max(0, range);
}
场景二、 ScrollableView向下滑动时,ABL跟随滑动,会判断FLAG_QUICK_RETURN,SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED,SCROLL_FLAG_EXIT_UNTIL_COLLAPSED,children从后往前判断。
- FLAG_QUICK_RETURN,设置了该flag 向下滑的时候,会跟随滑出一段距离。距离由SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED、SCROLL_FLAG_EXIT_UNTIL_COLLAPSED决定
- 如果设置了SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED,滑出距离为ViewCompat.getMinimumHeight(child)
- 如果不满足条件2,但是设置了SCROLL_FLAG_EXIT_UNTIL_COLLAPSED,滑出距离childHeight - ViewCompat.getMinimumHeight(child),但是由于设置了该flag 会有ViewCompat.getMinimumHeight(child)吸顶,效果等同于全部滑出。
- 如果不满足条件2和条件3,滑动距离为childHeight
- FLAG_QUICK_RETURN与SCROLL_FLAG_SCROLL不一样。SCROLL_FLAG_SCROLL从前往后遍历,一旦遇到没设置的就中断遍历了。FLAG_QUICK_RETURN从后往前遍历,遇到没设置的不会中断遍历,除非曾经遇到过设置过该Flag而且滑动距离>0的情况会中断遍历(很绕,看源码,多假设场景)
int getDownNestedPreScrollRange() {
if (downPreScrollRange != INVALID_SCROLL_RANGE) {
// If we already have a valid value, return it
return downPreScrollRange;
}
int range = 0;
for (int i = getChildCount() - 1; i >= 0; i--) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int childHeight = child.getMeasuredHeight();
final int flags = lp.scrollFlags;
if ((flags & LayoutParams.FLAG_QUICK_RETURN) == LayoutParams.FLAG_QUICK_RETURN) {
// First take the margin into account
int childRange = lp.topMargin + lp.bottomMargin;
// The view has the quick return flag combination...
if ((flags & LayoutParams.SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED) != 0) {
// If they're set to enter collapsed, use the minimum height
childRange += ViewCompat.getMinimumHeight(child);
} else if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) {
// Only enter by the amount of the collapsed height
childRange += childHeight - ViewCompat.getMinimumHeight(child);
} else {
// Else use the full height
childRange += childHeight;
}
if (i == 0 && ViewCompat.getFitsSystemWindows(child)) {
// If this is the first child and it wants to handle system windows, we need to make
// sure we don't scroll past the inset
childRange = Math.min(childRange, childHeight - getTopInset());
}
range += childRange;
} else if (range > 0) {
// If we've hit an non-quick return scrollable view, and we've already hit a
// quick return view, return now
break;
}
}
return downPreScrollRange = Math.max(0, range);
}
场景三、 ScrollableView在顶部,向下滑动时,ABL处理ScrollableView无法处理的滑动。该场景与场景一一样。只会判断SCROLL_FLAG_SCROLL和SCROLL_FLAG_EXIT_UNTIL_COLLAPSED。从前往后遍历。
int getDownNestedScrollRange() {
if (downScrollRange != INVALID_SCROLL_RANGE) {
// If we already have a valid value, return it
return downScrollRange;
}
int range = 0;
for (int i = 0, z = getChildCount(); i < z; i++) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
int childHeight = child.getMeasuredHeight();
childHeight += lp.topMargin + lp.bottomMargin;
final int flags = lp.scrollFlags;
if ((flags & LayoutParams.SCROLL_FLAG_SCROLL) != 0) {
// We're set to scroll so add the child's height
range += childHeight;
if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) {
// For a collapsing exit scroll, we to take the collapsed height into account.
// We also break the range straight away since later views can't scroll
// beneath us
range -= ViewCompat.getMinimumHeight(child);
break;
}
} else {
// As soon as a view doesn't have the scroll flag, we end the range calculation.
// This is because views below can not scroll under a fixed view.
break;
}
}
return downScrollRange = Math.max(0, range);
}