CoordinatorLayout 与 Behavior
CoordinatorLayout 的使用
先看官网对 CoordinatorLayout 的介绍
CoordinatorLayout is a super_powered FrameLayout。
CoordinatorLayout is intended for two primary use cases:
As a top-level application decor or chrome layout;
As a container for a specific interaction with one or more child views
我们常用的是第二种情况居多
CoordinatorLayout 结合 AppBarLayout,CollapsingToolbarLayout 和 Toolbar 一起使用,可以给我们的应用带来更多的交互效果。
它们的布局关系
<android.support.design.widget.CoordinatorLayout...>
<android.support.design.widget.AppBarLayout...>
<android.support.design.widget.CollapsingToolbarLayout...>
<!-- your collapsed view -->
<View.../>
<android.support.v7.widget.Toolbar.../>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<!-- Scroll view -->
<android.support.v7.widget.RecyclerView.../>
</android.support.design.widget.CoordinatorLayout>
这是效果图
先看看它们的几个属性常用的几个布局元素
CoordinatorLayout
这几个属性都是直接在子布局中使用的
app:layout_behavior
指定子 View 的 behavior, 关于 behavior 后面会有详细的论述
app:layout_anchor
指定锚点的 View
app:layout_anchorGravity
指相对于锚 view 的布局重心
app:layout_keyline
AppBarLayout
AppBarLayout 一般作为 CoordinatorLayout 的直接子类使用;
AppBarLayout 的子 View 通过设置自身的 srollFlags 进行希望的滑动行为;
如果把 AppBarLayout 放到普通的 ViewGroup 中而不是 CoordinatorLayout 中,AppBarLayout 的功能将不会起作用
app:layout_scrollFlags/ setScrollFlags(int)
app:layout_scrollFlags 标记位是子布局设置是否可滑动
scroll: 滑动
enterAlways: 获取屏幕外,向下滑,会重新出现
exitUntilCollapsed 滑动一定距离出屏幕,会收叠成 minHeight
snap: 在活动停止之后,会自动滑动靠边的一侧
-
enterAlwaysCollapsed:要与 minHeight 和 enterAlways 一起使用, 当 View 达到 minHeight 的高度时,CollapsingToolbarLayout 开始展开展开完之后,才会进行滚动
<android.support.design.widget.CoordinatorLayout ... <android.support.design.widget.CollapsingToolbarLayout android:id="@+id/collapsing_toolbar_layout" android:layout_width="match_parent" android:layout_height="220dp" android:fitsSystemWindows="true" android:minHeight="100dp" app:expandedTitleMarginStart="38dp" app:layout_scrollFlags="scroll|enterAlwaysCollapsed|enterAlways"/> .../>
app:expanded
设置 AppBarLayout 是否展开
CollapsingToolbarLayout
CollapsingToolbarLayout 是一个实现了折叠功能包裹 Toolbar 的 View, 它做一位 AppBarLayout 子 View 使用
app:layout_collapseMode
- pin 固定,钉住
- parallax 会呈现视觉差,需要collapseParallaxMultiplier(0.0~1.0之间) 视觉差系数一起配合使用在代码中设置
<ImageView
android:id="@+id/img_bg"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:scaleType="centerCrop"
android:src="@drawable/coor_bg"
app:layout_collapseMode="parallax"
app:layout_collapseParallaxMultiplier="0.7"/>
app:contentScrim
折叠之后 toolbar 的颜色
Behavior
CoordinatorLayout 的子 View 进行一些交互,需要设置 Behavior
如果是 CoordinatorLayout 内部滑动的 View 需要设置已经为我们提供好的 behavior
app:layout_behavior="@string/appbar_scrolling_view_behavior"
behavior 的方法分为几类,
布局相关的方法
// 给 Behavior 设置 LayoutParams 时会调用
public void onAttachedToLayoutParams(...) {}
// LayoutParams 移除时会调用
public void onDetachedFromLayoutParams() {}
// CoordinatorLayout 在测量时会回调这个方法
public boolean onMeasureChild(...) {
return false;
}
// CoordinatorLayout 在布局时会回调这个方法
public boolean onLayoutChild(...) {
return false;
}
事件处理相关的方法
// 是否拦截 CoordinatorLayout 发过了的点击事件
public boolean onInterceptTouchEvent(...) {
return false;
}
// 接收 CoordinatorLayout 发过了的点击事件
public boolean onTouchEvent(...) {
return false;
}
滑动事件相关的方法
// 当 CoordinatorLayout 内有 NestedScrollView 开始滑动的时候回调
public boolean onStartNestedScroll(...) {
return false;
}
// 当上面的 onStartNestedScroll 返回 true,会回到改方法
public void onNestedScrollAccepted(...) {}
// 当 CoordinatorLayout 内有 NestedScrollView 停止滑动的时候回调
public void onStopNestedScroll(...) {}
// 当 CoordinatorLayout 内有 NestedScrollView 滑动过程中的回调
public void onNestedScroll(...) {}
// 在 onNestedScroll 之前回调该方法
public void onNestedPreScroll(...) {}
// 是否滑动的惯性事件处理
public boolean onNestedFling(...) {
return false;
}
// 滑动的惯性事件开始的回调
public boolean onNestedPreFling(...) {
return false;
}
依赖 View 相关的方法
这也是我们在自定义 Behavior 时一定会重写的方法
// 当前 View 是否依赖指定 View 进行变化
public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) {
return false;
}
// 依赖的 View(dependency)变化时的回调
public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
return false;
}
// 依赖的 View 被移除时的回调
public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) {
}
下面是 behavior 重要的方法
public static abstract class Behavior<V extends View>{
public Behavior() {
}
public Behavior(Context context, AttributeSet attrs) {}
// 给 Behavior 设置 LayoutParams 时会调用
public void onAttachedToLayoutParams(@NonNull CoordinatorLayout.LayoutParams params) {}
// LayoutParams 移除时会调用
public void onDetachedFromLayoutParams() {}
// 是否拦截 CoordinatorLayout 发过了的点击事件
public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
return false;
}
// 接收 CoordinatorLayout 发过了的点击事件
public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
return false;
}
// 设置 Behavior 所在 View 之外的 View 的蒙层颜色
@ColorInt
public int getScrimColor(CoordinatorLayout parent, V child) {
return Color.BLACK;
}
// 设置蒙层的透明度
@FloatRange(from = 0, to = 1)
public float getScrimOpacity(CoordinatorLayout parent, V child) {
return 0.f;
}
// 是否对 Behavior 绑定 View 下面的 View 的进行交互,
// 默认是是根据 getScrimOpacity 的透明度决定的
public boolean blocksInteractionBelow(CoordinatorLayout parent, V child) {
return getScrimOpacity(parent, child) > 0.f;
}
// 当前 View 是否依赖指定 View 进行变化
public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) {
return false;
}
// 依赖的 View(dependency)变化时的回调
public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
return false;
}
// 依赖的 View 被移除时的回调
public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) {
}
// CoordinatorLayout 在测量时会回调这个方法
public boolean onMeasureChild(CoordinatorLayout parent, V child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
return false;
}
// CoordinatorLayout 在布局时会回调这个方法
public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
return false;
}
// 设置 tag
public static void setTag(View child, Object tag) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
lp.mBehaviorTag = tag;
}
// 获取 tag
public static Object getTag(View child) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
return lp.mBehaviorTag;
}
// 当 CoordinatorLayout 内有 NestedScrollView 开始滑动的时候回调
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View directTargetChild, @NonNull View target,
@ScrollAxis int axes, @NestedScrollType int type) {
if (type == ViewCompat.TYPE_TOUCH) {
return onStartNestedScroll(coordinatorLayout, child, directTargetChild,
target, axes);
}
return false;
}
// 当上面的 onStartNestedScroll 返回 true,会回到改方法
public void onNestedScrollAccepted(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View directTargetChild, @NonNull View target,
@ScrollAxis int axes, @NestedScrollType int type) {
if (type == ViewCompat.TYPE_TOUCH) {
onNestedScrollAccepted(coordinatorLayout, child, directTargetChild,
target, axes);
}
}
// 当 CoordinatorLayout 内有 NestedScrollView 停止滑动的时候回调
public void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View target, @NestedScrollType int type) {
if (type == ViewCompat.TYPE_TOUCH) {
onStopNestedScroll(coordinatorLayout, child, target);
}
}
// 当 CoordinatorLayout 内有 NestedScrollView 滑动过程中的回调
public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child,
@NonNull View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, @NestedScrollType int type) {
if (type == ViewCompat.TYPE_TOUCH) {
onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed);
}
}
// 在 onNestedScroll 之前回调该方法
public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View target, int dx, int dy, @NonNull int[] consumed,
@NestedScrollType int type) {
if (type == ViewCompat.TYPE_TOUCH) {
onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
}
}
// 是否滑动的惯性事件处理
public boolean onNestedFling(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View target, float velocityX, float velocityY,
boolean consumed) {
return false;
}
// 滑动的惯性事件开始的回调
public boolean onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View target, float velocityX, float velocityY) {
return false;
}
// 如果给CoordinatorLayout设置了fitSystemWindow=true,可以在这里自己处理WindowInsetsCompat
@NonNull
public WindowInsetsCompat onApplyWindowInsets(CoordinatorLayout coordinatorLayout,
V child, WindowInsetsCompat insets) {
return insets;
}
// 在CoordinatorLayout的requestChildRectangleOnScreen()中被调用
public boolean onRequestChildRectangleOnScreen(CoordinatorLayout coordinatorLayout,
V child, Rect rectangle, boolean immediate) {
return false;
}
}
CoordinatorLayout 与 Behavior 的关系
了解 CoordinatorLayout 与 Behavior 的关系,需要进入 CoordinatorLayout 的源码里面去看看。
CoordinatorLayout#onMeasure
CoordinatorLayout#onMeasure 方法里面会调用 Behavior.#onMeasureChild
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 准备工作, 用 DFS 深度遍历算法,对依赖的 View 进行排序
prepareChildren();
// 根据情况添加或者移除OnPreDrawListener
ensurePreDrawListener();
...
final boolean applyInsets = mLastInsets != null && ViewCompat.getFitsSystemWindows(this);
final int childCount = mDependencySortedChildren.size();
for (int i = 0; i < childCount; i++) {
// 从排好续的集合中依次获取Child Vie
final View child = mDependencySortedChildren.get(i);
if (child.getVisibility() == GONE) {
// If the child is GONE, skip...
continue;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
....
// 处理 FitsSystemWindows
int childWidthMeasureSpec = widthMeasureSpec;
int childHeightMeasureSpec = heightMeasureSpec;
if (applyInsets && !ViewCompat.getFitsSystemWindows(child)) {
// We're set to handle insets but this child isn't, so we will measure the
// child as if there are no insets
...
}
// Behavior
final Behavior b = lp.getBehavior();
// Behavior.onMeasureChild 方法调用
if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0)) {
onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0);
}
...
}
...
// 设置 width,height
setMeasuredDimension(width, height);
}
CoordinatorLayout#onLayout
CoordinatorLayout#onLayout 方法里面会调用 Behavior#onLayoutChild
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int layoutDirection = ViewCompat.getLayoutDirection(this);
final int childCount = mDependencySortedChildren.size();
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
if (child.getVisibility() == GONE) {
// If the child is GONE, skip...
continue;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Behavior behavior = lp.getBehavior();
// 调用 Behavior#onLayoutChild, 如果 behavior 不进行测量,则需要 CoordinatorLayout 自己测量
if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
onLayoutChild(child, layoutDirection);
}
}
}
public void onLayoutChild(View child, int layoutDirection) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp.checkAnchorChanged()) {
throw new IllegalStateException("An anchor may not be changed after CoordinatorLayout"
+ " measurement begins before layout is complete.");
}
if (lp.mAnchorView != null) {
// 设置了 AnchorView 时候的布局, app:layout_anchor
layoutChildWithAnchor(child, lp.mAnchorView, layoutDirection);
} else if (lp.keyline >= 0) {
// 设置了 keyline 的布局, app:layout_keyline
layoutChildWithKeyline(child, lp.keyline, layoutDirection);
} else {
// 正常测量,像 FrameLayout 那样布局
layoutChild(child, layoutDirection);
}
}
CoordinatorLayout#onLayout
CoordinatorLayout#onLayout 方法会调用 Behavior#onInterceptTouchEvent 方法询问是否要拦截,也会调用 调用 Behavior#onTouchEvent 处理点击事件
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
MotionEvent cancelEvent = null;
final int action = ev.getActionMasked();
// Make sure we reset in case we had missed a previous important event.
// 重置 Behavior
if (action == MotionEvent.ACTION_DOWN) {
resetTouchBehaviors(true);
}
// performIntercept 进行判断是否拦截
final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT);
if (cancelEvent != null) {
cancelEvent.recycle();
}
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
resetTouchBehaviors(true);
}
return intercepted;
}
private boolean performIntercept(MotionEvent ev, final int type) {
boolean intercepted = false;
boolean newBlock = false;
MotionEvent cancelEvent = null;
final int action = ev.getActionMasked();
final List<View> topmostChildList = mTempList1;
// View 按照 z-order 进行排序
getTopSortedChildren(topmostChildList);
// Let topmost child views inspect first
final int childCount = topmostChildList.size();
for (int i = 0; i < childCount; i++) {
final View child = topmostChildList.get(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Behavior b = lp.getBehavior();
if ((intercepted || newBlock) && action != MotionEvent.ACTION_DOWN) {
// Cancel all behaviors beneath the one that intercepted.
// If the event is "down" then we don't have anything to cancel yet.
// 如果一个 View 把事件拦截了,则把重叠于它之下的 behavior 事件都取消
if (b != null) {
if (cancelEvent == null) {
final long now = SystemClock.uptimeMillis();
cancelEvent = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
}
switch (type) {
case TYPE_ON_INTERCEPT:
b.onInterceptTouchEvent(this, child, cancelEvent);
break;
case TYPE_ON_TOUCH:
b.onTouchEvent(this, child, cancelEvent);
break;
}
}
continue;
}
if (!intercepted && b != null) {
switch (type) {
case TYPE_ON_INTERCEPT:
// 调用 Behavior#onInterceptTouchEvent 方法询问是否要拦截
intercepted = b.onInterceptTouchEvent(this, child, ev);
break;
case TYPE_ON_TOUCH:
// 调用 Behavior#onTouchEvent 处理点击事件
intercepted = b.onTouchEvent(this, child, ev);
break;
}
if (intercepted) {
mBehaviorTouchView = child;
}
}
...
}
topmostChildList.clear();
return intercepted;
}
CoordinatorLayout#onTouchEvent
在 CoordinatorLayout#onTouchEvent 方法里面会调用 Behavior#onTouchEvent 是否进行拦截判断
@Override
public boolean onTouchEvent(MotionEvent ev) {
boolean handled = false;
boolean cancelSuper = false;
MotionEvent cancelEvent = null;
final int action = ev.getActionMasked();
// 如果是 Behavior 拦截了,则把点击事件教给 Behavior#onTouchEvent 处理
if (mBehaviorTouchView != null || (cancelSuper = performIntercept(ev, TYPE_ON_TOUCH))) {
// Safe since performIntercept guarantees that
// mBehaviorTouchView != null if it returns true
final LayoutParams lp = (LayoutParams) mBehaviorTouchView.getLayoutParams();
final Behavior b = lp.getBehavior();
if (b != null) {
handled = b.onTouchEvent(this, mBehaviorTouchView, ev);
}
}
// Keep the super implementation correct
// 如果是 Behaivor 不拦截,则交给 CoordinatorLayout 的父类处理,按照事件传递流程处理
if (mBehaviorTouchView == null) {
handled |= super.onTouchEvent(ev);
} else if (cancelSuper) {
if (cancelEvent == null) {
final long now = SystemClock.uptimeMillis();
cancelEvent = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
}
super.onTouchEvent(cancelEvent);
}
if (!handled && action == MotionEvent.ACTION_DOWN) {
}
if (cancelEvent != null) {
cancelEvent.recycle();
}
// ACTION_UP 事件重置 Behavior
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
resetTouchBehaviors(false);
}
return handled;
}
CoordinatorLayout 的嵌套滑动
CoordinatorLayout 是实现了 NestedScrollingParent2,NestedScrollingParent2 继承了 NestedScrollingParent
NestedScrollingParent2.java 的接口
// This interface should be implemented by ViewGroup
// subclasses that wish to support scrolling operations
// delegated by a nested child view
public interface NestedScrollingParent2 extends NestedScrollingParent {
boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes,
@NestedScrollType int type);
boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes,
@NestedScrollType int type);
void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes,
@NestedScrollType int type);
void onStopNestedScroll(@NonNull View target, @NestedScrollType int type);
void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, @NestedScrollType int type);
void onNestedPreScroll(@NonNull View target, int dx, int dy, @Nullable int[] consumed,
@NestedScrollType int type)
}
在 CoordinatorLayout 滑动的时候,实现这些滑动方法,都会直接传入到 Behavior 中对应的方法,例如 CoordinatorLayout#onStartNestedScroll 会分发到 Behavior#onStartNestedScroll
@Override
public boolean onStartNestedScroll(View child, View target, int axes, int type) {
boolean handled = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
if (view.getVisibility() == View.GONE) {
// If it's GONE, don't dispatch
continue;
}
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
// 分发到 Behavior#onStartNestedScroll
final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child,
target, axes, type);
handled |= accepted;
lp.setNestedScrollAccepted(type, accepted);
} else {
lp.setNestedScrollAccepted(type, false);
}
}
return handled;
}
关于嵌套滑动,在后续文章中再细说。
当手指滑动的范围在 AppBarLayout 内滑动,CoordinatorLayout 会通过 NestScrollView 的 Behavior 分发事件,让 NestScrollView 产生滑动;
当手指滑动的范围在 NestScrollView 内滑动,CoordinatorLayout 会通过 AppBarLayout 的 Behavior 分发事件,让 AppBarLayout 产生滑;AppBarLayout 默认的 AppBarLayout#Behavior, 不需要显式指定 Behavior。
AppBarLayout 滑动监听
自定义 AppBarStateChangeListener, 同时定义 AppBarLayout 的状态 EXPANDED 展开,COLLAPSED 折叠 和 IDLE 转态
public abstract class AppBarStateChangeListener implements AppBarLayout.OnOffsetChangedListener {
private State mCurrentState = State.IDLE;
@Override
public final void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
if (verticalOffset == 0) {
if (mCurrentState != State.EXPANDED) {
onStateChanged(appBarLayout, State.EXPANDED, verticalOffset);
}
mCurrentState = State.EXPANDED;
} else if (Math.abs(verticalOffset) >= appBarLayout.getTotalScrollRange()) {
if (mCurrentState != State.COLLAPSED) {
onStateChanged(appBarLayout, State.COLLAPSED, verticalOffset);
}
mCurrentState = State.COLLAPSED;
} else {
if (mCurrentState != State.IDLE) {
onStateChanged(appBarLayout, State.IDLE, verticalOffset);
}
mCurrentState = State.IDLE;
}
}
public abstract void onStateChanged(AppBarLayout appBarLayout, State state, int verticalOffset);
}
enum State {
EXPANDED,
COLLAPSED,
IDLE
}
然后在 AppBarLayout 中设置监听
appBarLayout.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
@Override
public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
// 进行操作
});
这个回调是在 AppBarLayout$Behavior#setHeaderTopBottomOffset 方法中回调的。当 AppBarLayout 在进行滑动的时候,会调用 setHeaderTopBottomOffset 方法,在 setHeaderTopBottomOffset在中
@Override
int setHeaderTopBottomOffset(CoordinatorLayout coordinatorLayout,
AppBarLayout appBarLayout, int newOffset, int minOffset, int maxOffset) {
final int curOffset = getTopBottomOffsetForScrollingSibling();
int consumed = 0;
if (minOffset != 0 && curOffset >= minOffset && curOffset <= maxOffset) {
// If we have some scrolling range, and we're currently within the min and max
// offsets, calculate a new offset
newOffset = MathUtils.clamp(newOffset, minOffset, maxOffset);
if (curOffset != newOffset) {
final int interpolatedOffset = appBarLayout.hasChildWithInterpolator()
? interpolateOffset(appBarLayout, newOffset)
: newOffset;
final boolean offsetChanged = setTopAndBottomOffset(interpolatedOffset);
// Update how much dy we have consumed
consumed = curOffset - newOffset;
// Update the stored sibling offset
mOffsetDelta = newOffset - interpolatedOffset;
if (!offsetChanged && appBarLayout.hasChildWithInterpolator()) {
// If the offset hasn't changed and we're using an interpolated scroll
// then we need to keep any dependent views updated. CoL will do this for
// us when we move, but we need to do it manually when we don't (as an
// interpolated scroll may finish early).
coordinatorLayout.dispatchDependentViewsChanged(appBarLayout);
}
// 分发到监听
appBarLayout.dispatchOffsetUpdates(getTopAndBottomOffset());
// Update the AppBarLayout's drawable state (for any elevation changes)
updateAppBarLayoutDrawableState(coordinatorLayout, appBarLayout, newOffset,
newOffset < curOffset ? -1 : 1, false);
}
} else {
// Reset the offset delta
mOffsetDelta = 0;
}
return consumed;
}
}
// 分发到所有的监听器
void dispatchOffsetUpdates(int offset) {
if (mListeners != null) {
for (int i = 0, z = mListeners.size(); i < z; i++) {
final OnOffsetChangedListener listener = mListeners.get(i);
if (listener != null) {
listener.onOffsetChanged(this, offset);
}
}
}
注意事项
- 如果 CoordinatorLayout 里面使用了 ScrollView 或者 ViewPager, 需要指定 Bebeavior
<android.support.design.widget.CoordinatorLayout...>
<android.support.design.widget.AppBarLayout...>
<android.support.design.widget.CollapsingToolbarLayout...>
<!-- your collapsed view -->
<View.../>
<android.support.v7.widget.Toolbar.../>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<!-- 需要指定 appbar_scrolling_view_behavior -->
<android.support.v4.view.ViewPager
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</android.support.design.widget.CoordinatorLayout>