先從最簡單的看起
<android.support.design.widget.AppBarLayout
android:fitsSystemWindows="true"
android:layout_width="match_parent"
android:layout_height="256dp">
<android.support.design.widget.CollapsingToolbarLayout
android:id="@+id/collapsingToolbarLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_scrollFlags="scroll">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:minHeight="?attr/actionBarSize"
app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
效果以下所示,toolbar可以舒展
AppBarLayout里有個接口,叫做OnOffsetChangedListener,如果AppBarLayout滑動了就會觸發里面的回調onOffsetChanged
/**
* Interface definition for a callback to be invoked when an {@link AppBarLayout}'s vertical
* offset changes.
*/
public interface OnOffsetChangedListener {
/**
* Called when the {@link AppBarLayout}'s layout offset has been changed. This allows
* child views to implement custom behavior based on the offset (for instance pinning a
* view at a certain y value).
*
* @param appBarLayout the {@link AppBarLayout} which offset has changed
* @param verticalOffset the vertical offset for the parent {@link AppBarLayout}, in px
*/
void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset);
}
AppBarLayout滑動的時候會調用setHeaderTopBottomOffset,里面調用dispatchOffsetUpdates(appBarLayout),以下所示,會把移動的消息發給listeners
private void dispatchOffsetUpdates(AppBarLayout layout) {
final List<OnOffsetChangedListener> listeners = layout.mListeners;
// Iterate backwards through the list so that most recently added listeners
// get the first chance to decide
for (int i = 0, z = listeners.size(); i < z; i++) {
final OnOffsetChangedListener listener = listeners.get(i);
if (listener != null) {
listener.onOffsetChanged(layout, getTopAndBottomOffset());
}
}
}
而CollapsingToolbarLayout在onAttachedToWindow的時候加入
((AppBarLayout) parent).addOnOffsetChangedListener(mOnOffsetChangedListener);
其實就是注冊了1個listener,AppBarLayout滑動了,CollapsingToolbarLayout 內的mOnOffsetChangedListener就會知道并作出相應動畫,這里其實就是文字的縮小。主要代碼在CollapsingTextHelper內,主要就是根據當前AppBarLayout的offset來修改mScale。
此時CollapsingToolbarLayout和AppBarLayout1樣大小,包括statusbar 大小為256dp
mTotalScrollRange=range - getTopInset()=256dp-S=609
mDownPreScrollRange 0
mDownScrollRange =256dp=672
我曾以為mTotalScrollRange= mDownPreScrollRange+ mDownScrollRange,這里不成立了。
我試著把mDownScrollRange強行改成609,滑動仍然正常,由于下滑的時候offset是在變大的,所以不會到⑹72.
再看設置了exitUntilCollapsed 以后,exitUntilCollapsed意思就是滑出直到折疊狀態,即滑出的時候最多到折疊狀態,沒法完全滑出
exitUntilCollapsed會改變上滑的范圍,上滑的范圍就是mTotalScrollRange,
private int getUpNestedPreScrollRange() {
return getTotalScrollRange();
}
public final int getTotalScrollRange() {
if (mTotalScrollRange != INVALID_SCROLL_RANGE) {
return mTotalScrollRange;
}
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.mScrollFlags;
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 ((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
//減去標記了SCROLL_FLAG_EXIT_UNTIL_COLLAPSED的child的最小高度
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 mTotalScrollRange = Math.max(0, range - getTopInset());
}
由上可知,在算mTotalScrollRange的時候會減去標記了SCROLL_FLAG_EXIT_UNTIL_COLLAPSED的child的最小高度,這里就是減去CollapsingToolbarLayout的minHeight,但是又有個問題,CollapsingToolbarLayout我們并沒有設置minHeight,我們只是在Toolbar里設置了minHeight。CollapsingToolbarLayout在onLayout的時候會調用setMinimumHeight(getHeightWithMargins(mToolbar));,這樣CollapsingToolbarLayout就有了minHeight,這個值是toolbar的height加上下margin,跟Toolbar的minHeight沒關系。試試看把Toolbar的minHeight去掉,絕不影響。所以此時mTotalScrollRange會減去CollapsingToolbarLayout的minHeight,這樣上滑的時候就會留出1部份高度,不全部滑出,留出的高度就是CollapsingToolbarLayout的minHeight=toolbar高度+上下margin
Toolbar設置app:layout_collapseMode=”pin”
這竟然可以定住toolbar,和appbarlayout的設計又有點不符合,appbarlayout是認為底部可以存在不滑動的區域,但頂部不可以,那這里怎樣做到的,實際上,他是隨著appbarlayout往上offset了,然后他自己以后又offset了1次,使得toolbar相對屏幕的位置不變。實際上,假定appbarlayout往上滑了11,那末appbarlayout的offset是⑴1,此時我們又offset了1次,把toolbar相對CollapsingToolbarLayout的offset設置為11,這樣toolbar相對屏幕就相當于沒變化,核心代碼在android.support.design.widget.CollapsingToolbarLayout.OffsetUpdateListener#onOffsetChanged
//CollapsingToolbarLayout.OffsetUpdateListener#onOffsetChanged
for (int i = 0, z = getChildCount(); i < z; i++) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final ViewOffsetHelper offsetHelper = getViewOffsetHelper(child);
switch (lp.mCollapseMode) {
case LayoutParams.COLLAPSE_MODE_PIN:
//調劑offset使得child看起來不動
if (getHeight() - insetTop + verticalOffset >= child.getHeight()) {
offsetHelper.setTopAndBottomOffset(-verticalOffset);
}
break;
case LayoutParams.COLLAPSE_MODE_PARALLAX:
//調劑offset實現視差滑動
offsetHelper.setTopAndBottomOffset(
Math.round(-verticalOffset * lp.mParallaxMult));
break;
}
}
上滑的進程中,背景由圖片變成純色,狀態欄也由透明變成純色,這個變化是甚么時候呢?這個臨界點由getScrimTriggerOffset決定
//CollapsingToolbarLayout.OffsetUpdateListener#onOffsetChanged
// Show or hide the scrims if needed
if (mContentScrim != null || mStatusBarScrim != null) {
setScrimsShown(getHeight() + verticalOffset < getScrimTriggerOffset() + insetTop);
}
/**
* The additional offset used to define when to trigger the scrim visibility change.
*/
final int getScrimTriggerOffset() {
return 2 * ViewCompat.getMinimumHeight(this);
}
截了個圖,大概是這個位置,圖片可見部份的高度就是getScrimTriggerOffset的值,下1瞬間圖片就會變成純色。實際上就是在上面蓋了個mContentScrim,mContentScrim就是1個ColorDrawable ,色彩為colorPrimary.因而可知修改CollapsingToolbarLayout的minHeight就能夠修改變化瞬間的位置
變成純色的同時,狀態欄也從透明變成有色彩colorPrimaryDark。mScrimAlpha由1變成255,狀態欄變成純色,實際上是在狀態欄的位置畫了1個純色的矩形,由mStatusBarScrim來實現,mStatusBarScrim的色彩也能夠指定。
if (mStatusBarScrim != null && mScrimAlpha > 0) {
final int topInset = mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0;
if (topInset > 0) {
mStatusBarScrim.setBounds(0, -mCurrentOffset, getWidth(),
topInset - mCurrentOffset);
mStatusBarScrim.mutate().setAlpha(mScrimAlpha);
mStatusBarScrim.draw(canvas);
}
}
對應case3
給ImageView加上fitSystemWindow,為何就有效果,讓初始態覆蓋狀態欄
不加的話,ImageView會被設置1個offset(insetTop),讓他處于狀態欄下邊,如果加了,那就進不到L7,所以可以覆蓋狀態欄。
//android.support.design.widget.CollapsingToolbarLayout#onLayout
if (mLastInsets != null && !ViewCompat.getFitsSystemWindows(child)) {
final int insetTop = mLastInsets.getSystemWindowInsetTop();
if (child.getTop() < insetTop) {
// If the child isn't set to fit system windows but is drawing within the inset
// offset it down
ViewCompat.offsetTopAndBottom(child, insetTop);
}
}
再來看看enterAlwaysCollapsed有甚么用
我拿CollapsImageActivity3試了1下,app:layout_scrollFlags=”scroll|enterAlways|enterAlwaysCollapsed” 發現有bug,下滑pre的時候顯示以下,應當是下滑的范圍(mDownPreScrollRange)少算了個statubar。暫時沒有甚么好的解決方案,看google后期會不會修復這個bug還是放棄enterAlwaysCollapsed。