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