/* * Copyright (C) 2015 The Android Open Source Project * Copyright (C) 2015 Thomas Robert Altstidl * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.tr4android.support.extension.widget; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Build; import android.support.annotation.ColorInt; import android.support.annotation.DrawableRes; import android.support.annotation.IntDef; import android.support.annotation.Nullable; import android.support.annotation.StyleRes; import android.support.design.widget.AppBarLayout; import android.support.v4.content.ContextCompat; import android.support.v4.view.GravityCompat; import android.support.v4.view.ViewCompat; import android.support.v4.view.WindowInsetsCompat; import android.support.v7.widget.Toolbar; import android.text.TextUtils; import android.util.AttributeSet; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; import android.widget.FrameLayout; import android.widget.ImageView; import com.tr4android.appcompat.extension.R; import com.tr4android.support.extension.animation.AnimationUtils; import com.tr4android.support.extension.animation.ValueAnimatorCompat; import com.tr4android.support.extension.internal.ViewOffsetHelper; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * FlexibleToolbarLayout is a wrapper for {@link Toolbar} which implements a collapsing app bar. * It is designed to be used as a direct child of a {@link AppBarLayout}. * FlexibleToolbarLayout contains the following features: * <p/> * <h3>Collapsing title</h3> * A title which is larger when the layout is fully visible but collapses and becomes smaller as * the layout is scrolled off screen. You can set the title to display via * {@link #setTitle(CharSequence)}. The title appearance can be tweaked via the * {@code titleCollapsedTextAppearance} and {@code titleExpandedTextAppearance} attributes. * <p/> * <h3>Collapsing subtitle</h3> * A subtitle which can be larger when the layout is fully visible but collapses and becomes smaller as * the layout is scrolled off screen. You can set the subtitle to display via * {@link #setSubtitle(CharSequence)}. The subtitle appearance can be tweaked via the * {@code subtitleCollapsedTextAppearance} and {@code subtitleExpandedTextAppearance} attributes. * <p/> * <h3>Collapsing title</h3> * An icon which is larger when the layout is fully visible but collapses and becomes smaller as * the layout is scrolled off screen. You can set the icon to display via * {@link #setIcon(Drawable)}. The icon size can be tweaked via the * {@code iconCollapsedSize} and {@code iconExpandedSize} attributes. * <p/> * <h3>Content scrim</h3> * A full-bleed scrim which is show or hidden when the scroll position has hit a certain threshold. * You can change this via {@link #setContentScrim(Drawable)}. * <p/> * <h3>Status bar scrim</h3> * A scrim which is show or hidden behind the status bar when the scroll position has hit a certain * threshold. You can change this via {@link #setStatusBarScrim(Drawable)}. This only works * on {@link Build.VERSION_CODES#LOLLIPOP LOLLIPOP} devices when we set to fit system windows. * <p/> * <h3>Parallax scrolling children</h3> * Child views can opt to be scrolled within this layout in a parallax fashion. * See {@link LayoutParams#COLLAPSE_MODE_PARALLAX} and * {@link LayoutParams#setParallaxMultiplier(float)}. * <p/> * <h3>Pinned position children</h3> * Child views can opt to be pinned in space globally. This is useful when implementing a * collapsing as it allows the {@link Toolbar} to be fixed in place even though this layout is * moving. See {@link LayoutParams#COLLAPSE_MODE_PIN}. * * @attr ref android.support.design.R.styleable#FlexibleToolbarLayout_titleCollapsedTextAppearance * @attr ref android.support.design.R.styleable#FlexibleToolbarLayout_titleExpandedTextAppearance * @attr ref android.support.design.R.styleable#FlexibleToolbarLayout_subtitleCollapsedTextAppearance * @attr ref android.support.design.R.styleable#FlexibleToolbarLayout_subtitleExpandedTextAppearance * @attr ref android.support.design.R.styleable#FlexibleToolbarLayout_iconCollapsedSize * @attr ref android.support.design.R.styleable#FlexibleToolbarLayout_iconExpandedSize * @attr ref android.support.design.R.styleable#FlexibleToolbarLayout_contentScrimColor * @attr ref android.support.design.R.styleable#FlexibleToolbarLayout_expandedMargin * @attr ref android.support.design.R.styleable#FlexibleToolbarLayout_expandedMarginStart * @attr ref android.support.design.R.styleable#FlexibleToolbarLayout_expandedMarginEnd * @attr ref android.support.design.R.styleable#FlexibleToolbarLayout_expandedMarginBottom * @attr ref android.support.design.R.styleable#FlexibleToolbarLayout_statusBarScrimColor * @attr ref android.support.design.R.styleable#FlexibleToolbarLayout_toolbarRefId */ public class FlexibleToolbarLayout extends FrameLayout { private static final int SCRIM_ANIMATION_DURATION = 600; private boolean mRefreshToolbar = true; private int mToolbarId; private Toolbar mToolbar; private View mDummyView; private int mExpandedMarginLeft; private int mExpandedMarginTop; private int mExpandedMarginRight; private int mExpandedMarginBottom; private final Rect mTmpRect = new Rect(); private final Rect mExpandedBounds = new Rect(); private boolean mDrawTitles; private int mSpaceTitleSubtitle; private int mSpaceIconTitles; private final CollapsingTextHelper mTitleCollapsingTextHelper; private boolean mCollapsingTitleEnabled; private final CollapsingTextHelper mSubtitleCollapsingTextHelper; private boolean mCollapsingSubtitleEnabled; private final CollapsingDrawableHelper mIconCollapsingHelper; private boolean mCollapsingIconEnabled; private Drawable mContentScrim; private Drawable mStatusBarScrim; private int mScrimAlpha; private boolean mScrimsAreShown; private ValueAnimatorCompat mScrimAnimator; private AppBarLayout.OnOffsetChangedListener mOnOffsetChangedListener; private int mCurrentOffset; private WindowInsetsCompat mLastInsets; public FlexibleToolbarLayout(Context context) { this(context, null); } public FlexibleToolbarLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public FlexibleToolbarLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mTitleCollapsingTextHelper = new CollapsingTextHelper(this); mTitleCollapsingTextHelper.setTextSizeInterpolator(AnimationUtils.LINEAR_INTERPOLATOR); mSubtitleCollapsingTextHelper = new CollapsingTextHelper(this); mSubtitleCollapsingTextHelper.setTextSizeInterpolator(AnimationUtils.LINEAR_INTERPOLATOR); mIconCollapsingHelper = new CollapsingDrawableHelper(this); mIconCollapsingHelper.setIconSizeInterpolator(AnimationUtils.LINEAR_INTERPOLATOR); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FlexibleToolbarLayout, defStyleAttr, R.style.Widget_Design_FlexibleToolbarLayout); int expandedVerticalGravity = a.getInt(R.styleable.FlexibleToolbarLayout_expandedGravity, Gravity.CENTER_VERTICAL); mTitleCollapsingTextHelper.setExpandedTextGravity(GravityCompat.START | expandedVerticalGravity); mTitleCollapsingTextHelper.setCollapsedTextGravity(GravityCompat.START | Gravity.CENTER_VERTICAL); mSubtitleCollapsingTextHelper.setExpandedTextGravity(GravityCompat.START | expandedVerticalGravity); mSubtitleCollapsingTextHelper.setCollapsedTextGravity(GravityCompat.START | Gravity.CENTER_VERTICAL); mExpandedMarginLeft = mExpandedMarginTop = mExpandedMarginRight = mExpandedMarginBottom = a.getDimensionPixelSize(R.styleable.FlexibleToolbarLayout_expandedMargin, 0); final boolean isRtl = ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL; if (a.hasValue(R.styleable.FlexibleToolbarLayout_expandedMarginStart)) { final int marginStart = a.getDimensionPixelSize( R.styleable.FlexibleToolbarLayout_expandedMarginStart, 0); if (isRtl) { mExpandedMarginRight = marginStart; } else { mExpandedMarginLeft = marginStart; } } if (a.hasValue(R.styleable.FlexibleToolbarLayout_expandedMarginEnd)) { final int marginEnd = a.getDimensionPixelSize( R.styleable.FlexibleToolbarLayout_expandedMarginEnd, 0); if (isRtl) { mExpandedMarginLeft = marginEnd; } else { mExpandedMarginRight = marginEnd; } } if (a.hasValue(R.styleable.FlexibleToolbarLayout_expandedMarginTop)) { mExpandedMarginTop = a.getDimensionPixelSize( R.styleable.FlexibleToolbarLayout_expandedMarginTop, 0); } if (a.hasValue(R.styleable.FlexibleToolbarLayout_expandedMarginBottom)) { mExpandedMarginBottom = a.getDimensionPixelSize( R.styleable.FlexibleToolbarLayout_expandedMarginBottom, 0); } mCollapsingTitleEnabled = a.getBoolean( R.styleable.FlexibleToolbarLayout_titleFlexibleEnabled, true); setTitle(a.getText(R.styleable.FlexibleToolbarLayout_title)); mCollapsingSubtitleEnabled = a.getBoolean( R.styleable.FlexibleToolbarLayout_subtitleFlexibleEnabled, true); setSubtitle(a.getText(R.styleable.FlexibleToolbarLayout_subtitle)); mCollapsingIconEnabled = a.getBoolean( R.styleable.FlexibleToolbarLayout_iconFlexibleEnabled, true); setIcon(a.getDrawable(R.styleable.FlexibleToolbarLayout_icon)); // First load the default text appearances mTitleCollapsingTextHelper.setExpandedTextAppearance( R.style.TextAppearance_Design_FlexibleToolbarLayout_ExpandedTitle); mTitleCollapsingTextHelper.setCollapsedTextAppearance( R.style.TextAppearance_Design_FlexibleToolbarLayout_CollapsedTitle); mSubtitleCollapsingTextHelper.setExpandedTextAppearance( R.style.TextAppearance_Design_FlexibleToolbarLayout_Subtitle); mSubtitleCollapsingTextHelper.setCollapsedTextAppearance( R.style.TextAppearance_Design_FlexibleToolbarLayout_Subtitle); // Now overlay any custom text appearances if (a.hasValue(R.styleable.FlexibleToolbarLayout_titleExpandedTextAppearance)) { mTitleCollapsingTextHelper.setExpandedTextAppearance( a.getResourceId( R.styleable.FlexibleToolbarLayout_titleExpandedTextAppearance, 0)); } if (a.hasValue(R.styleable.FlexibleToolbarLayout_titleCollapsedTextAppearance)) { mTitleCollapsingTextHelper.setCollapsedTextAppearance( a.getResourceId( R.styleable.FlexibleToolbarLayout_titleCollapsedTextAppearance, 0)); } if (a.hasValue(R.styleable.FlexibleToolbarLayout_subtitleExpandedTextAppearance)) { mSubtitleCollapsingTextHelper.setExpandedTextAppearance( a.getResourceId( R.styleable.FlexibleToolbarLayout_subtitleExpandedTextAppearance, 0)); } if (a.hasValue(R.styleable.FlexibleToolbarLayout_subtitleCollapsedTextAppearance)) { mSubtitleCollapsingTextHelper.setCollapsedTextAppearance( a.getResourceId( R.styleable.FlexibleToolbarLayout_subtitleCollapsedTextAppearance, 0)); } // Load the icon sizes mIconCollapsingHelper.setCollapsedIconSize(a.getDimensionPixelSize(R.styleable.FlexibleToolbarLayout_iconCollapsedSize, 0)); mIconCollapsingHelper.setExpandedIconSize(a.getDimensionPixelSize(R.styleable.FlexibleToolbarLayout_iconExpandedSize, 0)); mSpaceTitleSubtitle = a.getDimensionPixelSize(R.styleable.FlexibleToolbarLayout_spaceTitleSubtitle, 0); mSpaceIconTitles = a.getDimensionPixelSize(R.styleable.FlexibleToolbarLayout_spaceIconTitles, 0); setContentScrim(a.getDrawable(R.styleable.FlexibleToolbarLayout_contentScrimColor)); setStatusBarScrim(a.getDrawable(R.styleable.FlexibleToolbarLayout_statusBarScrimColor)); mToolbarId = a.getResourceId(R.styleable.FlexibleToolbarLayout_toolbarRefId, -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(); } }); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); // Add an OnOffsetChangedListener if possible final ViewParent parent = getParent(); if (parent instanceof AppBarLayout) { if (mOnOffsetChangedListener == null) { mOnOffsetChangedListener = new OffsetUpdateListener(); } ((AppBarLayout) parent).addOnOffsetChangedListener(mOnOffsetChangedListener); } } @Override protected void onDetachedFromWindow() { // Remove our OnOffsetChangedListener if possible and it exists final ViewParent parent = getParent(); if (mOnOffsetChangedListener != null && parent instanceof AppBarLayout) { ((AppBarLayout) parent).removeOnOffsetChangedListener(mOnOffsetChangedListener); } super.onDetachedFromWindow(); } @Override 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 && mDrawTitles) { mTitleCollapsingTextHelper.draw(canvas); } if (mCollapsingSubtitleEnabled && mDrawTitles) { mSubtitleCollapsingTextHelper.draw(canvas); } // Let the collapsing drawable helper draw it's drawable if (mCollapsingIconEnabled && mDrawTitles) { mIconCollapsingHelper.draw(canvas); } // Now draw the status bar scrim 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); } } } @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); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); if (mContentScrim != null) { mContentScrim.setBounds(0, 0, w, h); } } 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; } 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); } } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { ensureToolbar(); super.onMeasure(widthMeasureSpec, heightMeasureSpec); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); // We only draw the title and subtitle if the dummy view is being displayed (Toolbar removes // views if there is no space) mDrawTitles = mDummyView != null && mDummyView.isShown(); // This is the amount of horizontal offset needed for the title and subtitle int horizontalOffsetCollapsed = mCollapsingIconEnabled ? (int) mIconCollapsingHelper.getCollapsedIconSize() + mSpaceIconTitles : 0; int horizontalOffsetExpanded = mCollapsingIconEnabled ? (int) mIconCollapsingHelper.getExpandedIconSize() + mSpaceIconTitles : 0; // These are the expanded bounds needed for the icon, title and subtitle int expandedIconSize = (int) mIconCollapsingHelper.getExpandedIconSize(); mExpandedBounds.set(mExpandedMarginLeft, mCollapsingIconEnabled ? (bottom - top - mExpandedMarginBottom - expandedIconSize) : (mTmpRect.bottom + mExpandedMarginTop), right - left - mExpandedMarginRight, bottom - top - mExpandedMarginBottom); // Update the collapsed bounds by getting it's transformed bounds. This needs to be done // before the children are offset below if (mCollapsingTitleEnabled && mDrawTitles) { ViewGroupUtils.getDescendantRect(this, mDummyView, mTmpRect); // Update the collapsed bounds mTitleCollapsingTextHelper.setCollapsedBounds(mTmpRect.left + horizontalOffsetCollapsed, bottom - mTmpRect.height(), mTmpRect.right, bottom); // Update the expanded bounds mTitleCollapsingTextHelper.setExpandedBounds( mExpandedBounds.left + horizontalOffsetExpanded, mExpandedBounds.top, mExpandedBounds.right, mExpandedBounds.bottom); // Adjust the offset when subtitle is present if (mCollapsingSubtitleEnabled) { mTitleCollapsingTextHelper.setCollapsedTextOffsetBottom( mSpaceTitleSubtitle + mSubtitleCollapsingTextHelper.getCollapsedTextHeight()); mTitleCollapsingTextHelper.setExpandedTextOffsetBottom( mSpaceTitleSubtitle + mSubtitleCollapsingTextHelper.getExpandedTextHeight()); } // Now recalculate using the new bounds mTitleCollapsingTextHelper.recalculate(); } if (mCollapsingSubtitleEnabled && mDrawTitles) { ViewGroupUtils.getDescendantRect(this, mDummyView, mTmpRect); // Update the collapsed bounds mSubtitleCollapsingTextHelper.setCollapsedBounds(mTmpRect.left + horizontalOffsetCollapsed, bottom - mTmpRect.height(), mTmpRect.right, bottom); // Update the expanded bounds mSubtitleCollapsingTextHelper.setExpandedBounds( mExpandedBounds.left + horizontalOffsetExpanded, mExpandedBounds.top, mExpandedBounds.right, mExpandedBounds.bottom); // Adjust the offset when title is present if (mCollapsingTitleEnabled) { mSubtitleCollapsingTextHelper.setCollapsedTextOffsetTop( mSpaceTitleSubtitle + mTitleCollapsingTextHelper.getCollapsedTextHeight()); mSubtitleCollapsingTextHelper.setExpandedTextOffsetTop( mSpaceTitleSubtitle + mTitleCollapsingTextHelper.getExpandedTextHeight()); } // Now recalculate using the new bounds mSubtitleCollapsingTextHelper.recalculate(); } if (mCollapsingIconEnabled && mDrawTitles) { ViewGroupUtils.getDescendantRect(this, mDummyView, mTmpRect); // Update the collapsed bounds int collapsedIconSize = (int) mIconCollapsingHelper.getCollapsedIconSize(); int collapsedTop = bottom - mTmpRect.height() + Math.round((mTmpRect.height() - collapsedIconSize) / 2f); mIconCollapsingHelper.setCollapsedBounds(mTmpRect.left, collapsedTop, mTmpRect.left + collapsedIconSize, collapsedTop + collapsedIconSize); // Update the expanded bounds mIconCollapsingHelper.setExpandedBounds( mExpandedBounds.left, mExpandedBounds.top, mExpandedMarginLeft + expandedIconSize, mExpandedBounds.bottom); // Now recalculate using the new bounds mIconCollapsingHelper.recalculate(); } // Update our child view offset helpers for (int i = 0, z = getChildCount(); i < z; i++) { final View child = getChildAt(i); 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 child.offsetTopAndBottom(insetTop); } } getViewOffsetHelper(child).onViewLayout(); } // Finally, set our minimum height to enable proper AppBarLayout collapsing if (mToolbar != null) { if (mCollapsingTitleEnabled && TextUtils.isEmpty(mTitleCollapsingTextHelper.getText())) { // If we do not currently have a title, try and grab it from the Toolbar mTitleCollapsingTextHelper.setText(mToolbar.getTitle()); } setMinimumHeight(mToolbar.getHeight()); } } private static ViewOffsetHelper getViewOffsetHelper(View view) { ViewOffsetHelper offsetHelper = (ViewOffsetHelper) view.getTag(R.id.view_offset_helper); if (offsetHelper == null) { offsetHelper = new ViewOffsetHelper(view); view.setTag(R.id.view_offset_helper, offsetHelper); } return offsetHelper; } /** * Collapses the {@link FlexibleToolbarLayout}. * This will only have an effect if the {@link FlexibleToolbarLayout} * is used as a child of {@link AppBarLayout}. */ public void collapse() { // Passes call to AppBarLayout if possible ViewParent parent = getParent(); if (parent instanceof AppBarLayout) { ((AppBarLayout) parent).setExpanded(false); } } /** * Collapses the FlexibleToolbarLayout. * This will only have an effect if the {@link FlexibleToolbarLayout} * is used as a child of {@link AppBarLayout}. * * @param animate Whether or not the collapse should be animated */ public void collapse(boolean animate) { // Passes call to AppBarLayout if possible ViewParent parent = getParent(); if (parent instanceof AppBarLayout) { ((AppBarLayout) parent).setExpanded(false, animate); } } /** * Expands the {@link FlexibleToolbarLayout}. * This will only have an effect if the {@link FlexibleToolbarLayout} * is used as a child of {@link AppBarLayout}. */ public void expand() { // Passes call to AppBarLayout if possible ViewParent parent = getParent(); if (parent instanceof AppBarLayout) { ((AppBarLayout) parent).setExpanded(true); } } /** * Expands the FlexibleToolbarLayout. * This will only have an effect if the {@link FlexibleToolbarLayout} * is used as a child of {@link AppBarLayout}. * * @param animate Whether or not the expansion should be animated */ public void expand(boolean animate) { // Passes call to AppBarLayout if possible ViewParent parent = getParent(); if (parent instanceof AppBarLayout) { ((AppBarLayout) parent).setExpanded(true, animate); } } /** * Sets the title to be displayed by this view, if enabled. * * @attr ref R.styleable#FlexibleToolbarLayout_title * @see #setTitleEnabled(boolean) * @see #getTitle() */ public void setTitle(@Nullable CharSequence title) { mTitleCollapsingTextHelper.setText(title); } /** * Returns the title currently being displayed by this view. If the title is not enabled, then * this will return {@code null}. * * @attr ref R.styleable#FlexibleToolbarLayout_title */ @Nullable public CharSequence getTitle() { return mCollapsingTitleEnabled ? mTitleCollapsingTextHelper.getText() : null; } /** * Sets whether this view should display its own title. * <p/> * <p>The title displayed by this view will shrink and grow based on the scroll offset.</p> * * @attr ref R.styleable#FlexibleToolbarLayout_titleFlexibleEnabled * @see #setTitle(CharSequence) * @see #isTitleEnabled() */ public void setTitleEnabled(boolean enabled) { if (enabled != mCollapsingTitleEnabled) { mCollapsingTitleEnabled = enabled; updateDummyView(); requestLayout(); } } /** * Returns whether this view is currently displaying its own title. * * @attr ref R.styleable#FlexibleToolbarLayout_titleFlexibleEnabled * @see #setTitleEnabled(boolean) */ public boolean isTitleEnabled() { return mCollapsingTitleEnabled; } /** * Sets the subtitle to be displayed by this view, if enabled. * * @attr ref R.styleable#FlexibleToolbarLayout_subtitle * @see #setSubtitleEnabled(boolean) * @see #getSubtitle() */ public void setSubtitle(@Nullable CharSequence subtitle) { mSubtitleCollapsingTextHelper.setText(subtitle); } /** * Returns the subtitle currently being displayed by this view. If the subtitle is not enabled, then * this will return {@code null}. * * @attr ref R.styleable#FlexibleToolbarLayout_subtitle */ @Nullable public CharSequence getSubtitle() { return mCollapsingSubtitleEnabled ? mSubtitleCollapsingTextHelper.getText() : null; } /** * Sets whether this view should display its own subtitle. * <p/> * <p>The subtitle displayed by this view will shrink and grow based on the scroll offset.</p> * * @attr ref R.styleable#FlexibleToolbarLayout_subtitleFlexibleEnabled * @see #setSubtitle(CharSequence) * @see #isSubtitleEnabled() */ public void setSubtitleEnabled(boolean enabled) { if (enabled != mCollapsingSubtitleEnabled) { mCollapsingSubtitleEnabled = enabled; updateDummyView(); requestLayout(); } } /** * Returns whether this view is currently displaying its own subtitle. * * @attr ref R.styleable#FlexibleToolbarLayout_subtitleFlexibleEnabled * @see #setSubtitleEnabled(boolean) */ public boolean isSubtitleEnabled() { return mCollapsingSubtitleEnabled; } /** * Sets the icon to be displayed by this view, if enabled. * * @attr ref R.styleable#FlexibleToolbarLayout_icon * @see #setIconEnabled(boolean) * @see #getIcon() */ public void setIcon(@Nullable Drawable icon) { mIconCollapsingHelper.setDrawable(icon); } /** * Returns the icon currently being displayed by this view. If the icon is not enabled, then * this will return {@code null}. * * @attr ref R.styleable#FlexibleToolbarLayout_icon */ @Nullable public Drawable getIcon() { return mCollapsingIconEnabled ? mIconCollapsingHelper.getDrawable() : null; } /** * Sets whether this view should display its own icon. * <p/> * <p>The icon displayed by this view will shrink and grow based on the scroll offset.</p> * * @attr ref R.styleable#FlexibleToolbarLayout_iconFlexibleEnabled * @see #setIcon(Drawable) * @see #isIconEnabled() */ public void setIconEnabled(boolean enabled) { if (enabled != mCollapsingIconEnabled) { mCollapsingIconEnabled = enabled; requestLayout(); } } /** * Returns whether this view is currently displaying its own icon. * * @attr ref R.styleable#FlexibleToolbarLayout_iconFlexibleEnabled * @see #setIconEnabled(boolean) */ public boolean isIconEnabled() { return mCollapsingIconEnabled; } /** * Set whether the content scrim and/or status bar scrim should be shown or not. Any change * in the vertical scroll may overwrite this value. Any visibility change will be animated if * this view has already been laid out. * * @param shown whether the scrims should be shown * @see #getStatusBarScrim() * @see #getContentScrim() */ public void setScrimsShown(boolean shown) { setScrimsShown(shown, ViewCompat.isLaidOut(this) && !isInEditMode()); } /** * Set whether the content scrim and/or status bar scrim should be shown or not. Any change * in the vertical scroll may overwrite this value. * * @param shown whether the scrims should be shown * @param animate whether to animate the visibility change * @see #getStatusBarScrim() * @see #getContentScrim() */ public void setScrimsShown(boolean shown, boolean animate) { if (mScrimsAreShown != shown) { if (animate) { animateScrim(shown ? 0xFF : 0x0); } else { setScrimAlpha(shown ? 0xFF : 0x0); } mScrimsAreShown = shown; } } private void animateScrim(int targetAlpha) { ensureToolbar(); if (mScrimAnimator == null) { mScrimAnimator = AnimationUtils.createAnimator(); mScrimAnimator.setDuration(SCRIM_ANIMATION_DURATION); mScrimAnimator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR); mScrimAnimator.setUpdateListener(new ValueAnimatorCompat.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimatorCompat animator) { setScrimAlpha(animator.getAnimatedIntValue()); } }); } else if (mScrimAnimator.isRunning()) { mScrimAnimator.cancel(); } mScrimAnimator.setIntValues(mScrimAlpha, targetAlpha); mScrimAnimator.start(); } private void setScrimAlpha(int alpha) { if (alpha != mScrimAlpha) { final Drawable contentScrim = mContentScrim; if (contentScrim != null && mToolbar != null) { ViewCompat.postInvalidateOnAnimation(mToolbar); } mScrimAlpha = alpha; ViewCompat.postInvalidateOnAnimation(FlexibleToolbarLayout.this); } } /** * Set the drawable to use for the content scrim from resources. Providing null will disable * the scrim functionality. * * @param drawable the drawable to display * @attr ref R.styleable#FlexibleToolbarLayout_contentScrimColor * @see #getContentScrim() */ public void setContentScrim(@Nullable Drawable drawable) { if (mContentScrim != drawable) { if (mContentScrim != null) { mContentScrim.setCallback(null); } if (drawable != null) { mContentScrim = drawable.mutate(); drawable.setBounds(0, 0, getWidth(), getHeight()); drawable.setCallback(this); drawable.setAlpha(mScrimAlpha); } else { mContentScrim = null; } ViewCompat.postInvalidateOnAnimation(this); } } /** * Set the color to use for the content scrim. * * @param color the color to display * @attr ref R.styleable#FlexibleToolbarLayout_contentScrimColor * @see #getContentScrim() */ public void setContentScrimColor(@ColorInt int color) { setContentScrim(new ColorDrawable(color)); } /** * Set the drawable to use for the content scrim from resources. * * @param resId drawable resource id * @attr ref R.styleable#FlexibleToolbarLayout_contentScrimColor * @see #getContentScrim() */ public void setContentScrimResource(@DrawableRes int resId) { setContentScrim(ContextCompat.getDrawable(getContext(), resId)); } /** * Returns the drawable which is used for the foreground scrim. * * @attr ref R.styleable#FlexibleToolbarLayout_contentScrimColor * @see #setContentScrim(Drawable) */ public Drawable getContentScrim() { return mContentScrim; } /** * Set the drawable to use for the status bar scrim from resources. * Providing null will disable the scrim functionality. * <p/> * <p>This scrim is only shown when we have been given a top system inset.</p> * * @param drawable the drawable to display * @attr ref R.styleable#FlexibleToolbarLayout_statusBarScrimColor * @see #getStatusBarScrim() */ public void setStatusBarScrim(@Nullable Drawable drawable) { if (mStatusBarScrim != drawable) { if (mStatusBarScrim != null) { mStatusBarScrim.setCallback(null); } mStatusBarScrim = drawable; drawable.setCallback(this); drawable.mutate().setAlpha(mScrimAlpha); ViewCompat.postInvalidateOnAnimation(this); } } /** * Set the color to use for the status bar scrim. * <p/> * <p>This scrim is only shown when we have been given a top system inset.</p> * * @param color the color to display * @attr ref R.styleable#FlexibleToolbarLayout_statusBarScrimColor * @see #getStatusBarScrim() */ public void setStatusBarScrimColor(@ColorInt int color) { setStatusBarScrim(new ColorDrawable(color)); } /** * Set the drawable to use for the content scrim from resources. * * @param resId drawable resource id * @attr ref R.styleable#FlexibleToolbarLayout_statusBarScrimColor * @see #getStatusBarScrim() */ public void setStatusBarScrimResource(@DrawableRes int resId) { setStatusBarScrim(ContextCompat.getDrawable(getContext(), resId)); } /** * Returns the drawable which is used for the status bar scrim. * * @attr ref R.styleable#FlexibleToolbarLayout_statusBarScrimColor * @see #setStatusBarScrim(Drawable) */ public Drawable getStatusBarScrim() { return mStatusBarScrim; } /** * Sets the text color and size for the collapsed title from the specified * TextAppearance resource. * * @attr ref android.support.design.R.styleable#FlexibleToolbarLayout_titleCollapsedTextAppearance */ public void setCollapsedTitleTextAppearance(@StyleRes int resId) { mTitleCollapsingTextHelper.setCollapsedTextAppearance(resId); } /** * Sets the text color of the collapsed title. * * @param color The new text color in ARGB format */ public void setCollapsedTitleTextColor(@ColorInt int color) { mTitleCollapsingTextHelper.setCollapsedTextColor(color); } /** * Sets the text color and size for the expanded title from the specified * TextAppearance resource. * * @attr ref android.support.design.R.styleable#FlexibleToolbarLayout_titleExpandedTextAppearance */ public void setExpandedTitleTextAppearance(@StyleRes int resId) { mTitleCollapsingTextHelper.setExpandedTextAppearance(resId); } /** * Sets the text color of the expanded title. * * @param color The new text color in ARGB format */ public void setExpandedTitleColor(@ColorInt int color) { mTitleCollapsingTextHelper.setExpandedTextColor(color); } /** * The additional offset used to define when to trigger the scrim visibility change. */ final int getScrimTriggerOffset() { return 2 * ViewCompat.getMinimumHeight(this); } @Override protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { return p instanceof LayoutParams; } @Override protected LayoutParams generateDefaultLayoutParams() { return new LayoutParams(super.generateDefaultLayoutParams()); } @Override public FrameLayout.LayoutParams generateLayoutParams(AttributeSet attrs) { return new LayoutParams(getContext(), attrs); } @Override protected FrameLayout.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { return new LayoutParams(p); } public static class LayoutParams extends FrameLayout.LayoutParams { private static final float DEFAULT_PARALLAX_MULTIPLIER = 0.5f; /** * @hide */ @IntDef({ COLLAPSE_MODE_OFF, COLLAPSE_MODE_PIN, COLLAPSE_MODE_PARALLAX }) @Retention(RetentionPolicy.SOURCE) @interface CollapseMode { } /** * The view will act as normal with no collapsing behavior. */ public static final int COLLAPSE_MODE_OFF = 0; /** * The view will pin in place until it reaches the bottom of the * {@link FlexibleToolbarLayout}. */ public static final int COLLAPSE_MODE_PIN = 1; /** * The view will scroll in a parallax fashion. See {@link #setParallaxMultiplier(float)} * to change the multiplier used. */ public static final int COLLAPSE_MODE_PARALLAX = 2; int mCollapseMode = COLLAPSE_MODE_OFF; float mParallaxMult = DEFAULT_PARALLAX_MULTIPLIER; public LayoutParams(Context c, AttributeSet attrs) { super(c, attrs); TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.FlexibleToolbarLayout_LayoutParams); mCollapseMode = a.getInt( R.styleable.FlexibleToolbarLayout_LayoutParams_layout_collapseMode, COLLAPSE_MODE_OFF); setParallaxMultiplier(a.getFloat( R.styleable.FlexibleToolbarLayout_LayoutParams_layout_collapseParallaxMultiplier, DEFAULT_PARALLAX_MULTIPLIER)); a.recycle(); } public LayoutParams(int width, int height) { super(width, height); } public LayoutParams(int width, int height, int gravity) { super(width, height, gravity); } public LayoutParams(ViewGroup.LayoutParams p) { super(p); } public LayoutParams(MarginLayoutParams source) { super(source); } public LayoutParams(FrameLayout.LayoutParams source) { super(source); } /** * Set the collapse mode. * * @param collapseMode one of {@link #COLLAPSE_MODE_OFF}, {@link #COLLAPSE_MODE_PIN} * or {@link #COLLAPSE_MODE_PARALLAX}. */ public void setCollapseMode(@CollapseMode int collapseMode) { mCollapseMode = collapseMode; } /** * Returns the requested collapse mode. * * @return the current mode. One of {@link #COLLAPSE_MODE_OFF}, {@link #COLLAPSE_MODE_PIN} * or {@link #COLLAPSE_MODE_PARALLAX}. */ @CollapseMode public int getCollapseMode() { return mCollapseMode; } /** * Set the parallax scroll multiplier used in conjunction with * {@link #COLLAPSE_MODE_PARALLAX}. A value of {@code 0.0} indicates no movement at all, * {@code 1.0f} indicates normal scroll movement. * * @param multiplier the multiplier. * @see #getParallaxMultiplier() */ public void setParallaxMultiplier(float multiplier) { mParallaxMult = multiplier; } /** * Returns the parallax scroll multiplier used in conjunction with * {@link #COLLAPSE_MODE_PARALLAX}. * * @see #setParallaxMultiplier(float) */ public float getParallaxMultiplier() { return mParallaxMult; } } private class OffsetUpdateListener implements AppBarLayout.OnOffsetChangedListener { @Override public void onOffsetChanged(AppBarLayout layout, int verticalOffset) { 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: if (getHeight() - insetTop + verticalOffset >= child.getHeight()) { offsetHelper.setTopAndBottomOffset(-verticalOffset); } break; case LayoutParams.COLLAPSE_MODE_PARALLAX: offsetHelper.setTopAndBottomOffset( Math.round(-verticalOffset * lp.mParallaxMult)); break; } } // Show or hide the scrims if needed if (mContentScrim != null || mStatusBarScrim != null) { setScrimsShown(getHeight() + verticalOffset < getScrimTriggerOffset() + insetTop); } if (mStatusBarScrim != null && insetTop > 0) { ViewCompat.postInvalidateOnAnimation(FlexibleToolbarLayout.this); } // Update the collapsing text's fraction final int expandRange = getHeight() - ViewCompat.getMinimumHeight( FlexibleToolbarLayout.this) - insetTop; float fraction = Math.abs(verticalOffset) / (float) expandRange; mTitleCollapsingTextHelper.setExpansionFraction(fraction); mSubtitleCollapsingTextHelper.setExpansionFraction(fraction); mIconCollapsingHelper.setExpansionFraction(fraction); } } }