上一篇中已经讲解了CoordinatorLayout、AppBarLayout、CollapsingToolbarLayout之间的关系,这一篇探索一下CollapsingToolbarLayout内部是怎么实现的,不熟悉CoordinatorLayout、AppBarLayout、CollapsingToolbarLayout之间的关系的请先看上一篇文章android5.0协调布局CoordinatorLayout(第一篇CoordinatorLayout、AppBarLayout、CollapsingToolbarLayout之间的关系详解)原理
首先看一下CollapsingToolbarLayout的一些属性说明,首先下面这些属性是要写在CollapsingToolbarLayout中的
app1:collapsedTitleGravity="center_horizontal":关闭后标题的位置
app1:contentScrim:完全折叠后的背景颜色
app1:collapsedTitleTextAppearance:关闭后的标题颜色,存在两个颜色值渐变效果
app1:statusBarScrim 折叠完成后状态栏的颜色
app1:expandedTitleTextAppearance 展开后的tittle的颜色
app1:expandedTitleGravity展开后的标题位置
app1:expandedTitleMargin展开后的标题偏移量
app1:title:设置的标题名字
app:toolbarId:ToolBar的id必须设置,它通过id获取对象操作ToolBar
app1:titleEnabled 标题是否存在
app1:layout_collapseMode 折叠模式有两个值
pin - 设置为这个模式时,当CollapsingToolbarLayout完全收缩后,Toolbar还可以保留在屏幕上。
parallax - 设置为这个模式时,在内容滚动时,CollapsingToolbarLayout中的View(比如ImageView)也可以同时滚动,实现视差滚动效果,通常和layout_collapseParallaxMultiplier(设置视差因子)搭配使用。
layout_collapseParallaxMultiplier(视差因子) - 设置视差滚动因子,值为:0~1
现在先以一个简单的例子为入口点,先看一下效果图
标题部分如下布局代码
public CollapsingToolbarLayout(Context context, AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
ThemeUtils.checkAppCompatTheme(context);
mCollapsingTextHelper = new CollapsingTextHelper(this);
mCollapsingTextHelper
.setTextSizeInterpolator(AnimationUtils.DECELERATE_INTERPOLATOR);
// 设置了默认stytle,如果布局里面没有设置的话,
// 默认 - 32dp
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.CollapsingToolbarLayout, defStyleAttr,
R.style.Widget_Design_CollapsingToolbar);
// 获得展开后的tittle的位置expandedTitleGravity,默认在左边和底部
mCollapsingTextHelper.setExpandedTextGravity(a.getInt(
R.styleable.CollapsingToolbarLayout_expandedTitleGravity,
GravityCompat.START | Gravity.BOTTOM));
// 收缩后的tittle位置默认在左边,垂直居中
mCollapsingTextHelper.setCollapsedTextGravity(a.getInt(
R.styleable.CollapsingToolbarLayout_collapsedTitleGravity,
GravityCompat.START | Gravity.CENTER_VERTICAL));
// 扩展后tittle的偏移量
mExpandedMarginLeft = mExpandedMarginTop = mExpandedMarginRight = mExpandedMarginBottom = a
.getDimensionPixelSize(
R.styleable.CollapsingToolbarLayout_expandedTitleMargin,
0);
final boolean isRtl = ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL;
if (a.hasValue(R.styleable.CollapsingToolbarLayout_expandedTitleMarginStart)) {
final int marginStart = a
.getDimensionPixelSize(
R.styleable.CollapsingToolbarLayout_expandedTitleMarginStart,
0);
if (isRtl) {
mExpandedMarginRight = marginStart;
} else {
mExpandedMarginLeft = marginStart;
}
}
if (a.hasValue(R.styleable.CollapsingToolbarLayout_expandedTitleMarginEnd)) {
final int marginEnd = a.getDimensionPixelSize(
R.styleable.CollapsingToolbarLayout_expandedTitleMarginEnd,
0);
if (isRtl) {
mExpandedMarginLeft = marginEnd;
} else {
mExpandedMarginRight = marginEnd;
}
}
if (a.hasValue(R.styleable.CollapsingToolbarLayout_expandedTitleMarginTop)) {
mExpandedMarginTop = a.getDimensionPixelSize(
R.styleable.CollapsingToolbarLayout_expandedTitleMarginTop,
0);
}
if (a.hasValue(R.styleable.CollapsingToolbarLayout_expandedTitleMarginBottom)) {
mExpandedMarginBottom = a
.getDimensionPixelSize(
R.styleable.CollapsingToolbarLayout_expandedTitleMarginBottom,
0);
}
// 收缩后的tittle是否显示,默认显示
mCollapsingTitleEnabled = a.getBoolean(
R.styleable.CollapsingToolbarLayout_titleEnabled, true);
setTitle(a.getText(R.styleable.CollapsingToolbarLayout_title));
// First load the default text appearances
mCollapsingTextHelper
.setExpandedTextAppearance(R.style.TextAppearance_Design_CollapsingToolbar_Expanded);
mCollapsingTextHelper
.setCollapsedTextAppearance(R.style.TextAppearance_AppCompat_Widget_ActionBar_Title);
// Now overlay any custom text appearances
// 展开后的tittle的颜色设置
if (a.hasValue(R.styleable.CollapsingToolbarLayout_expandedTitleTextAppearance)) {
mCollapsingTextHelper
.setExpandedTextAppearance(a
.getResourceId(
R.styleable.CollapsingToolbarLayout_expandedTitleTextAppearance,
0));
}
// 收缩后的tittle颜色
if (a.hasValue(R.styleable.CollapsingToolbarLayout_collapsedTitleTextAppearance)) {
mCollapsingTextHelper
.setCollapsedTextAppearance(a
.getResourceId(
R.styleable.CollapsingToolbarLayout_collapsedTitleTextAppearance,
0));
}
setContentScrim(a
.getDrawable(R.styleable.CollapsingToolbarLayout_contentScrim));
setStatusBarScrim(a
.getDrawable(R.styleable.CollapsingToolbarLayout_statusBarScrim));
mToolbarId = a.getResourceId(
R.styleable.CollapsingToolbarLayout_toolbarId, -1);
a.recycle();
setWillNotDraw(false);
/**
* 设置处理状态栏或导航栏的监听回调
*/
ViewCompat.setOnApplyWindowInsetsListener(this,
new android.support.v4.view.OnApplyWindowInsetsListener() {
@Override
public WindowInsetsCompat onApplyWindowInsets(View v,
WindowInsetsCompat insets) {
mLastInsets = insets;
requestLayout();
return insets.consumeSystemWindowInsets();
}
});
}
app1:collapsedTitleTextAppearance="@color/abc_primary_text_material_light"
app1:contentScrim="@android:color/holo_blue_light"
app1:title="6666"
先看看tittle属性做了什么,获取完tittle属性的值调用了这个方法
setTitle(a.getText(R.styleable.CollapsingToolbarLayout_title));
public void setTitle(@Nullable CharSequence title) {
mCollapsingTextHelper.setText(title);
}
这个方法将我们的tittle值交给了mCollapsingTextHelper这个对象处理,基本上很多属性都是交给CollapsingTextHelper类处理的,暂且叫它属性帮助类,获取完属性之后呢,当然是测量了
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
ensureToolbar();
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
private void ensureToolbar() {
if (!mRefreshToolbar) {
return;
}
Toolbar fallback = null, selected = null;
for (int i = 0, count = getChildCount(); i < count; i++) {
final View child = getChildAt(i);
if (child instanceof Toolbar) {
if (mToolbarId != -1) {
// There's a toolbar id set so try and find it...
if (mToolbarId == child.getId()) {
// We found the primary Toolbar, use it
selected = (Toolbar) child;
break;
}
if (fallback == null) {
// We'll record the first Toolbar as our fallback
fallback = (Toolbar) child;
}
} else {
// We don't have a id to check for so just use the first we
// come across
selected = (Toolbar) child;
break;
}
}
}
if (selected == null) {
// If we didn't find a primary Toolbar, use the fallback
selected = fallback;
}
mToolbar = selected;
updateDummyView();
mRefreshToolbar = false;
}
mToolbarId = a.getResourceId(
R.styleable.CollapsingToolbarLayout_toolbarId, -1);
private void updateDummyView() {
if (!mCollapsingTitleEnabled && mDummyView != null) {
// If we have a dummy view and we have our title disabled, remove it
// from its parent
final ViewParent parent = mDummyView.getParent();
if (parent instanceof ViewGroup) {
((ViewGroup) parent).removeView(mDummyView);
}
}
if (mCollapsingTitleEnabled && mToolbar != null) {
if (mDummyView == null) {
mDummyView = new View(getContext());
}
if (mDummyView.getParent() == null) {
mToolbar.addView(mDummyView, LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT);
}
}
}
protected void onLayout(boolean changed, int left, int top, int right,
int bottom) {
super.onLayout(changed, left, top, right, bottom);
// Update the collapsed bounds by getting it's transformed bounds. This
// needs to be done
// before the children are offset below
if (mCollapsingTitleEnabled && mDummyView != null) {
// We only draw the title if the dummy view is being displayed
// (Toolbar removes
// views if there is no space)
mDrawCollapsingTitle = mDummyView.isShown();
if (mDrawCollapsingTitle) {
ViewGroupUtils.getDescendantRect(this, mDummyView, mTmpRect);
//设置收缩后的Rect
mCollapsingTextHelper.setCollapsedBounds(mTmpRect.left, bottom
- mTmpRect.height(), mTmpRect.right, bottom);
// 设置展开后的Rect
mCollapsingTextHelper.setExpandedBounds(mExpandedMarginLeft,
mTmpRect.bottom + mExpandedMarginTop, right - left
- mExpandedMarginRight, bottom - top
- mExpandedMarginBottom);
// Now recalculate using the new bounds
mCollapsingTextHelper.recalculate();
}
}
//此处省略对状态栏栏距离的处理……
// Finally, set our minimum height to enable proper AppBarLayout
// collapsing
//如果没有设置tittle属性默认设置mToolbar的tittle
if (mToolbar != null) {
if (mCollapsingTitleEnabled
&& TextUtils.isEmpty(mCollapsingTextHelper.getText())) {
// If we do not currently have a title, try and grab it from the
// Toolbar
mCollapsingTextHelper.setText(mToolbar.getTitle());
}
//设置最小高度为toolbar的高度,也就是说自己设置的会失效
setMinimumHeight(mToolbar.getHeight());
}
}
接下来看一下画图的方法
public void draw(Canvas canvas) {
super.draw(canvas);
// If we don't have a toolbar, the scrim will be not be drawn in
// drawChild() below.
// Instead, we draw it here, before our collapsing text.
ensureToolbar();
// 到达多大位置的时候画背景
if (mToolbar == null && mContentScrim != null && mScrimAlpha > 0) {
mContentScrim.mutate().setAlpha(mScrimAlpha);
mContentScrim.draw(canvas);
}
// Let the collapsing text helper draw it's text
// 画折叠后的标题
if (mCollapsingTitleEnabled && mDrawCollapsingTitle) {
mCollapsingTextHelper.draw(canvas);
}
// 最后满足条件的话画标题栏的背景色
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);
}
}
// Now draw the status bar scrim
}
@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
// This is a little weird. Our scrim needs to be behind the Toolbar (if
// it is present),
// but in front of any other children which are behind it. To do this we
// intercept the
// drawChild() call, and draw our scrim first when drawing the toolbar
ensureToolbar();
if (child == mToolbar && mContentScrim != null && mScrimAlpha > 0) {
mContentScrim.mutate().setAlpha(mScrimAlpha);
mContentScrim.draw(canvas);
}
// Carry on drawing the child...
return super.drawChild(canvas, child, drawingTime);
}
最后标题栏变得那个颜色就是通过它画的
也就是图中所示的颜色就相当于为ToolBar画上了背景图,当图片刚要消失的时候会出现
是在draw方法里的这个判断画的
// 到达多大位置的时候画背景
if (mToolbar == null && mContentScrim != null && mScrimAlpha > 0) {
mContentScrim.mutate().setAlpha(mScrimAlpha);
mContentScrim.draw(canvas);
}
最后通过CollapsingTextHelper.draw(canvas)方法将标题画到屏幕上。通过上一篇的讲述CollapsingToolbarLayout的效果的变化都是根据AppBarLayout移动后的回调方法通知而进行的子View响应状态的变化,也就是说CollapsingToolbarLayout向AppBarLayout注册了OnOffsetChangedListener 监听方法,每次AppBarLayout的每次移动都会告诉
CollapsingToolbarLayout我现在的top或bottom在哪,由于是朝上移动的,那么实际上是移动了AppBarLayout的距离父View的top位置,当然这个top位置一直是负值,也就是下面方法的verticalOffset变量
private class OffsetUpdateListener implements
AppBarLayout.OnOffsetChangedListener {
@Override
public void onOffsetChanged(AppBarLayout layout, int verticalOffset) {
// AppBarLayout的top或bottom的偏移量
mCurrentOffset = verticalOffset;
final int insetTop = mLastInsets != null ? mLastInsets
.getSystemWindowInsetTop() : 0;
final int scrollRange = layout.getTotalScrollRange();
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:
// 父View朝上移动的时候,当前view的大小减去偏移量仍然大于悬浮的view的时候,那么
if (getHeight() - insetTop + verticalOffset >= child
.getHeight()) {
// 父View移动多少,子View反向移动多少,看到的效果就是子View的位置视角看起来没有改变
offsetHelper.setTopAndBottomOffset(-verticalOffset);
}
break;
// 如果是视差滚动
case LayoutParams.COLLAPSE_MODE_PARALLAX:
// mParallaxMult=1的朝下移动的效果越明显,越小的话越接近fuView的移动位置,所以产生视差效果
// 反向移动子View
offsetHelper.setTopAndBottomOffset(Math
.round(-verticalOffset * lp.mParallaxMult));
break;
}
}
// Show or hide the scrims if needed
if (mContentScrim != null || mStatusBarScrim != null) {
// 让contentScrim显示
setScrimsShown(getHeight() + verticalOffset < getScrimTriggerOffset()
+ insetTop);
}
if (mStatusBarScrim != null && insetTop > 0) {
ViewCompat
.postInvalidateOnAnimation(CollapsingToolbarLayout.this);
}
// Update the collapsing text's fraction
final int expandRange = getHeight()
- ViewCompat.getMinimumHeight(CollapsingToolbarLayout.this)
- insetTop;
// 不断改变偏移量
mCollapsingTextHelper.setExpansionFraction(Math.abs(verticalOffset)
/ (float) expandRange);
if (Math.abs(verticalOffset) == scrollRange) {
// If we have some pinned children, and we're offset to only
// show those views,
// we want to be elevate
ViewCompat.setElevation(layout, layout.getTargetElevation());
} else {
// Otherwise, we're inline with the content
ViewCompat.setElevation(layout, 0f);
}
}
}
这个方法遍历子View判断collapseMode属性的值,布局当中我们设置的ToolBar是pin方法,那么每次fuView朝上走的话,子View就朝下走,那么眼睛看起来,这个子View就像没有动一样,但是前提条件得满足,有足够的剩余空间容纳这个子View,如果属性设置为parallax,那么子View和CollapsingToolbarLayout走的相反的位置的时候需要乘以视差值,也就是设置的这个值 app:layout_collapseParallaxMultiplier="0.7",如果父View朝上走了100,那么子View就朝下走70,看起来,子View只朝上走了30,那么人眼看起来就形成了视差的效果。那么接下来根据偏移量计算上面说的过渡的颜色是否可以画,也就是
setScrimsShown(getHeight() + verticalOffset < getScrimTriggerOffset()
+ insetTop);
这个方法,当剩余的可见高度为标题栏的二倍的时候,图片将会被上面的背景色覆盖,从而出现我们看到的效果,
private void setScrimAlpha(int alpha) {
if (alpha != mScrimAlpha) {
final Drawable contentScrim = mContentScrim;
if (contentScrim != null && mToolbar != null) {
ViewCompat.postInvalidateOnAnimation(mToolbar);
}
mScrimAlpha = alpha;
ViewCompat.postInvalidateOnAnimation(CollapsingToolbarLayout.this);
}
}
然后这个方法调用ViewCompat.postInvalidateOnAnimation方法进行重新绘制,最终调用属性帮助类 setExpansionFraction方法将当前的进行的百分比设置进去void setExpansionFraction(float fraction) {
fraction = MathUtils.constrain(fraction, 0f, 1f);
if (fraction != mExpandedFraction) {
mExpandedFraction = fraction;
calculateCurrentOffsets();
}
}
将变量因子设置给
mExpandedFraction
,然后调用
calculateCurrentOffsets方法
private void calculateOffsets(final float fraction) {
interpolateBounds(fraction);
// 得到当前该画的x位置
mCurrentDrawX = lerp(mExpandedDrawX, mCollapsedDrawX, fraction,
mPositionInterpolator);
// 当前该画的y位置
mCurrentDrawY = lerp(mExpandedDrawY, mCollapsedDrawY, fraction,
mPositionInterpolator);
// 得到字体size的大小
setInterpolatedTextSize(lerp(mExpandedTextSize, mCollapsedTextSize,
fraction, mTextSizeInterpolator));
if (mCollapsedTextColor != mExpandedTextColor) {
// If the collapsed and expanded text colors are different, blend
// them based on the
// fraction
mTextPaint.setColor(blendColors(mExpandedTextColor,
mCollapsedTextColor, fraction));
} else {
mTextPaint.setColor(mCollapsedTextColor);
}
//如果设置了阴影属性将画上阴影,此处我们并没有画隐影
mTextPaint.setShadowLayer(
lerp(mExpandedShadowRadius, mCollapsedShadowRadius, fraction,
null),
lerp(mExpandedShadowDx, mCollapsedShadowDx, fraction, null),
lerp(mExpandedShadowDy, mCollapsedShadowDy, fraction, null),
blendColors(mExpandedShadowColor, mCollapsedShadowColor,
fraction));
//最后执行重画逻辑
ViewCompat.postInvalidateOnAnimation(mView);
}
位置计算,字体大小插值器如下
private static float lerp(float startValue, float endValue, float fraction,
Interpolator interpolator) {
if (interpolator != null) {
fraction = interpolator.getInterpolation(fraction);
}
return AnimationUtils.lerp(startValue, endValue, fraction);
}
static float lerp(float startValue, float endValue, float fraction) {
return startValue + (fraction * (endValue - startValue));
}
private static int blendColors(int color1, int color2, float ratio) {
final float inverseRatio = 1f - ratio;
float a = (Color.alpha(color1) * inverseRatio)
+ (Color.alpha(color2) * ratio);
float r = (Color.red(color1) * inverseRatio)
+ (Color.red(color2) * ratio);
float g = (Color.green(color1) * inverseRatio)
+ (Color.green(color2) * ratio);
float b = (Color.blue(color1) * inverseRatio)
+ (Color.blue(color2) * ratio);
return Color.argb((int) a, (int) r, (int) g, (int) b);
}
app1:collapsedTitleTextAppearance属性或者app1:expandedTitleTextAppearance,只要设置其一就可以产生效果,这一些列动作完成后就剩下画标题了
public void draw(Canvas canvas) {
final int saveCount = canvas.save();
if (mTextToDraw != null && mDrawTitle) {
float x = mCurrentDrawX;
float y = mCurrentDrawY;
final boolean drawTexture = mUseTexture
&& mExpandedTitleTexture != null;
final float ascent;
final float descent;
// Update the TextPaint to the current text size
mTextPaint.setTextSize(mCurrentTextSize);
if (drawTexture) {
ascent = mTextureAscent * mScale;
descent = mTextureDescent * mScale;
} else {
ascent = mTextPaint.ascent() * mScale;
descent = mTextPaint.descent() * mScale;
}
if (DEBUG_DRAW) {
// Just a debug tool, which drawn a Magneta rect in the text
// bounds
canvas.drawRect(mCurrentBounds.left, y + ascent,
mCurrentBounds.right, y + descent, DEBUG_DRAW_PAINT);
}
if (drawTexture) {
y += ascent;
}
if (mScale != 1f) {
canvas.scale(mScale, mScale, x, y);
}
if (drawTexture) {
// If we should use a texture, draw it instead of text
canvas.drawBitmap(mExpandedTitleTexture, x, y, mTexturePaint);
} else {
// 画标题
canvas.drawText(mTextToDraw, 0, mTextToDraw.length(), x, y,
mTextPaint);
}
}
canvas.restoreToCount(saveCount);
}
如下方法进行计算
private void calculateUsingTextSize(final float textSize) {
if (mText == null)
return;
final float availableWidth;
final float newTextSize;
boolean updateDrawText = false;
/**
* 假如当前textSize接近mCollapsedTextSize缩放值mScale=1
*/
if (isClose(textSize, mCollapsedTextSize)) {
availableWidth = mCollapsedBounds.width();
newTextSize = mCollapsedTextSize;
mScale = 1f;
if (mCurrentTypeface != mCollapsedTypeface) {
mCurrentTypeface = mCollapsedTypeface;
updateDrawText = true;
}
} else {
availableWidth = mExpandedBounds.width();
newTextSize = mExpandedTextSize;
if (mCurrentTypeface != mExpandedTypeface) {
mCurrentTypeface = mExpandedTypeface;
updateDrawText = true;
}
// 当前textSize接近mExpandedTextSize的时候缩放值也等于1,否则缩放值等于textSize /
// mExpandedTextSize
if (isClose(textSize, mExpandedTextSize)) {
// If we're close to the expanded text size, snap to it and use
// a scale of 1
mScale = 1f;
} else {
// Else, we'll scale down from the expanded text size
mScale = textSize / mExpandedTextSize;
}
}
if (availableWidth > 0) {
updateDrawText = (mCurrentTextSize != newTextSize)
|| mBoundsChanged || updateDrawText;
mCurrentTextSize = newTextSize;
mBoundsChanged = false;
}
if (mTextToDraw == null || updateDrawText) {
mTextPaint.setTextSize(mCurrentTextSize);
mTextPaint.setTypeface(mCurrentTypeface);
// If we don't currently have text to draw, or the text size has
// changed, ellipsize...
final CharSequence title = TextUtils.ellipsize(mText, mTextPaint,
availableWidth, TextUtils.TruncateAt.END);
if (!TextUtils.equals(title, mTextToDraw)) {
mTextToDraw = title;
mIsRtl = calculateIsRtl(mTextToDraw);
}
}
}
CollapsingToolbarLayout通过这两个方法分别设置了mExpandedTextSize和mCollapsedTextSize,这里采用的是用默认的样式赋的值
mCollapsingTextHelper
.setExpandedTextAppearance(R.style.TextAppearance_Design_CollapsingToolbar_Expanded);
mCollapsingTextHelper
.setCollapsedTextAppearance(R.style.TextAppearance_AppCompat_Widget_ActionBar_Title);
仔细观察上面的动态图的话发现tittle在上移的时候是不断缩放的,直到接近收缩的字体停止缩放,以上源码正好证明了这个情况,也就是说越朝上滑动,当前的字体越小,在没
接近收缩时,比值会越来越小,那么mScale 会越来越小,那么画布就会有缩放效果。
到此CollapsingToolbarLayout效果实现的机制介绍完毕。