CollapsingToolbarLayout.java revision a6a508b2296730ca6954aaebcca52a9962a5cb55
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.Color;
23import android.graphics.Rect;
24import android.support.annotation.ColorRes;
25import android.support.design.R;
26import android.support.v4.graphics.ColorUtils;
27import android.support.v4.view.ViewCompat;
28import android.support.v7.widget.Toolbar;
29import android.util.AttributeSet;
30import android.view.Gravity;
31import android.view.View;
32import android.view.ViewGroup;
33import android.view.animation.Animation;
34import android.view.animation.Transformation;
35import android.widget.FrameLayout;
36
37/**
38 * CollapsingToolbarLayout is a wrapper for {@link Toolbar} which implements a collapsing app bar.
39 * It is designed to be used as a direct child of a {@link AppBarLayout}.
40 * CollapsingToolbarLayout contains the following features:
41 *
42 * <h3>Collapsing title</h3>
43 * A title which is larger when the layout is fully visible but collapses and becomes smaller as
44 * the layout is scrolled off screen. You can set the title to display via
45 * {@link #setTitle(CharSequence)}. The title appearance can be tweaked via the
46 * {@code collapsedTextAppearance} and {@code expandedTextAppearance} attributes.
47 *
48 * <h3>Background scrim</h3>
49 * A full-bleed scrim which is show or hidden when the scroll position has hit a certain threshold.
50 * You can change the color via {@link #setForegroundScrimColor(int)}.
51 *
52 * <h3>Parallax scrolling children</h3>
53 * Child views can opt to be scrolled within this layout in a parallax fashion.
54 * See {@link LayoutParams#COLLAPSE_MODE_PARALLAX} and
55 * {@link LayoutParams#setParallaxMultiplier(float)}.
56 *
57 * <h3>Pinned position children</h3>
58 * Child views can opt to be pinned in space globally. This is useful when implementing a
59 * collapsing as it allows the {@link Toolbar} to be fixed in place even though this layout is
60 * moving. See {@link LayoutParams#COLLAPSE_MODE_PIN}.
61 *
62 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_collapsedTitleTextAppearance
63 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleTextAppearance
64 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_foregroundScrimColor
65 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMargin
66 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginStart
67 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginEnd
68 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginBottom
69 */
70public class CollapsingToolbarLayout extends FrameLayout implements AppBarLayout.AppBarLayoutChild {
71
72    private static final int SCRIM_ANIMATION_DURATION = 200;
73
74    private Toolbar mToolbar;
75    private View mDummyView;
76
77    private int mExpandedMarginLeft;
78    private int mExpandedMarginRight;
79    private int mExpandedMarginBottom;
80
81    private final Rect mRect = new Rect();
82    private final CollapsingTextHelper mCollapsingTextHelper;
83
84    private int mForegroundScrimColor;
85    private int mCurrentForegroundColor;
86    private boolean mScrimIsShown;
87
88    public CollapsingToolbarLayout(Context context) {
89        this(context, null);
90    }
91
92    public CollapsingToolbarLayout(Context context, AttributeSet attrs) {
93        this(context, attrs, 0);
94    }
95
96    public CollapsingToolbarLayout(Context context, AttributeSet attrs, int defStyleAttr) {
97        super(context, attrs, defStyleAttr);
98
99        mCollapsingTextHelper = new CollapsingTextHelper(this);
100        mCollapsingTextHelper.setExpandedTextVerticalGravity(Gravity.BOTTOM);
101        mCollapsingTextHelper.setTextSizeInterpolator(AnimationUtils.DECELERATE_INTERPOLATOR);
102
103        TypedArray a = context.obtainStyledAttributes(attrs,
104                R.styleable.CollapsingToolbarLayout, defStyleAttr,
105                R.style.Widget_Design_CollapsingToolbar);
106
107        mExpandedMarginLeft = mExpandedMarginRight = mExpandedMarginBottom =
108                a.getDimensionPixelSize(R.styleable.CollapsingToolbarLayout_expandedTitleMargin, 0);
109
110        final boolean isRtl = ViewCompat.getLayoutDirection(this)
111                == ViewCompat.LAYOUT_DIRECTION_RTL;
112        if (a.hasValue(R.styleable.CollapsingToolbarLayout_expandedTitleMarginStart)) {
113            final int marginStart = a.getDimensionPixelSize(
114                    R.styleable.CollapsingToolbarLayout_expandedTitleMarginStart, 0);
115            if (isRtl) {
116                mExpandedMarginRight = marginStart;
117            } else {
118                mExpandedMarginLeft = marginStart;
119            }
120        }
121        if (a.hasValue(R.styleable.CollapsingToolbarLayout_expandedTitleMarginEnd)) {
122            final int marginEnd = a.getDimensionPixelSize(
123                    R.styleable.CollapsingToolbarLayout_expandedTitleMarginEnd, 0);
124            if (isRtl) {
125                mExpandedMarginLeft = marginEnd;
126            } else {
127                mExpandedMarginRight = marginEnd;
128            }
129        }
130        if (a.hasValue(R.styleable.CollapsingToolbarLayout_expandedTitleMarginBottom)) {
131            mExpandedMarginBottom = a.getDimensionPixelSize(
132                    R.styleable.CollapsingToolbarLayout_expandedTitleMarginBottom, 0);
133        }
134
135        int tp = a.getResourceId(
136                R.styleable.CollapsingToolbarLayout_expandedTitleTextAppearance,
137                R.style.TextAppearance_AppCompat_Title);
138        mCollapsingTextHelper.setExpandedTextAppearance(tp);
139
140        tp = a.getResourceId(
141                R.styleable.CollapsingToolbarLayout_collapsedTitleTextAppearance,
142                R.style.TextAppearance_AppCompat_Widget_ActionBar_Title);
143        mCollapsingTextHelper.setCollapsedTextAppearance(tp);
144
145        mForegroundScrimColor = a.getColor(
146                R.styleable.CollapsingToolbarLayout_foregroundScrimColor, 0);
147
148        a.recycle();
149
150        setWillNotDraw(false);
151    }
152
153    @Override
154    public void addView(View child, int index, ViewGroup.LayoutParams params) {
155        super.addView(child, index, params);
156
157        if (child instanceof Toolbar) {
158            mToolbar = (Toolbar) child;
159            mDummyView = new View(getContext());
160            mToolbar.addView(mDummyView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
161        }
162    }
163
164    @Override
165    public void draw(Canvas canvas) {
166        super.draw(canvas);
167
168        // Let the collapsing text helper draw it's text
169        mCollapsingTextHelper.draw(canvas);
170    }
171
172    @Override
173    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
174        // This is a little weird. Our scrim needs to be behind the Toolbar (if it is present),
175        // but in front of any other children which are behind it. To do this we intercept the
176        // drawChild() call, and draw our scrim first when drawing the toolbar
177        if (child == mToolbar && Color.alpha(mCurrentForegroundColor) > 0) {
178            canvas.drawColor(mCurrentForegroundColor);
179        }
180        // Carry on drawing the child...
181        return super.drawChild(canvas, child, drawingTime);
182    }
183
184    @Override
185    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
186        super.onLayout(changed, left, top, right, bottom);
187
188        // Update our child view offset helpers
189        for (int i = 0, z = getChildCount(); i < z; i++) {
190            getViewOffsetHelper(getChildAt(i)).onViewLayout();
191        }
192
193        // Now let the collapsing text helper update itself
194        mCollapsingTextHelper.onLayout(changed, left, top, right, bottom);
195
196        // Update the collapsed bounds by getting it's transformed bounds
197        ViewGroupUtils.getDescendantRect(this, mDummyView, mRect);
198        mCollapsingTextHelper.setCollapsedBounds(mRect.left, bottom - mRect.height(),
199                mRect.right, bottom);
200        // Update the expanded bounds
201        mCollapsingTextHelper.setExpandedBounds(left + mExpandedMarginLeft, mDummyView.getBottom(),
202                right - mExpandedMarginRight, bottom - mExpandedMarginBottom);
203
204        // Finally, set our minimum height to enable proper AppBarLayout collapsing
205        setMinimumHeight(mToolbar.getHeight());
206    }
207
208    private static ViewOffsetHelper getViewOffsetHelper(View view) {
209        ViewOffsetHelper offsetHelper = (ViewOffsetHelper) view.getTag(R.id.view_offset_helper);
210        if (offsetHelper == null) {
211            offsetHelper = new ViewOffsetHelper(view);
212            view.setTag(R.id.view_offset_helper, offsetHelper);
213        }
214        return offsetHelper;
215    }
216
217    /**
218     * Set the title to display
219     *
220     * @param title
221     */
222    public void setTitle(CharSequence title) {
223        mCollapsingTextHelper.setText(title);
224    }
225
226    /**
227     * @hide
228     */
229    @Override
230    public void onOffsetUpdate(int leftRightOffset, int topBottomOffset) {
231        boolean toolbarOffsetChanged = false;
232
233        for (int i = 0, z = getChildCount(); i < z; i++) {
234            final View child = getChildAt(i);
235            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
236            final ViewOffsetHelper offsetHelper = getViewOffsetHelper(child);
237            boolean offsetChanged = false;
238
239            switch (lp.mCollapseMode) {
240                case LayoutParams.COLLAPSE_MODE_PIN:
241                    if (getHeight() + topBottomOffset >= child.getHeight()) {
242                        offsetChanged = offsetHelper.setTopAndBottomOffset(-topBottomOffset);
243                    }
244                    break;
245                case LayoutParams.COLLAPSE_MODE_PARALLAX:
246                    offsetChanged = offsetHelper.setTopAndBottomOffset(
247                            Math.round(-topBottomOffset * lp.mParallaxMult));
248                    break;
249            }
250
251            toolbarOffsetChanged = child == mToolbar && offsetChanged;
252
253            // Show or hide the scrim if needed
254            if (Color.alpha(mForegroundScrimColor) > 0) {
255                if (Math.abs(topBottomOffset) < getScrimTriggerOffset()) {
256                    hideScrim();
257                } else {
258                    showScrim();
259                }
260            }
261        }
262
263        // Update the collapsing text's fraction
264        mCollapsingTextHelper.setExpansionFraction(Math.abs(topBottomOffset) /
265                (float) (getHeight() - ViewCompat.getMinimumHeight(this)));
266    }
267
268    private void showScrim() {
269        if (mScrimIsShown) return;
270        Animation anim = new Animation() {
271            @Override
272            protected void applyTransformation(float interpolatedTime, Transformation t) {
273                final int originalAlpha = Color.alpha(mForegroundScrimColor);
274                mCurrentForegroundColor = ColorUtils.setAlphaComponent(mForegroundScrimColor,
275                        AnimationUtils.lerp(0, originalAlpha, interpolatedTime));
276            }
277        };
278        anim.setDuration(SCRIM_ANIMATION_DURATION);
279        anim.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
280        startAnimation(anim);
281        mScrimIsShown = true;
282    }
283
284    private void hideScrim() {
285        if (!mScrimIsShown) return;
286
287        Animation anim = new Animation() {
288            @Override
289            protected void applyTransformation(float interpolatedTime, Transformation t) {
290                final int originalAlpha = Color.alpha(mForegroundScrimColor);
291                mCurrentForegroundColor = ColorUtils.setAlphaComponent(mForegroundScrimColor,
292                        AnimationUtils.lerp(originalAlpha, 0, interpolatedTime));
293            }
294        };
295        anim.setDuration(SCRIM_ANIMATION_DURATION);
296        anim.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
297        startAnimation(anim);
298
299        mScrimIsShown = false;
300    }
301
302    /**
303     * Set the color to use for the  foreground scrim. Providing a transparent color which disable
304     * the scrim.
305     *
306     * @param color ARGB color to use
307     *
308     * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_foregroundScrimColor
309     * @see #getForegroundScrimColor()
310     */
311    public void setForegroundScrimColor(int color) {
312        mForegroundScrimColor = color;
313    }
314
315    /**
316     * Set the color to use for the  foreground scrim from resources. Providing a transparent color
317     * which disable the scrim.
318     *
319     * @param resId color resource id
320     *
321     * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_foregroundScrimColor
322     * @see #getForegroundScrimColor()
323     */
324    public void setForegroundScrimColorResource(@ColorRes int resId) {
325        mForegroundScrimColor = getResources().getColor(resId);
326    }
327
328    /**
329     * Returns the color which is used for the foreground scrim.
330     *
331     * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_foregroundScrimColor
332     * @see #setForegroundScrimColor(int)
333     */
334    public int getForegroundScrimColor() {
335        return mForegroundScrimColor;
336    }
337
338    /**
339     * Sets the text color and size for the collapsed title from the specified
340     * TextAppearance resource.
341     *
342     * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_collapsedTitleTextAppearance
343     */
344    public void setCollapsedTitleTextAppearance(int resId) {
345        mCollapsingTextHelper.setCollapsedTextAppearance(resId);
346    }
347
348    /**
349     * Sets the text color of the collapsed title.
350     *
351     * @param color The new text color in ARGB format
352     */
353    public void setCollapsedTitleTextColor(int color) {
354        mCollapsingTextHelper.setCollapsedTextColor(color);
355    }
356
357    /**
358     * Sets the text color and size for the expanded title from the specified
359     * TextAppearance resource.
360     *
361     * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleTextAppearance
362     */
363    public void setExpandedTitleTextAppearance(int resId) {
364        mCollapsingTextHelper.setExpandedTextAppearance(resId);
365    }
366
367    /**
368     * Sets the text color of the expanded title.
369     *
370     * @param color The new text color in ARGB format
371     */
372    public void setExpandedTitleColor(int color) {
373        mCollapsingTextHelper.setExpandedTextColor(color);
374    }
375
376    final int getScrimTriggerOffset() {
377        return 2 * ViewCompat.getMinimumHeight(this);
378    }
379
380    @Override
381    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
382        return p instanceof LayoutParams;
383    }
384
385    @Override
386    protected LayoutParams generateDefaultLayoutParams() {
387        return new LayoutParams(super.generateDefaultLayoutParams());
388    }
389
390    @Override
391    public FrameLayout.LayoutParams generateLayoutParams(AttributeSet attrs) {
392        return new LayoutParams(getContext(), attrs);
393    }
394
395    @Override
396    protected FrameLayout.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
397        return new LayoutParams(p);
398    }
399
400    public static class LayoutParams extends FrameLayout.LayoutParams {
401
402        private static final float DEFAULT_PARALLAX_MULTIPLIER = 0.5f;
403
404        /**
405         * The view will act as normal with no collapsing behavior.
406         */
407        public static final int COLLAPSE_MODE_OFF = 0;
408
409        /**
410         * The view will pin in place until it reaches the bottom of the
411         * {@link CollapsingToolbarLayout}.
412         */
413        public static final int COLLAPSE_MODE_PIN = 1;
414
415        /**
416         * The view will scroll in a parallax fashion. See {@link #setParallaxMultiplier(float)}
417         * to change the multiplier used.
418         */
419        public static final int COLLAPSE_MODE_PARALLAX = 2;
420
421        int mCollapseMode = COLLAPSE_MODE_OFF;
422        float mParallaxMult = DEFAULT_PARALLAX_MULTIPLIER;
423
424        public LayoutParams(Context c, AttributeSet attrs) {
425            super(c, attrs);
426
427            TypedArray a = c.obtainStyledAttributes(attrs,
428                    R.styleable.CollapsingAppBarLayout_LayoutParams);
429            mCollapseMode = a.getInt(
430                    R.styleable.CollapsingAppBarLayout_LayoutParams_layout_collapseMode,
431                    COLLAPSE_MODE_OFF);
432            setParallaxMultiplier(a.getFloat(
433                    R.styleable.CollapsingAppBarLayout_LayoutParams_layout_collapseParallaxMultiplier,
434                    DEFAULT_PARALLAX_MULTIPLIER));
435            a.recycle();
436        }
437
438        public LayoutParams(int width, int height) {
439            super(width, height);
440        }
441
442        public LayoutParams(int width, int height, int gravity) {
443            super(width, height, gravity);
444        }
445
446        public LayoutParams(ViewGroup.LayoutParams p) {
447            super(p);
448        }
449
450        public LayoutParams(MarginLayoutParams source) {
451            super(source);
452        }
453
454        public LayoutParams(FrameLayout.LayoutParams source) {
455            super(source);
456        }
457
458        /**
459         * Set the collapse mode.
460         *
461         * @param collapseMode one of {@link #COLLAPSE_MODE_OFF}, {@link #COLLAPSE_MODE_PIN}
462         *                     or {@link #COLLAPSE_MODE_PARALLAX}.
463         */
464        public void setCollapseMode(int collapseMode) {
465            mCollapseMode = collapseMode;
466        }
467
468        /**
469         * Set the parallax scroll multiplier used in conjuction with
470         * {@link #COLLAPSE_MODE_PARALLAX}. A value of {@code 0.0} indicates no movement at all,
471         * {@code 1.0f} indicates normal scroll movement.
472         *
473         * @param multiplier the multiplier.
474         */
475        public void setParallaxMultiplier(float multiplier) {
476            mParallaxMult = multiplier;
477        }
478    }
479}
480