CollapsingToolbarLayout.java revision 6759b1021d8198ad1d239bb30e5a102b99624bce
1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.support.design.widget;
18
19import android.content.Context;
20import android.content.res.TypedArray;
21import android.graphics.Canvas;
22import android.graphics.Rect;
23import android.graphics.Typeface;
24import android.graphics.drawable.ColorDrawable;
25import android.graphics.drawable.Drawable;
26import android.support.annotation.ColorInt;
27import android.support.annotation.DrawableRes;
28import android.support.annotation.IntDef;
29import android.support.annotation.NonNull;
30import android.support.annotation.Nullable;
31import android.support.annotation.StyleRes;
32import android.support.design.R;
33import android.support.v4.content.ContextCompat;
34import android.support.v4.graphics.drawable.DrawableCompat;
35import android.support.v4.view.GravityCompat;
36import android.support.v4.view.ViewCompat;
37import android.support.v4.view.WindowInsetsCompat;
38import android.support.v7.widget.Toolbar;
39import android.text.TextUtils;
40import android.util.AttributeSet;
41import android.view.Gravity;
42import android.view.View;
43import android.view.ViewGroup;
44import android.view.ViewParent;
45import android.widget.FrameLayout;
46
47import java.lang.annotation.Retention;
48import java.lang.annotation.RetentionPolicy;
49
50/**
51 * CollapsingToolbarLayout is a wrapper for {@link Toolbar} which implements a collapsing app bar.
52 * It is designed to be used as a direct child of a {@link AppBarLayout}.
53 * CollapsingToolbarLayout contains the following features:
54 *
55 * <h4>Collapsing title</h4>
56 * A title which is larger when the layout is fully visible but collapses and becomes smaller as
57 * the layout is scrolled off screen. You can set the title to display via
58 * {@link #setTitle(CharSequence)}. The title appearance can be tweaked via the
59 * {@code collapsedTextAppearance} and {@code expandedTextAppearance} attributes.
60 *
61 * <h4>Content scrim</h4>
62 * A full-bleed scrim which is show or hidden when the scroll position has hit a certain threshold.
63 * You can change this via {@link #setContentScrim(Drawable)}.
64 *
65 * <h4>Status bar scrim</h4>
66 * A scrim which is show or hidden behind the status bar when the scroll position has hit a certain
67 * threshold. You can change this via {@link #setStatusBarScrim(Drawable)}. This only works
68 * on {@link android.os.Build.VERSION_CODES#LOLLIPOP LOLLIPOP} devices when we set to fit system
69 * windows.
70 *
71 * <h4>Parallax scrolling children</h4>
72 * Child views can opt to be scrolled within this layout in a parallax fashion.
73 * See {@link LayoutParams#COLLAPSE_MODE_PARALLAX} and
74 * {@link LayoutParams#setParallaxMultiplier(float)}.
75 *
76 * <h4>Pinned position children</h4>
77 * Child views can opt to be pinned in space globally. This is useful when implementing a
78 * collapsing as it allows the {@link Toolbar} to be fixed in place even though this layout is
79 * moving. See {@link LayoutParams#COLLAPSE_MODE_PIN}.
80 *
81 * <p><strong>Do not manually add views to the Toolbar at run time</strong>.
82 * We will add a 'dummy view' to the Toolbar which allows us to work out the available space
83 * for the title. This can interfere with any views which you add.</p>
84 *
85 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_collapsedTitleTextAppearance
86 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleTextAppearance
87 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_contentScrim
88 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMargin
89 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginStart
90 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginEnd
91 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginBottom
92 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_statusBarScrim
93 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_toolbarId
94 */
95public class CollapsingToolbarLayout extends FrameLayout {
96
97    private static final int SCRIM_ANIMATION_DURATION = 600;
98
99    private boolean mRefreshToolbar = true;
100    private int mToolbarId;
101    private Toolbar mToolbar;
102    private View mToolbarDirectChild;
103    private View mDummyView;
104
105    private int mExpandedMarginStart;
106    private int mExpandedMarginTop;
107    private int mExpandedMarginEnd;
108    private int mExpandedMarginBottom;
109
110    private final Rect mTmpRect = new Rect();
111    private final CollapsingTextHelper mCollapsingTextHelper;
112    private boolean mCollapsingTitleEnabled;
113    private boolean mDrawCollapsingTitle;
114
115    private Drawable mContentScrim;
116    private Drawable mStatusBarScrim;
117    private int mScrimAlpha;
118    private boolean mScrimsAreShown;
119    private ValueAnimatorCompat mScrimAnimator;
120
121    private AppBarLayout.OnOffsetChangedListener mOnOffsetChangedListener;
122
123    private int mCurrentOffset;
124
125    private WindowInsetsCompat mLastInsets;
126
127    public CollapsingToolbarLayout(Context context) {
128        this(context, null);
129    }
130
131    public CollapsingToolbarLayout(Context context, AttributeSet attrs) {
132        this(context, attrs, 0);
133    }
134
135    public CollapsingToolbarLayout(Context context, AttributeSet attrs, int defStyleAttr) {
136        super(context, attrs, defStyleAttr);
137
138        ThemeUtils.checkAppCompatTheme(context);
139
140        mCollapsingTextHelper = new CollapsingTextHelper(this);
141        mCollapsingTextHelper.setTextSizeInterpolator(AnimationUtils.DECELERATE_INTERPOLATOR);
142
143        TypedArray a = context.obtainStyledAttributes(attrs,
144                R.styleable.CollapsingToolbarLayout, defStyleAttr,
145                R.style.Widget_Design_CollapsingToolbar);
146
147        mCollapsingTextHelper.setExpandedTextGravity(
148                a.getInt(R.styleable.CollapsingToolbarLayout_expandedTitleGravity,
149                        GravityCompat.START | Gravity.BOTTOM));
150        mCollapsingTextHelper.setCollapsedTextGravity(
151                a.getInt(R.styleable.CollapsingToolbarLayout_collapsedTitleGravity,
152                        GravityCompat.START | Gravity.CENTER_VERTICAL));
153
154        mExpandedMarginStart = mExpandedMarginTop = mExpandedMarginEnd = mExpandedMarginBottom =
155                a.getDimensionPixelSize(R.styleable.CollapsingToolbarLayout_expandedTitleMargin, 0);
156
157        if (a.hasValue(R.styleable.CollapsingToolbarLayout_expandedTitleMarginStart)) {
158            mExpandedMarginStart = a.getDimensionPixelSize(
159                    R.styleable.CollapsingToolbarLayout_expandedTitleMarginStart, 0);
160        }
161        if (a.hasValue(R.styleable.CollapsingToolbarLayout_expandedTitleMarginEnd)) {
162            mExpandedMarginEnd = a.getDimensionPixelSize(
163                    R.styleable.CollapsingToolbarLayout_expandedTitleMarginEnd, 0);
164        }
165        if (a.hasValue(R.styleable.CollapsingToolbarLayout_expandedTitleMarginTop)) {
166            mExpandedMarginTop = a.getDimensionPixelSize(
167                    R.styleable.CollapsingToolbarLayout_expandedTitleMarginTop, 0);
168        }
169        if (a.hasValue(R.styleable.CollapsingToolbarLayout_expandedTitleMarginBottom)) {
170            mExpandedMarginBottom = a.getDimensionPixelSize(
171                    R.styleable.CollapsingToolbarLayout_expandedTitleMarginBottom, 0);
172        }
173
174        mCollapsingTitleEnabled = a.getBoolean(
175                R.styleable.CollapsingToolbarLayout_titleEnabled, true);
176        setTitle(a.getText(R.styleable.CollapsingToolbarLayout_title));
177
178        // First load the default text appearances
179        mCollapsingTextHelper.setExpandedTextAppearance(
180                R.style.TextAppearance_Design_CollapsingToolbar_Expanded);
181        mCollapsingTextHelper.setCollapsedTextAppearance(
182                android.support.v7.appcompat.R.style.TextAppearance_AppCompat_Widget_ActionBar_Title);
183
184        // Now overlay any custom text appearances
185        if (a.hasValue(R.styleable.CollapsingToolbarLayout_expandedTitleTextAppearance)) {
186            mCollapsingTextHelper.setExpandedTextAppearance(
187                    a.getResourceId(
188                            R.styleable.CollapsingToolbarLayout_expandedTitleTextAppearance, 0));
189        }
190        if (a.hasValue(R.styleable.CollapsingToolbarLayout_collapsedTitleTextAppearance)) {
191            mCollapsingTextHelper.setCollapsedTextAppearance(
192                    a.getResourceId(
193                            R.styleable.CollapsingToolbarLayout_collapsedTitleTextAppearance, 0));
194        }
195
196        setContentScrim(a.getDrawable(R.styleable.CollapsingToolbarLayout_contentScrim));
197        setStatusBarScrim(a.getDrawable(R.styleable.CollapsingToolbarLayout_statusBarScrim));
198
199        mToolbarId = a.getResourceId(R.styleable.CollapsingToolbarLayout_toolbarId, -1);
200
201        a.recycle();
202
203        setWillNotDraw(false);
204
205        ViewCompat.setOnApplyWindowInsetsListener(this,
206                new android.support.v4.view.OnApplyWindowInsetsListener() {
207                    @Override
208                    public WindowInsetsCompat onApplyWindowInsets(View v,
209                            WindowInsetsCompat insets) {
210                        return setWindowInsets(insets);
211                    }
212                });
213    }
214
215    @Override
216    protected void onAttachedToWindow() {
217        super.onAttachedToWindow();
218
219        // Add an OnOffsetChangedListener if possible
220        final ViewParent parent = getParent();
221        if (parent instanceof AppBarLayout) {
222            if (mOnOffsetChangedListener == null) {
223                mOnOffsetChangedListener = new OffsetUpdateListener();
224            }
225            ((AppBarLayout) parent).addOnOffsetChangedListener(mOnOffsetChangedListener);
226        }
227
228        // We're attached, so lets request an inset dispatch
229        ViewCompat.requestApplyInsets(this);
230    }
231
232    @Override
233    protected void onDetachedFromWindow() {
234        // Remove our OnOffsetChangedListener if possible and it exists
235        final ViewParent parent = getParent();
236        if (mOnOffsetChangedListener != null && parent instanceof AppBarLayout) {
237            ((AppBarLayout) parent).removeOnOffsetChangedListener(mOnOffsetChangedListener);
238        }
239
240        super.onDetachedFromWindow();
241    }
242
243    private WindowInsetsCompat setWindowInsets(WindowInsetsCompat insets) {
244        if (mLastInsets != insets) {
245            mLastInsets = insets;
246            requestLayout();
247        }
248        return insets.consumeSystemWindowInsets();
249    }
250
251    @Override
252    public void draw(Canvas canvas) {
253        super.draw(canvas);
254
255        // If we don't have a toolbar, the scrim will be not be drawn in drawChild() below.
256        // Instead, we draw it here, before our collapsing text.
257        ensureToolbar();
258        if (mToolbar == null && mContentScrim != null && mScrimAlpha > 0) {
259            mContentScrim.mutate().setAlpha(mScrimAlpha);
260            mContentScrim.draw(canvas);
261        }
262
263        // Let the collapsing text helper draw its text
264        if (mCollapsingTitleEnabled && mDrawCollapsingTitle) {
265            mCollapsingTextHelper.draw(canvas);
266        }
267
268        // Now draw the status bar scrim
269        if (mStatusBarScrim != null && mScrimAlpha > 0) {
270            final int topInset = mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0;
271            if (topInset > 0) {
272                mStatusBarScrim.setBounds(0, -mCurrentOffset, getWidth(),
273                        topInset - mCurrentOffset);
274                mStatusBarScrim.mutate().setAlpha(mScrimAlpha);
275                mStatusBarScrim.draw(canvas);
276            }
277        }
278    }
279
280    @Override
281    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
282        // This is a little weird. Our scrim needs to be behind the Toolbar (if it is present),
283        // but in front of any other children which are behind it. To do this we intercept the
284        // drawChild() call, and draw our scrim first when drawing the toolbar
285        ensureToolbar();
286        if (child == mToolbar && mContentScrim != null && mScrimAlpha > 0) {
287            mContentScrim.mutate().setAlpha(mScrimAlpha);
288            mContentScrim.draw(canvas);
289        }
290
291        // Carry on drawing the child...
292        return super.drawChild(canvas, child, drawingTime);
293    }
294
295    @Override
296    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
297        super.onSizeChanged(w, h, oldw, oldh);
298        if (mContentScrim != null) {
299            mContentScrim.setBounds(0, 0, w, h);
300        }
301    }
302
303    private void ensureToolbar() {
304        if (!mRefreshToolbar) {
305            return;
306        }
307
308        // First clear out the current Toolbar
309        mToolbar = null;
310        mToolbarDirectChild = null;
311
312        if (mToolbarId != -1) {
313            // If we have an ID set, try and find it and it's direct parent to us
314            mToolbar = (Toolbar) findViewById(mToolbarId);
315            if (mToolbar != null) {
316                mToolbarDirectChild = findDirectChild(mToolbar);
317            }
318        }
319
320        if (mToolbar == null) {
321            // If we don't have an ID, or couldn't find a Toolbar with the correct ID, try and find
322            // one from our direct children
323            Toolbar toolbar = null;
324            for (int i = 0, count = getChildCount(); i < count; i++) {
325                final View child = getChildAt(i);
326                if (child instanceof Toolbar) {
327                    toolbar = (Toolbar) child;
328                    break;
329                }
330            }
331            mToolbar = toolbar;
332        }
333
334        updateDummyView();
335        mRefreshToolbar = false;
336    }
337
338    /**
339     * Returns the direct child of this layout, which itself is the ancestor of the
340     * given view.
341     */
342    private View findDirectChild(final View descendant) {
343        View directChild = descendant;
344        for (ViewParent p = descendant.getParent(); p != this && p != null; p = p.getParent()) {
345            if (p instanceof View) {
346                directChild = (View) p;
347            }
348        }
349        return directChild;
350    }
351
352    private void updateDummyView() {
353        if (!mCollapsingTitleEnabled && mDummyView != null) {
354            // If we have a dummy view and we have our title disabled, remove it from its parent
355            final ViewParent parent = mDummyView.getParent();
356            if (parent instanceof ViewGroup) {
357                ((ViewGroup) parent).removeView(mDummyView);
358            }
359        }
360        if (mCollapsingTitleEnabled && mToolbar != null) {
361            if (mDummyView == null) {
362                mDummyView = new View(getContext());
363            }
364            if (mDummyView.getParent() == null) {
365                mToolbar.addView(mDummyView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
366            }
367        }
368    }
369
370    @Override
371    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
372        ensureToolbar();
373        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
374    }
375
376    @Override
377    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
378        super.onLayout(changed, left, top, right, bottom);
379
380        // Update the collapsed bounds by getting it's transformed bounds. This needs to be done
381        // before the children are offset below
382        if (mCollapsingTitleEnabled && mDummyView != null) {
383            // We only draw the title if the dummy view is being displayed (Toolbar removes
384            // views if there is no space)
385            mDrawCollapsingTitle = ViewCompat.isAttachedToWindow(mDummyView)
386                    && mDummyView.getVisibility() == VISIBLE;
387
388            if (mDrawCollapsingTitle) {
389                final boolean isRtl = ViewCompat.getLayoutDirection(this)
390                        == ViewCompat.LAYOUT_DIRECTION_RTL;
391
392                // Update the collapsed bounds
393                int bottomOffset = 0;
394                if (mToolbarDirectChild != null && mToolbarDirectChild != this) {
395                    final LayoutParams lp = (LayoutParams) mToolbarDirectChild.getLayoutParams();
396                    bottomOffset = lp.bottomMargin;
397                }
398                ViewGroupUtils.getDescendantRect(this, mDummyView, mTmpRect);
399                mCollapsingTextHelper.setCollapsedBounds(
400                        mTmpRect.left + (isRtl
401                                ? mToolbar.getTitleMarginEnd()
402                                : mToolbar.getTitleMarginStart()),
403                        bottom + mToolbar.getTitleMarginTop() - mTmpRect.height() - bottomOffset,
404                        mTmpRect.right + (isRtl
405                                ? mToolbar.getTitleMarginStart()
406                                : mToolbar.getTitleMarginEnd()),
407                        bottom - bottomOffset - mToolbar.getTitleMarginBottom());
408
409                // Update the expanded bounds
410                mCollapsingTextHelper.setExpandedBounds(
411                        isRtl ? mExpandedMarginEnd : mExpandedMarginStart,
412                        mTmpRect.bottom + mExpandedMarginTop,
413                        right - left - (isRtl ? mExpandedMarginStart : mExpandedMarginEnd),
414                        bottom - top - mExpandedMarginBottom);
415                // Now recalculate using the new bounds
416                mCollapsingTextHelper.recalculate();
417            }
418        }
419
420        // Update our child view offset helpers
421        for (int i = 0, z = getChildCount(); i < z; i++) {
422            final View child = getChildAt(i);
423
424            if (mLastInsets != null && !ViewCompat.getFitsSystemWindows(child)) {
425                final int insetTop = mLastInsets.getSystemWindowInsetTop();
426                if (child.getTop() < insetTop) {
427                    // If the child isn't set to fit system windows but is drawing within the inset
428                    // offset it down
429                    ViewCompat.offsetTopAndBottom(child, insetTop);
430                }
431            }
432
433            getViewOffsetHelper(child).onViewLayout();
434        }
435
436        // Finally, set our minimum height to enable proper AppBarLayout collapsing
437        if (mToolbar != null) {
438            if (mCollapsingTitleEnabled && TextUtils.isEmpty(mCollapsingTextHelper.getText())) {
439                // If we do not currently have a title, try and grab it from the Toolbar
440                mCollapsingTextHelper.setText(mToolbar.getTitle());
441            }
442            if (mToolbarDirectChild == null || mToolbarDirectChild == this) {
443                setMinimumHeight(getHeightWithMargins(mToolbar));
444            } else {
445                setMinimumHeight(getHeightWithMargins(mToolbarDirectChild));
446            }
447        }
448    }
449
450    private static int getHeightWithMargins(@NonNull final View view) {
451        final ViewGroup.LayoutParams lp = view.getLayoutParams();
452        if (lp instanceof MarginLayoutParams) {
453            final MarginLayoutParams mlp = (MarginLayoutParams) lp;
454            return view.getHeight() + mlp.topMargin + mlp.bottomMargin;
455        }
456        return view.getHeight();
457    }
458
459    private static ViewOffsetHelper getViewOffsetHelper(View view) {
460        ViewOffsetHelper offsetHelper = (ViewOffsetHelper) view.getTag(R.id.view_offset_helper);
461        if (offsetHelper == null) {
462            offsetHelper = new ViewOffsetHelper(view);
463            view.setTag(R.id.view_offset_helper, offsetHelper);
464        }
465        return offsetHelper;
466    }
467
468    /**
469     * Sets the title to be displayed by this view, if enabled.
470     *
471     * @see #setTitleEnabled(boolean)
472     * @see #getTitle()
473     *
474     * @attr ref R.styleable#CollapsingToolbarLayout_title
475     */
476    public void setTitle(@Nullable CharSequence title) {
477        mCollapsingTextHelper.setText(title);
478    }
479
480    /**
481     * Returns the title currently being displayed by this view. If the title is not enabled, then
482     * this will return {@code null}.
483     *
484     * @attr ref R.styleable#CollapsingToolbarLayout_title
485     */
486    @Nullable
487    public CharSequence getTitle() {
488        return mCollapsingTitleEnabled ? mCollapsingTextHelper.getText() : null;
489    }
490
491    /**
492     * Sets whether this view should display its own title.
493     *
494     * <p>The title displayed by this view will shrink and grow based on the scroll offset.</p>
495     *
496     * @see #setTitle(CharSequence)
497     * @see #isTitleEnabled()
498     *
499     * @attr ref R.styleable#CollapsingToolbarLayout_titleEnabled
500     */
501    public void setTitleEnabled(boolean enabled) {
502        if (enabled != mCollapsingTitleEnabled) {
503            mCollapsingTitleEnabled = enabled;
504            updateDummyView();
505            requestLayout();
506        }
507    }
508
509    /**
510     * Returns whether this view is currently displaying its own title.
511     *
512     * @see #setTitleEnabled(boolean)
513     *
514     * @attr ref R.styleable#CollapsingToolbarLayout_titleEnabled
515     */
516    public boolean isTitleEnabled() {
517        return mCollapsingTitleEnabled;
518    }
519
520    /**
521     * Set whether the content scrim and/or status bar scrim should be shown or not. Any change
522     * in the vertical scroll may overwrite this value. Any visibility change will be animated if
523     * this view has already been laid out.
524     *
525     * @param shown whether the scrims should be shown
526     *
527     * @see #getStatusBarScrim()
528     * @see #getContentScrim()
529     */
530    public void setScrimsShown(boolean shown) {
531        setScrimsShown(shown, ViewCompat.isLaidOut(this) && !isInEditMode());
532    }
533
534    /**
535     * Set whether the content scrim and/or status bar scrim should be shown or not. Any change
536     * in the vertical scroll may overwrite this value.
537     *
538     * @param shown whether the scrims should be shown
539     * @param animate whether to animate the visibility change
540     *
541     * @see #getStatusBarScrim()
542     * @see #getContentScrim()
543     */
544    public void setScrimsShown(boolean shown, boolean animate) {
545        if (mScrimsAreShown != shown) {
546            if (animate) {
547                animateScrim(shown ? 0xFF : 0x0);
548            } else {
549                setScrimAlpha(shown ? 0xFF : 0x0);
550            }
551            mScrimsAreShown = shown;
552        }
553    }
554
555    private void animateScrim(int targetAlpha) {
556        ensureToolbar();
557        if (mScrimAnimator == null) {
558            mScrimAnimator = ViewUtils.createAnimator();
559            mScrimAnimator.setDuration(SCRIM_ANIMATION_DURATION);
560            mScrimAnimator.setInterpolator(
561                    targetAlpha > mScrimAlpha
562                            ? AnimationUtils.FAST_OUT_LINEAR_IN_INTERPOLATOR
563                            : AnimationUtils.LINEAR_OUT_SLOW_IN_INTERPOLATOR);
564            mScrimAnimator.setUpdateListener(new ValueAnimatorCompat.AnimatorUpdateListener() {
565                @Override
566                public void onAnimationUpdate(ValueAnimatorCompat animator) {
567                    setScrimAlpha(animator.getAnimatedIntValue());
568                }
569            });
570        } else if (mScrimAnimator.isRunning()) {
571            mScrimAnimator.cancel();
572        }
573
574        mScrimAnimator.setIntValues(mScrimAlpha, targetAlpha);
575        mScrimAnimator.start();
576    }
577
578    private void setScrimAlpha(int alpha) {
579        if (alpha != mScrimAlpha) {
580            final Drawable contentScrim = mContentScrim;
581            if (contentScrim != null && mToolbar != null) {
582                ViewCompat.postInvalidateOnAnimation(mToolbar);
583            }
584            mScrimAlpha = alpha;
585            ViewCompat.postInvalidateOnAnimation(CollapsingToolbarLayout.this);
586        }
587    }
588
589    /**
590     * Set the drawable to use for the content scrim from resources. Providing null will disable
591     * the scrim functionality.
592     *
593     * @param drawable the drawable to display
594     *
595     * @attr ref R.styleable#CollapsingToolbarLayout_contentScrim
596     * @see #getContentScrim()
597     */
598    public void setContentScrim(@Nullable Drawable drawable) {
599        if (mContentScrim != drawable) {
600            if (mContentScrim != null) {
601                mContentScrim.setCallback(null);
602            }
603            mContentScrim = drawable != null ? drawable.mutate() : null;
604            if (mContentScrim != null) {
605                mContentScrim.setBounds(0, 0, getWidth(), getHeight());
606                mContentScrim.setCallback(this);
607                mContentScrim.setAlpha(mScrimAlpha);
608            }
609            ViewCompat.postInvalidateOnAnimation(this);
610        }
611    }
612
613    /**
614     * Set the color to use for the content scrim.
615     *
616     * @param color the color to display
617     *
618     * @attr ref R.styleable#CollapsingToolbarLayout_contentScrim
619     * @see #getContentScrim()
620     */
621    public void setContentScrimColor(@ColorInt int color) {
622        setContentScrim(new ColorDrawable(color));
623    }
624
625    /**
626     * Set the drawable to use for the content scrim from resources.
627     *
628     * @param resId drawable resource id
629     *
630     * @attr ref R.styleable#CollapsingToolbarLayout_contentScrim
631     * @see #getContentScrim()
632     */
633    public void setContentScrimResource(@DrawableRes int resId) {
634        setContentScrim(ContextCompat.getDrawable(getContext(), resId));
635
636    }
637
638    /**
639     * Returns the drawable which is used for the foreground scrim.
640     *
641     * @attr ref R.styleable#CollapsingToolbarLayout_contentScrim
642     * @see #setContentScrim(Drawable)
643     */
644    @Nullable
645    public Drawable getContentScrim() {
646        return mContentScrim;
647    }
648
649    /**
650     * Set the drawable to use for the status bar scrim from resources.
651     * Providing null will disable the scrim functionality.
652     *
653     * <p>This scrim is only shown when we have been given a top system inset.</p>
654     *
655     * @param drawable the drawable to display
656     *
657     * @attr ref R.styleable#CollapsingToolbarLayout_statusBarScrim
658     * @see #getStatusBarScrim()
659     */
660    public void setStatusBarScrim(@Nullable Drawable drawable) {
661        if (mStatusBarScrim != drawable) {
662            if (mStatusBarScrim != null) {
663                mStatusBarScrim.setCallback(null);
664            }
665            mStatusBarScrim = drawable != null ? drawable.mutate() : null;
666            if (mStatusBarScrim != null) {
667                if (mStatusBarScrim.isStateful()) {
668                    mStatusBarScrim.setState(getDrawableState());
669                }
670                DrawableCompat.setLayoutDirection(mStatusBarScrim,
671                        ViewCompat.getLayoutDirection(this));
672                mStatusBarScrim.setVisible(getVisibility() == VISIBLE, false);
673                mStatusBarScrim.setCallback(this);
674                mStatusBarScrim.setAlpha(mScrimAlpha);
675            }
676            ViewCompat.postInvalidateOnAnimation(this);
677        }
678    }
679
680    @Override
681    protected void drawableStateChanged() {
682        super.drawableStateChanged();
683
684        final int[] state = getDrawableState();
685        boolean changed = false;
686
687        Drawable d = mStatusBarScrim;
688        if (d != null && d.isStateful()) {
689            changed |= d.setState(state);
690        }
691        d = mContentScrim;
692        if (d != null && d.isStateful()) {
693            changed |= d.setState(state);
694        }
695
696        if (changed) {
697            invalidate();
698        }
699    }
700
701    @Override
702    protected boolean verifyDrawable(Drawable who) {
703        return super.verifyDrawable(who) || who == mContentScrim || who == mStatusBarScrim;
704    }
705
706    @Override
707    public void setVisibility(int visibility) {
708        super.setVisibility(visibility);
709
710        final boolean visible = visibility == VISIBLE;
711        if (mStatusBarScrim != null && mStatusBarScrim.isVisible() != visible) {
712            mStatusBarScrim.setVisible(visible, false);
713        }
714        if (mContentScrim != null && mContentScrim.isVisible() != visible) {
715            mContentScrim.setVisible(visible, false);
716        }
717    }
718
719    /**
720     * Set the color to use for the status bar scrim.
721     *
722     * <p>This scrim is only shown when we have been given a top system inset.</p>
723     *
724     * @param color the color to display
725     *
726     * @attr ref R.styleable#CollapsingToolbarLayout_statusBarScrim
727     * @see #getStatusBarScrim()
728     */
729    public void setStatusBarScrimColor(@ColorInt int color) {
730        setStatusBarScrim(new ColorDrawable(color));
731    }
732
733    /**
734     * Set the drawable to use for the content scrim from resources.
735     *
736     * @param resId drawable resource id
737     *
738     * @attr ref R.styleable#CollapsingToolbarLayout_statusBarScrim
739     * @see #getStatusBarScrim()
740     */
741    public void setStatusBarScrimResource(@DrawableRes int resId) {
742        setStatusBarScrim(ContextCompat.getDrawable(getContext(), resId));
743    }
744
745    /**
746     * Returns the drawable which is used for the status bar scrim.
747     *
748     * @attr ref R.styleable#CollapsingToolbarLayout_statusBarScrim
749     * @see #setStatusBarScrim(Drawable)
750     */
751    @Nullable
752    public Drawable getStatusBarScrim() {
753        return mStatusBarScrim;
754    }
755
756    /**
757     * Sets the text color and size for the collapsed title from the specified
758     * TextAppearance resource.
759     *
760     * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_collapsedTitleTextAppearance
761     */
762    public void setCollapsedTitleTextAppearance(@StyleRes int resId) {
763        mCollapsingTextHelper.setCollapsedTextAppearance(resId);
764    }
765
766    /**
767     * Sets the text color of the collapsed title.
768     *
769     * @param color The new text color in ARGB format
770     */
771    public void setCollapsedTitleTextColor(@ColorInt int color) {
772        mCollapsingTextHelper.setCollapsedTextColor(color);
773    }
774
775    /**
776     * Sets the horizontal alignment of the collapsed title and the vertical gravity that will
777     * be used when there is extra space in the collapsed bounds beyond what is required for
778     * the title itself.
779     *
780     * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_collapsedTitleGravity
781     */
782    public void setCollapsedTitleGravity(int gravity) {
783        mCollapsingTextHelper.setCollapsedTextGravity(gravity);
784    }
785
786    /**
787     * Returns the horizontal and vertical alignment for title when collapsed.
788     *
789     * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_collapsedTitleGravity
790     */
791    public int getCollapsedTitleGravity() {
792        return mCollapsingTextHelper.getCollapsedTextGravity();
793    }
794
795    /**
796     * Sets the text color and size for the expanded title from the specified
797     * TextAppearance resource.
798     *
799     * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleTextAppearance
800     */
801    public void setExpandedTitleTextAppearance(@StyleRes int resId) {
802        mCollapsingTextHelper.setExpandedTextAppearance(resId);
803    }
804
805    /**
806     * Sets the text color of the expanded title.
807     *
808     * @param color The new text color in ARGB format
809     */
810    public void setExpandedTitleColor(@ColorInt int color) {
811        mCollapsingTextHelper.setExpandedTextColor(color);
812    }
813
814    /**
815     * Sets the horizontal alignment of the expanded title and the vertical gravity that will
816     * be used when there is extra space in the expanded bounds beyond what is required for
817     * the title itself.
818     *
819     * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleGravity
820     */
821    public void setExpandedTitleGravity(int gravity) {
822        mCollapsingTextHelper.setExpandedTextGravity(gravity);
823    }
824
825    /**
826     * Returns the horizontal and vertical alignment for title when expanded.
827     *
828     * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleGravity
829     */
830    public int getExpandedTitleGravity() {
831        return mCollapsingTextHelper.getExpandedTextGravity();
832    }
833
834    /**
835     * Set the typeface to use for the collapsed title.
836     *
837     * @param typeface typeface to use, or {@code null} to use the default.
838     */
839    public void setCollapsedTitleTypeface(@Nullable Typeface typeface) {
840        mCollapsingTextHelper.setCollapsedTypeface(typeface);
841    }
842
843    /**
844     * Returns the typeface used for the collapsed title.
845     */
846    @NonNull
847    public Typeface getCollapsedTitleTypeface() {
848        return mCollapsingTextHelper.getCollapsedTypeface();
849    }
850
851    /**
852     * Set the typeface to use for the expanded title.
853     *
854     * @param typeface typeface to use, or {@code null} to use the default.
855     */
856    public void setExpandedTitleTypeface(@Nullable Typeface typeface) {
857        mCollapsingTextHelper.setExpandedTypeface(typeface);
858    }
859
860    /**
861     * Returns the typeface used for the expanded title.
862     */
863    @NonNull
864    public Typeface getExpandedTitleTypeface() {
865        return mCollapsingTextHelper.getExpandedTypeface();
866    }
867
868    /**
869     * Sets the expanded title margins.
870     *
871     * @param start the starting title margin in pixels
872     * @param top the top title margin in pixels
873     * @param end the ending title margin in pixels
874     * @param bottom the bottom title margin in pixels
875     *
876     * @see #getExpandedTitleMarginStart()
877     * @see #getExpandedTitleMarginTop()
878     * @see #getExpandedTitleMarginEnd()
879     * @see #getExpandedTitleMarginBottom()
880     * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMargin
881     */
882    public void setExpandedTitleMargin(int start, int top, int end, int bottom) {
883        mExpandedMarginStart = start;
884        mExpandedMarginTop = top;
885        mExpandedMarginEnd = end;
886        mExpandedMarginBottom = bottom;
887        requestLayout();
888    }
889
890    /**
891     * @return the starting expanded title margin in pixels
892     *
893     * @see #setExpandedTitleMarginStart(int)
894     * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginStart
895     */
896    public int getExpandedTitleMarginStart() {
897        return mExpandedMarginStart;
898    }
899
900    /**
901     * Sets the starting expanded title margin in pixels.
902     *
903     * @param margin the starting title margin in pixels
904     * @see #getExpandedTitleMarginStart()
905     * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginStart
906     */
907    public void setExpandedTitleMarginStart(int margin) {
908        mExpandedMarginStart = margin;
909        requestLayout();
910    }
911
912    /**
913     * @return the top expanded title margin in pixels
914     * @see #setExpandedTitleMarginTop(int)
915     * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginTop
916     */
917    public int getExpandedTitleMarginTop() {
918        return mExpandedMarginTop;
919    }
920
921    /**
922     * Sets the top expanded title margin in pixels.
923     *
924     * @param margin the top title margin in pixels
925     * @see #getExpandedTitleMarginTop()
926     * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginTop
927     */
928    public void setExpandedTitleMarginTop(int margin) {
929        mExpandedMarginTop = margin;
930        requestLayout();
931    }
932
933    /**
934     * @return the ending expanded title margin in pixels
935     * @see #setExpandedTitleMarginEnd(int)
936     * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginEnd
937     */
938    public int getExpandedTitleMarginEnd() {
939        return mExpandedMarginEnd;
940    }
941
942    /**
943     * Sets the ending expanded title margin in pixels.
944     *
945     * @param margin the ending title margin in pixels
946     * @see #getExpandedTitleMarginEnd()
947     * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginEnd
948     */
949    public void setExpandedTitleMarginEnd(int margin) {
950        mExpandedMarginEnd = margin;
951        requestLayout();
952    }
953
954    /**
955     * @return the bottom expanded title margin in pixels
956     * @see #setExpandedTitleMarginBottom(int)
957     * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginBottom
958     */
959    public int getExpandedTitleMarginBottom() {
960        return mExpandedMarginBottom;
961    }
962
963    /**
964     * Sets the bottom expanded title margin in pixels.
965     *
966     * @param margin the bottom title margin in pixels
967     * @see #getExpandedTitleMarginBottom()
968     * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginBottom
969     */
970    public void setExpandedTitleMarginBottom(int margin) {
971        mExpandedMarginBottom = margin;
972        requestLayout();
973    }
974
975    /**
976     * The additional offset used to define when to trigger the scrim visibility change.
977     */
978    final int getScrimTriggerOffset() {
979        return 2 * ViewCompat.getMinimumHeight(this);
980    }
981
982    @Override
983    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
984        return p instanceof LayoutParams;
985    }
986
987    @Override
988    protected LayoutParams generateDefaultLayoutParams() {
989        return new LayoutParams(super.generateDefaultLayoutParams());
990    }
991
992    @Override
993    public FrameLayout.LayoutParams generateLayoutParams(AttributeSet attrs) {
994        return new LayoutParams(getContext(), attrs);
995    }
996
997    @Override
998    protected FrameLayout.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
999        return new LayoutParams(p);
1000    }
1001
1002    public static class LayoutParams extends FrameLayout.LayoutParams {
1003
1004        private static final float DEFAULT_PARALLAX_MULTIPLIER = 0.5f;
1005
1006        /** @hide */
1007        @IntDef({
1008                COLLAPSE_MODE_OFF,
1009                COLLAPSE_MODE_PIN,
1010                COLLAPSE_MODE_PARALLAX
1011        })
1012        @Retention(RetentionPolicy.SOURCE)
1013        @interface CollapseMode {}
1014
1015        /**
1016         * The view will act as normal with no collapsing behavior.
1017         */
1018        public static final int COLLAPSE_MODE_OFF = 0;
1019
1020        /**
1021         * The view will pin in place until it reaches the bottom of the
1022         * {@link CollapsingToolbarLayout}.
1023         */
1024        public static final int COLLAPSE_MODE_PIN = 1;
1025
1026        /**
1027         * The view will scroll in a parallax fashion. See {@link #setParallaxMultiplier(float)}
1028         * to change the multiplier used.
1029         */
1030        public static final int COLLAPSE_MODE_PARALLAX = 2;
1031
1032        int mCollapseMode = COLLAPSE_MODE_OFF;
1033        float mParallaxMult = DEFAULT_PARALLAX_MULTIPLIER;
1034
1035        public LayoutParams(Context c, AttributeSet attrs) {
1036            super(c, attrs);
1037
1038            TypedArray a = c.obtainStyledAttributes(attrs,
1039                    R.styleable.CollapsingAppBarLayout_LayoutParams);
1040            mCollapseMode = a.getInt(
1041                    R.styleable.CollapsingAppBarLayout_LayoutParams_layout_collapseMode,
1042                    COLLAPSE_MODE_OFF);
1043            setParallaxMultiplier(a.getFloat(
1044                    R.styleable.CollapsingAppBarLayout_LayoutParams_layout_collapseParallaxMultiplier,
1045                    DEFAULT_PARALLAX_MULTIPLIER));
1046            a.recycle();
1047        }
1048
1049        public LayoutParams(int width, int height) {
1050            super(width, height);
1051        }
1052
1053        public LayoutParams(int width, int height, int gravity) {
1054            super(width, height, gravity);
1055        }
1056
1057        public LayoutParams(ViewGroup.LayoutParams p) {
1058            super(p);
1059        }
1060
1061        public LayoutParams(MarginLayoutParams source) {
1062            super(source);
1063        }
1064
1065        public LayoutParams(FrameLayout.LayoutParams source) {
1066            super(source);
1067        }
1068
1069        /**
1070         * Set the collapse mode.
1071         *
1072         * @param collapseMode one of {@link #COLLAPSE_MODE_OFF}, {@link #COLLAPSE_MODE_PIN}
1073         *                     or {@link #COLLAPSE_MODE_PARALLAX}.
1074         */
1075        public void setCollapseMode(@CollapseMode int collapseMode) {
1076            mCollapseMode = collapseMode;
1077        }
1078
1079        /**
1080         * Returns the requested collapse mode.
1081         *
1082         * @return the current mode. One of {@link #COLLAPSE_MODE_OFF}, {@link #COLLAPSE_MODE_PIN}
1083         * or {@link #COLLAPSE_MODE_PARALLAX}.
1084         */
1085        @CollapseMode
1086        public int getCollapseMode() {
1087            return mCollapseMode;
1088        }
1089
1090        /**
1091         * Set the parallax scroll multiplier used in conjunction with
1092         * {@link #COLLAPSE_MODE_PARALLAX}. A value of {@code 0.0} indicates no movement at all,
1093         * {@code 1.0f} indicates normal scroll movement.
1094         *
1095         * @param multiplier the multiplier.
1096         *
1097         * @see #getParallaxMultiplier()
1098         */
1099        public void setParallaxMultiplier(float multiplier) {
1100            mParallaxMult = multiplier;
1101        }
1102
1103        /**
1104         * Returns the parallax scroll multiplier used in conjunction with
1105         * {@link #COLLAPSE_MODE_PARALLAX}.
1106         *
1107         * @see #setParallaxMultiplier(float)
1108         */
1109        public float getParallaxMultiplier() {
1110            return mParallaxMult;
1111        }
1112    }
1113
1114    private class OffsetUpdateListener implements AppBarLayout.OnOffsetChangedListener {
1115        @Override
1116        public void onOffsetChanged(AppBarLayout layout, int verticalOffset) {
1117            mCurrentOffset = verticalOffset;
1118
1119            final int insetTop = mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0;
1120            final int scrollRange = layout.getTotalScrollRange();
1121
1122            for (int i = 0, z = getChildCount(); i < z; i++) {
1123                final View child = getChildAt(i);
1124                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
1125                final ViewOffsetHelper offsetHelper = getViewOffsetHelper(child);
1126
1127                switch (lp.mCollapseMode) {
1128                    case LayoutParams.COLLAPSE_MODE_PIN:
1129                        if (getHeight() - insetTop + verticalOffset >= child.getHeight()) {
1130                            offsetHelper.setTopAndBottomOffset(-verticalOffset);
1131                        }
1132                        break;
1133                    case LayoutParams.COLLAPSE_MODE_PARALLAX:
1134                        offsetHelper.setTopAndBottomOffset(
1135                                Math.round(-verticalOffset * lp.mParallaxMult));
1136                        break;
1137                }
1138            }
1139
1140            // Show or hide the scrims if needed
1141            if (mContentScrim != null || mStatusBarScrim != null) {
1142                setScrimsShown(getHeight() + verticalOffset < getScrimTriggerOffset() + insetTop);
1143            }
1144
1145            if (mStatusBarScrim != null && insetTop > 0) {
1146                ViewCompat.postInvalidateOnAnimation(CollapsingToolbarLayout.this);
1147            }
1148
1149            // Update the collapsing text's fraction
1150            final int expandRange = getHeight() - ViewCompat.getMinimumHeight(
1151                    CollapsingToolbarLayout.this) - insetTop;
1152            mCollapsingTextHelper.setExpansionFraction(
1153                    Math.abs(verticalOffset) / (float) expandRange);
1154
1155            if (Math.abs(verticalOffset) == scrollRange) {
1156                // If we have some pinned children, and we're offset to only show those views,
1157                // we want to be elevate
1158                ViewCompat.setElevation(layout, layout.getTargetElevation());
1159            } else {
1160                // Otherwise, we're inline with the content
1161                ViewCompat.setElevation(layout, 0f);
1162            }
1163        }
1164    }
1165}
1166