上一篇中已经讲解了CoordinatorLayout、AppBarLayout、CollapsingToolbarLayout之间的关系,这一篇探索一下CollapsingToolbarLayout内部是怎么实现的,不熟悉CoordinatorLayout、AppBarLayout、CollapsingToolbarLayout之间的关系的请先看上一篇文章android5.0协调布局CoordinatorLayout(第一篇CoordinatorLayout、AppBarLayout、CollapsingToolbarLayout之间的关系详解)原理java
首先看一下CollapsingToolbarLayout的一些属性说明,首先下面这些属性是要写在CollapsingToolbarLayout中的android
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
如今先以一个简单的例子为入口点,先看一下效果图算法
标题部分以下布局代码canvas
<android.support.design.widget.CollapsingToolbarLayout android:layout_width="match_parent" android:layout_height="match_parent" app:layout_scrollFlags="scroll|exitUntilCollapsed" app1:collapsedTitleTextAppearance="@color/abc_primary_text_material_light" app1:contentScrim="@android:color/holo_blue_light" app1:title="6666" > <!-- 视差值越小滚动越明显 --> <ImageView android:layout_width="match_parent" android:layout_height="match_parent" app:layout_collapseMode="parallax" app:layout_collapseParallaxMultiplier="0.7" android:scaleType="centerCrop" android:src="@drawable/tulips2" /> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:layout_collapseMode="pin" app:navigationIcon="@drawable/abc_ic_ab_back_mtrl_am_alpha" /> </android.support.design.widget.CollapsingToolbarLayout>
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,若是布局里面没有设置的话, // 默认 <item name="expandedTitleMargin">32dp</item> 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()); } }
接下来看一下画图的方法app
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画上了背景图,当图片刚要消失的时候会出现ide
是在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);post
这个方法,当剩余的可见高度为标题栏的二倍的时候,图片将会被上面的背景色覆盖,从而出现咱们看到的效果,
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效果实现的机制介绍完毕。