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>