8CollapsingToolbarLayout源码分析
纯色Toolbar滑动
最简单代码
先从最简单的看起
<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);
其实就是注册了一个listener,AppBarLayout滑动了,CollapsingToolbarLayout 内的mOnOffsetChangedListener就会知道并作出相应动画,这里其实就是文字的缩小。主要代码在CollapsingTextHelper内,主要就是根据当前AppBarLayout的offset来修改mScale。
此时CollapsingToolbarLayout和AppBarLayout一样大小,包含statusbar 大小为256dp
mTotalScrollRange=range - getTopInset()=256dp-S=609
mDownPreScrollRange 0
mDownScrollRange =256dp=672
我曾经以为mTotalScrollRange= mDownPreScrollRange+ mDownScrollRange,这里不成立了。
我试着把mDownScrollRange强行改为609,滑动依然正常,因为下滑的时候offset是在变大的,所以不会到-672.
exitUntilCollapsed
再看设置了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,这样上滑的时候就会留出一部分高度,不全部滑出,留出的高度就是CollapsingToolbarLayout的minHeight=toolbar高度+上下margin
定住toolbar
Toolbar设置app:layout_collapseMode=”pin”
这居然可以定住toolbar,和appbarlayout的设计又有点不符合,appbarlayout是认为底部可以存在不滑动的区域,但顶部不可以,那这里怎么做到的,实际上,他是随着appbarlayout往上offset了,然后他自己之后又offset了一次,使得toolbar相对屏幕的位置不变。实际上,假设appbarlayout往上滑了11,那么appbarlayout的offset是-11,此时我们又offset了一次,把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;
}
}
带背景图toolbar
对应case1
上滑的过程中,背景由图片变成纯色,状态栏也由透明变为纯色,这个变化是什么时候呢?这个临界点由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的值,下一瞬间图片就会变成纯色。实际上就是在上面盖了个mContentScrim,mContentScrim就是一个ColorDrawable ,颜色为colorPrimary.由此可见修改CollapsingToolbarLayout的minHeight就可以修改变化瞬间的位置
变成纯色的同时,状态栏也从透明变为有颜色colorPrimaryDark。mScrimAlpha由1变为255,状态栏变为纯色,实际上是在状态栏的位置画了一个纯色的矩形,由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会被设置一个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
再来看看enterAlwaysCollapsed有什么用
我拿CollapsImageActivity3试了一下,app:layout_scrollFlags=”scroll|enterAlways|enterAlwaysCollapsed” 发现有bug,下滑pre的时候显示如下,应该是下滑的范围(mDownPreScrollRange)少算了个statubar。暂时没有什么好的解决方案,看google后期会不会修复这个bug还是放弃enterAlwaysCollapsed。