Skip to content

Instantly share code, notes, and snippets.

@marlonlom
Forked from chrisbanes/CollapsingTitleLayout.java
Last active August 29, 2015 14:12

Revisions

  1. @chrisbanes chrisbanes revised this gist Dec 18, 2014. 2 changed files with 17 additions and 4 deletions.
    20 changes: 16 additions & 4 deletions CollapsingTitleLayout.java
    Original file line number Diff line number Diff line change
    @@ -14,7 +14,7 @@
    * limitations under the License.
    */

    package app.philm.in.view;
    package your.package;

    import android.content.Context;
    import android.content.res.TypedArray;
    @@ -33,9 +33,11 @@
    import android.util.TypedValue;
    import android.view.View;
    import android.view.ViewGroup;
    import android.view.animation.AnimationUtils;
    import android.view.animation.Interpolator;
    import android.widget.FrameLayout;

    import your.package;
    import app.philm.in.R;

    public class CollapsingTitleLayout extends FrameLayout {

    @@ -85,6 +87,8 @@ public class CollapsingTitleLayout extends FrameLayout {
    private final TextPaint mTextPaint;
    private Paint mTexturePaint;

    private Interpolator mTextSizeInterpolator;

    public CollapsingTitleLayout(Context context) {
    this(context, null);
    }
    @@ -141,6 +145,11 @@ public CollapsingTitleLayout(Context context, AttributeSet attrs, int defStyleAt
    mRequestedExpandedTitleTextSize = a.getDimensionPixelSize(
    R.styleable.CollapsingTitleLayout_expandedTextSize, mCollapsedTitleTextSize);

    final int interpolatorId = a
    .getResourceId(R.styleable.CollapsingTitleLayout_textSizeInterpolator,
    android.R.anim.accelerate_interpolator);
    mTextSizeInterpolator = AnimationUtils.loadInterpolator(context, interpolatorId);

    a.recycle();

    mToolbarContentBounds = new Rect();
    @@ -187,13 +196,16 @@ public void setScrollOffset(float offset) {

    private void calculateOffsets() {
    final float offset = mScrollOffset;
    final float textSizeOffset = mTextSizeInterpolator != null
    ? mTextSizeInterpolator.getInterpolation(mScrollOffset)
    : offset;

    mTextLeft = interpolate(mExpandedMarginLeft, mToolbarContentBounds.left, offset);
    mTextTop = interpolate(mExpandedTop, mCollapsedTop, offset);
    mTextRight = interpolate(getWidth() - mExpandedMarginRight, mToolbarContentBounds.right, offset);

    setInterpolatedTextSize(interpolate(mExpandedTitleTextSize,
    mCollapsedTitleTextSize, offset));
    setInterpolatedTextSize(
    interpolate(mExpandedTitleTextSize, mCollapsedTitleTextSize, textSizeOffset));

    ViewCompat.postInvalidateOnAnimation(this);
    }
    1 change: 1 addition & 0 deletions attrs.xml
    Original file line number Diff line number Diff line change
    @@ -25,6 +25,7 @@
    <attr name="expandedTextSize" format="reference|dimension" />
    <attr name="collapsedTextSize" format="reference|dimension" />
    <attr name="android:textAppearance" />
    <attr name="textSizeInterpolator" format="reference" />
    </declare-styleable>

    <declare-styleable name="CollapsingTextAppearance">
  2. @chrisbanes chrisbanes revised this gist Dec 15, 2014. 3 changed files with 104 additions and 75 deletions.
    163 changes: 93 additions & 70 deletions CollapsingTitleLayout.java
    Original file line number Diff line number Diff line change
    @@ -35,12 +35,10 @@
    import android.view.ViewGroup;
    import android.widget.FrameLayout;

    import app.philm.in.R;
    import your.package;

    public class CollapsingTitleLayout extends FrameLayout {

    private static final float DEFAULT_MIN_TEXT_SIZE = 12f; // 12dp

    // Pre-JB-MR2 doesn't support HW accelerated canvas scaled text so we will workaround it
    // by using our own texture
    private static final boolean USE_SCALING_TEXTURE = Build.VERSION.SDK_INT < 18;
    @@ -55,24 +53,20 @@ public class CollapsingTitleLayout extends FrameLayout {
    }
    }

    private static final Rect TEMP_RECT = new Rect();

    private Toolbar mToolbar;
    private View mDummyView;

    private float mScrollOffset;

    private final Rect mToolbarContentBounds;
    private final Rect mTextPaintBounds;
    private final Rect mDrawnTextBounds;

    private float mMinTextSize;
    private float mExpandedMarginLeft;
    private float mExpandedMarginRight;
    private float mExpandedMarginBottom;

    private float mExpandedMargin;
    private float mRequestedExpandedTitleTextSize;
    private float mExpandedTitleTextSize;
    private float mRequestedCollapsedTitleTextSize;
    private float mCollapsedTitleTextSize;
    private int mRequestedExpandedTitleTextSize;
    private int mExpandedTitleTextSize;
    private int mCollapsedTitleTextSize;

    private float mExpandedTop;
    private float mCollapsedTop;
    @@ -105,35 +99,67 @@ public CollapsingTitleLayout(Context context, AttributeSet attrs, int defStyleAt
    mTextPaint = new TextPaint();
    mTextPaint.setAntiAlias(true);

    mMinTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULT_MIN_TEXT_SIZE,
    getResources().getDisplayMetrics());

    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CollapsingTitleLayout);

    mExpandedMargin = a.getDimensionPixelSize(
    R.styleable.CollapsingTitleLayout_expandedMargin, 0);
    mExpandedMarginLeft = mExpandedMarginRight = mExpandedMarginBottom =
    a.getDimensionPixelSize(R.styleable.CollapsingTitleLayout_expandedMargin, 0);

    final boolean isRtl = ViewCompat.getLayoutDirection(this)
    == ViewCompat.LAYOUT_DIRECTION_RTL;
    if (a.hasValue(R.styleable.CollapsingTitleLayout_expandedMarginStart)) {
    final int marginStart = a.getDimensionPixelSize(
    R.styleable.CollapsingTitleLayout_expandedMarginStart, 0);
    if (isRtl) {
    mExpandedMarginRight = marginStart;
    } else {
    mExpandedMarginLeft = marginStart;
    }
    }
    if (a.hasValue(R.styleable.CollapsingTitleLayout_expandedMarginEnd)) {
    final int marginEnd = a.getDimensionPixelSize(
    R.styleable.CollapsingTitleLayout_expandedMarginEnd, 0);
    if (isRtl) {
    mExpandedMarginLeft = marginEnd;
    } else {
    mExpandedMarginRight = marginEnd;
    }
    }
    if (a.hasValue(R.styleable.CollapsingTitleLayout_expandedMarginBottom)) {
    mExpandedMarginBottom = a.getDimensionPixelSize(
    R.styleable.CollapsingTitleLayout_expandedMarginBottom, 0);
    }

    final int tp = a.getResourceId(R.styleable.CollapsingTitleLayout_android_textAppearance,
    android.R.style.TextAppearance);
    setTextAppearance(tp);

    if (a.hasValue(R.styleable.CollapsingTitleLayout_collapsedTextSize)) {
    mCollapsedTitleTextSize = a.getDimensionPixelSize(
    R.styleable.CollapsingTitleLayout_collapsedTextSize, 0);
    }

    mRequestedExpandedTitleTextSize = a.getDimensionPixelSize(
    R.styleable.CollapsingTitleLayout_expandedTextSize, 0);
    mRequestedCollapsedTitleTextSize = a.getDimensionPixelSize(
    R.styleable.CollapsingTitleLayout_collapsedTextSize, 0);
    mTextPaint.setColor(a.getColor(
    R.styleable.CollapsingTitleLayout_android_textColor, Color.WHITE));

    final int defaultMinTextSize = (int) TypedValue
    .applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULT_MIN_TEXT_SIZE,
    getResources().getDisplayMetrics());
    mMinTextSize = a.getDimensionPixelSize(R.styleable.CollapsingTitleLayout_minTextSize,
    defaultMinTextSize);
    R.styleable.CollapsingTitleLayout_expandedTextSize, mCollapsedTitleTextSize);

    a.recycle();

    mTextPaintBounds = new Rect();
    mDrawnTextBounds = new Rect();
    mToolbarContentBounds = new Rect();

    setWillNotDraw(false);
    }

    public void setTextAppearance(int resId) {
    TypedArray atp = getContext().obtainStyledAttributes(resId,
    R.styleable.CollapsingTextAppearance);
    mTextPaint.setColor(atp.getColor(
    R.styleable.CollapsingTextAppearance_android_textColor, Color.WHITE));
    mCollapsedTitleTextSize = atp.getDimensionPixelSize(
    R.styleable.CollapsingTextAppearance_android_textSize, 0);
    atp.recycle();

    recalculate();
    }

    @Override
    public void addView(View child, int index, ViewGroup.LayoutParams params) {
    super.addView(child, index, params);
    @@ -162,9 +188,9 @@ public void setScrollOffset(float offset) {
    private void calculateOffsets() {
    final float offset = mScrollOffset;

    mTextLeft = interpolate(mExpandedMargin, mToolbarContentBounds.left, offset);
    mTextLeft = interpolate(mExpandedMarginLeft, mToolbarContentBounds.left, offset);
    mTextTop = interpolate(mExpandedTop, mCollapsedTop, offset);
    mTextRight = interpolate(getWidth() - mExpandedMargin, mToolbarContentBounds.right, offset);
    mTextRight = interpolate(getWidth() - mExpandedMarginRight, mToolbarContentBounds.right, offset);

    setInterpolatedTextSize(interpolate(mExpandedTitleTextSize,
    mCollapsedTitleTextSize, offset));
    @@ -175,22 +201,19 @@ private void calculateOffsets() {
    private void calculateTextBounds() {
    final DisplayMetrics metrics = getResources().getDisplayMetrics();

    // We then calculate the collapsed text size, using the same logic
    mTextPaint.setTextSize(mCollapsedTitleTextSize);
    float textHeight = mTextPaint.descent() - mTextPaint.ascent();
    float textOffset = (textHeight / 2) - mTextPaint.descent();
    mCollapsedTop = mToolbarContentBounds.centerY() + textOffset;

    // First, let's calculate the expanded text size so that it fit within the bounds
    // We make sure this value is at least our minimum text size
    mExpandedTitleTextSize = Math.max(mMinTextSize,
    getSingleLineTextSize(mTitle, mTextPaint, getWidth() - (mExpandedMargin * 2f), 0f,
    mExpandedTitleTextSize = (int) Math.max(mCollapsedTitleTextSize,
    getSingleLineTextSize(mTitle, mTextPaint,
    getWidth() - mExpandedMarginLeft -mExpandedMarginRight, 0f,
    mRequestedExpandedTitleTextSize, 0.5f, metrics));
    mTextPaint.setTextSize(mExpandedTitleTextSize);
    mTextPaint.getTextBounds(mTitle, 0, mTitle.length(), TEMP_RECT);
    mExpandedTop = getHeight() - TEMP_RECT.height() - mExpandedMargin;

    // We then calculate the collapsed text size, using the same logic
    mCollapsedTitleTextSize = Math.max(mMinTextSize,
    getSingleLineTextSize(mTitle, mTextPaint, mToolbarContentBounds.width(), 0f,
    mRequestedCollapsedTitleTextSize, 0.5f, metrics));
    mTextPaint.setTextSize(mCollapsedTitleTextSize);
    mTextPaint.getTextBounds(mTitle, 0, mTitle.length(), TEMP_RECT);
    mCollapsedTop = mToolbarContentBounds.centerY() - (TEMP_RECT.height() / 2f);
    mExpandedTop = getHeight() - mExpandedMarginBottom;

    // The bounds have changed so we need to clear the texture
    clearTexture();
    @@ -211,19 +234,23 @@ public void draw(Canvas canvas) {
    float x = mTextLeft;
    float y = mTextTop;

    if (!mUseTexture) {
    // If we're not drawing a texture, we need to properly offset the text
    x -= mDrawnTextBounds.left;
    y -= mDrawnTextBounds.top;
    }
    final float ascent = mTextPaint.ascent() * mScale;
    final float descent = mTextPaint.descent() * mScale;
    final float h = descent - ascent;

    if (DEBUG_DRAW) {
    // Just a debug tool, which drawn a Magneta rect in the text bounds
    canvas.drawRect(mTextLeft, mTextTop, mTextRight,
    mTextTop + mDrawnTextBounds.height(),
    canvas.drawRect(mTextLeft,
    y - h + descent,
    mTextRight,
    y + descent,
    DEBUG_DRAW_PAINT);
    }

    if (mUseTexture) {
    y = y - h + descent;
    }

    if (mScale != 1f) {
    canvas.scale(mScale, mScale, x, y);
    }
    @@ -251,17 +278,13 @@ private void setInterpolatedTextSize(final float textSize) {

    // We also use this as an opportunity to ellipsize the string
    final CharSequence title = TextUtils.ellipsize(mTitle, mTextPaint,
    (mTextRight - mTextLeft),
    mTextRight - mTextLeft,
    TextUtils.TruncateAt.END);
    if (title != mTitleToDraw) {
    // If the title has changed, turn it into a string
    mTitleToDraw = title.toString();
    }

    // As we've changed the text size (and possibly the text) we'll re-measure the text
    mTextPaint.getTextBounds(mTitleToDraw, 0, mTitleToDraw.length(), mTextPaintBounds);
    mDrawnTextBounds.set(mTextPaintBounds);

    if (USE_SCALING_TEXTURE && isClose(textSize, mExpandedTitleTextSize)) {
    ensureExpandedTexture();
    }
    @@ -274,13 +297,6 @@ private void setInterpolatedTextSize(final float textSize) {
    mScale = textSize / mTextPaint.getTextSize();
    }

    // Because we're scaling using canvas, we need to update the drawn text bounds too
    mDrawnTextBounds.set(mTextPaintBounds);
    mDrawnTextBounds.left *= mScale;
    mDrawnTextBounds.top *= mScale;
    mDrawnTextBounds.right *= mScale;
    mDrawnTextBounds.bottom *= mScale;

    mUseTexture = USE_SCALING_TEXTURE;
    }

    @@ -290,11 +306,13 @@ private void setInterpolatedTextSize(final float textSize) {
    private void ensureExpandedTexture() {
    if (mExpandedTitleTexture != null) return;

    mExpandedTitleTexture = Bitmap.createBitmap(mTextPaintBounds.width(),
    mTextPaintBounds.height(), Bitmap.Config.ARGB_8888);
    int w = (int) (getWidth() - mExpandedMarginLeft - mExpandedMarginRight);
    int h = (int) (mTextPaint.descent() - mTextPaint.ascent());

    mExpandedTitleTexture = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);

    Canvas c = new Canvas(mExpandedTitleTexture);
    c.drawText(mTitleToDraw, -mTextPaintBounds.left, -mTextPaintBounds.top, mTextPaint);
    c.drawText(mTitleToDraw, 0, h - mTextPaint.descent(), mTextPaint);

    if (mTexturePaint == null) {
    // Make sure we have a paint
    @@ -315,6 +333,12 @@ protected void onLayout(boolean changed, int left, int top, int right, int botto

    if (changed && mTitle != null) {
    // If we've changed and we have a title, re-calculate everything!
    recalculate();
    }
    }

    private void recalculate() {
    if (getHeight() > 0) {
    calculateTextBounds();
    calculateOffsets();
    }
    @@ -334,8 +358,7 @@ public void setTitle(String title) {
    if (getHeight() > 0) {
    // If we've already been laid out, calculate everything now otherwise we'll wait
    // until a layout
    calculateTextBounds();
    calculateOffsets();
    recalculate();
    }
    }
    }
    9 changes: 8 additions & 1 deletion attrs.xml
    Original file line number Diff line number Diff line change
    @@ -19,9 +19,16 @@

    <declare-styleable name="CollapsingTitleLayout">
    <attr name="expandedMargin" format="reference|dimension" />
    <attr name="expandedMarginStart" format="reference|dimension" />
    <attr name="expandedMarginBottom" format="reference|dimension" />
    <attr name="expandedMarginEnd" format="reference|dimension" />
    <attr name="expandedTextSize" format="reference|dimension" />
    <attr name="collapsedTextSize" format="reference|dimension" />
    <attr name="minTextSize" format="reference|dimension" />
    <attr name="android:textAppearance" />
    </declare-styleable>

    <declare-styleable name="CollapsingTextAppearance">
    <attr name="android:textSize" />
    <attr name="android:textColor" />
    </declare-styleable>

    7 changes: 3 additions & 4 deletions example_layout.xml
    Original file line number Diff line number Diff line change
    @@ -27,10 +27,9 @@
    android:id="@+id/backdrop_toolbar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:collapsedTextSize="16dp"
    app:expandedTextSize="32sp"
    app:expandedMargin="16sp"
    android:textColor="?android:attr/textColorPrimaryInverse">
    android:textAppearance="@style/TextAppearance.AppCompat.Widget.ActionBar.Title.Inverse"
    app:expandedTextSize="40dp"
    app:expandedMargin="16dp">

    <android.support.v7.widget.Toolbar
    android:id="@+id/toolbar"
  3. @chrisbanes chrisbanes revised this gist Dec 8, 2014. 1 changed file with 42 additions and 0 deletions.
    42 changes: 42 additions & 0 deletions example_layout.xml
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,42 @@
    <?xml version="1.0" encoding="utf-8"?>

    <!--
    ~ Copyright 2014 Chris Banes
    ~
    ~ Licensed under the Apache License, Version 2.0 (the "License");
    ~ you may not use this file except in compliance with the License.
    ~ You may obtain a copy of the License at
    ~
    ~ http://www.apache.org/licenses/LICENSE-2.0
    ~
    ~ Unless required by applicable law or agreed to in writing, software
    ~ distributed under the License is distributed on an "AS IS" BASIS,
    ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    ~ See the License for the specific language governing permissions and
    ~ limitations under the License.
    -->

    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_height="match_parent"
    android:layout_width="match_parent">

    <!-- Your content, maybe a ListView? -->

    <app.philm.in.view.CollapsingTitleLayout
    android:id="@+id/backdrop_toolbar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:collapsedTextSize="16dp"
    app:expandedTextSize="32sp"
    app:expandedMargin="16sp"
    android:textColor="?android:attr/textColorPrimaryInverse">

    <android.support.v7.widget.Toolbar
    android:id="@+id/toolbar"
    android:layout_height="?attr/actionBarSize"
    android:layout_width="match_parent" />

    </app.philm.in.view.CollapsingTitleLayout>

    </FrameLayout>
  4. @chrisbanes chrisbanes created this gist Dec 8, 2014.
    387 changes: 387 additions & 0 deletions CollapsingTitleLayout.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,387 @@
    /*
    * Copyright 2014 Chris Banes
    *
    * Licensed under the Apache License, Version 2.0 (the "License");
    * you may not use this file except in compliance with the License.
    * You may obtain a copy of the License at
    *
    * http://www.apache.org/licenses/LICENSE-2.0
    *
    * Unless required by applicable law or agreed to in writing, software
    * distributed under the License is distributed on an "AS IS" BASIS,
    * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    * See the License for the specific language governing permissions and
    * limitations under the License.
    */

    package app.philm.in.view;

    import android.content.Context;
    import android.content.res.TypedArray;
    import android.graphics.Bitmap;
    import android.graphics.Canvas;
    import android.graphics.Color;
    import android.graphics.Paint;
    import android.graphics.Rect;
    import android.os.Build;
    import android.support.v4.view.ViewCompat;
    import android.support.v7.widget.Toolbar;
    import android.text.TextPaint;
    import android.text.TextUtils;
    import android.util.AttributeSet;
    import android.util.DisplayMetrics;
    import android.util.TypedValue;
    import android.view.View;
    import android.view.ViewGroup;
    import android.widget.FrameLayout;

    import app.philm.in.R;

    public class CollapsingTitleLayout extends FrameLayout {

    private static final float DEFAULT_MIN_TEXT_SIZE = 12f; // 12dp

    // Pre-JB-MR2 doesn't support HW accelerated canvas scaled text so we will workaround it
    // by using our own texture
    private static final boolean USE_SCALING_TEXTURE = Build.VERSION.SDK_INT < 18;

    private static final boolean DEBUG_DRAW = false;
    private static final Paint DEBUG_DRAW_PAINT;
    static {
    DEBUG_DRAW_PAINT = DEBUG_DRAW ? new Paint() : null;
    if (DEBUG_DRAW_PAINT != null) {
    DEBUG_DRAW_PAINT.setAntiAlias(true);
    DEBUG_DRAW_PAINT.setColor(Color.MAGENTA);
    }
    }

    private static final Rect TEMP_RECT = new Rect();

    private Toolbar mToolbar;
    private View mDummyView;

    private float mScrollOffset;

    private final Rect mToolbarContentBounds;
    private final Rect mTextPaintBounds;
    private final Rect mDrawnTextBounds;

    private float mMinTextSize;

    private float mExpandedMargin;
    private float mRequestedExpandedTitleTextSize;
    private float mExpandedTitleTextSize;
    private float mRequestedCollapsedTitleTextSize;
    private float mCollapsedTitleTextSize;

    private float mExpandedTop;
    private float mCollapsedTop;

    private String mTitle;
    private String mTitleToDraw;
    private boolean mUseTexture;
    private Bitmap mExpandedTitleTexture;

    private float mTextLeft;
    private float mTextRight;
    private float mTextTop;

    private float mScale;

    private final TextPaint mTextPaint;
    private Paint mTexturePaint;

    public CollapsingTitleLayout(Context context) {
    this(context, null);
    }

    public CollapsingTitleLayout(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
    }

    public CollapsingTitleLayout(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);

    mTextPaint = new TextPaint();
    mTextPaint.setAntiAlias(true);

    mMinTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULT_MIN_TEXT_SIZE,
    getResources().getDisplayMetrics());

    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CollapsingTitleLayout);

    mExpandedMargin = a.getDimensionPixelSize(
    R.styleable.CollapsingTitleLayout_expandedMargin, 0);
    mRequestedExpandedTitleTextSize = a.getDimensionPixelSize(
    R.styleable.CollapsingTitleLayout_expandedTextSize, 0);
    mRequestedCollapsedTitleTextSize = a.getDimensionPixelSize(
    R.styleable.CollapsingTitleLayout_collapsedTextSize, 0);
    mTextPaint.setColor(a.getColor(
    R.styleable.CollapsingTitleLayout_android_textColor, Color.WHITE));

    final int defaultMinTextSize = (int) TypedValue
    .applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULT_MIN_TEXT_SIZE,
    getResources().getDisplayMetrics());
    mMinTextSize = a.getDimensionPixelSize(R.styleable.CollapsingTitleLayout_minTextSize,
    defaultMinTextSize);

    a.recycle();

    mTextPaintBounds = new Rect();
    mDrawnTextBounds = new Rect();
    mToolbarContentBounds = new Rect();

    setWillNotDraw(false);
    }

    @Override
    public void addView(View child, int index, ViewGroup.LayoutParams params) {
    super.addView(child, index, params);

    if (child instanceof Toolbar) {
    mToolbar = (Toolbar) child;
    mDummyView = new View(getContext());
    mToolbar.addView(mDummyView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    }
    }

    /**
    * Set the value indicating the current scroll value. This decides how much of the
    * background will be displayed, as well as the title metrics/positioning.
    *
    * A value of {@code 0.0} indicates that the layout is fully expanded.
    * A value of {@code 1.0} indicates that the layout is fully collapsed.
    */
    public void setScrollOffset(float offset) {
    if (offset != mScrollOffset) {
    mScrollOffset = offset;
    calculateOffsets();
    }
    }

    private void calculateOffsets() {
    final float offset = mScrollOffset;

    mTextLeft = interpolate(mExpandedMargin, mToolbarContentBounds.left, offset);
    mTextTop = interpolate(mExpandedTop, mCollapsedTop, offset);
    mTextRight = interpolate(getWidth() - mExpandedMargin, mToolbarContentBounds.right, offset);

    setInterpolatedTextSize(interpolate(mExpandedTitleTextSize,
    mCollapsedTitleTextSize, offset));

    ViewCompat.postInvalidateOnAnimation(this);
    }

    private void calculateTextBounds() {
    final DisplayMetrics metrics = getResources().getDisplayMetrics();

    // First, let's calculate the expanded text size so that it fit within the bounds
    // We make sure this value is at least our minimum text size
    mExpandedTitleTextSize = Math.max(mMinTextSize,
    getSingleLineTextSize(mTitle, mTextPaint, getWidth() - (mExpandedMargin * 2f), 0f,
    mRequestedExpandedTitleTextSize, 0.5f, metrics));
    mTextPaint.setTextSize(mExpandedTitleTextSize);
    mTextPaint.getTextBounds(mTitle, 0, mTitle.length(), TEMP_RECT);
    mExpandedTop = getHeight() - TEMP_RECT.height() - mExpandedMargin;

    // We then calculate the collapsed text size, using the same logic
    mCollapsedTitleTextSize = Math.max(mMinTextSize,
    getSingleLineTextSize(mTitle, mTextPaint, mToolbarContentBounds.width(), 0f,
    mRequestedCollapsedTitleTextSize, 0.5f, metrics));
    mTextPaint.setTextSize(mCollapsedTitleTextSize);
    mTextPaint.getTextBounds(mTitle, 0, mTitle.length(), TEMP_RECT);
    mCollapsedTop = mToolbarContentBounds.centerY() - (TEMP_RECT.height() / 2f);

    // The bounds have changed so we need to clear the texture
    clearTexture();
    }

    @Override
    public void draw(Canvas canvas) {
    final int saveCount = canvas.save();

    final int toolbarHeight = mToolbar.getHeight();
    canvas.clipRect(0, 0, canvas.getWidth(),
    interpolate(canvas.getHeight(), toolbarHeight, mScrollOffset));

    // Now call super and let it draw the background, etc
    super.draw(canvas);

    if (mTitleToDraw != null) {
    float x = mTextLeft;
    float y = mTextTop;

    if (!mUseTexture) {
    // If we're not drawing a texture, we need to properly offset the text
    x -= mDrawnTextBounds.left;
    y -= mDrawnTextBounds.top;
    }

    if (DEBUG_DRAW) {
    // Just a debug tool, which drawn a Magneta rect in the text bounds
    canvas.drawRect(mTextLeft, mTextTop, mTextRight,
    mTextTop + mDrawnTextBounds.height(),
    DEBUG_DRAW_PAINT);
    }

    if (mScale != 1f) {
    canvas.scale(mScale, mScale, x, y);
    }

    if (mUseTexture && mExpandedTitleTexture != null) {
    // If we should use a texture, draw it instead of text
    canvas.drawBitmap(mExpandedTitleTexture, x, y, mTexturePaint);
    } else {
    canvas.drawText(mTitleToDraw, x, y, mTextPaint);
    }
    }

    canvas.restoreToCount(saveCount);
    }

    private void setInterpolatedTextSize(final float textSize) {
    if (mTitle == null) return;

    if (isClose(textSize, mCollapsedTitleTextSize) || isClose(textSize, mExpandedTitleTextSize)
    || mTitleToDraw == null) {
    // If the text size is 'close' to being a decimal, then we use this as a sync-point.
    // We disable our manual scaling and set the paint's text size.
    mTextPaint.setTextSize(textSize);
    mScale = 1f;

    // We also use this as an opportunity to ellipsize the string
    final CharSequence title = TextUtils.ellipsize(mTitle, mTextPaint,
    (mTextRight - mTextLeft),
    TextUtils.TruncateAt.END);
    if (title != mTitleToDraw) {
    // If the title has changed, turn it into a string
    mTitleToDraw = title.toString();
    }

    // As we've changed the text size (and possibly the text) we'll re-measure the text
    mTextPaint.getTextBounds(mTitleToDraw, 0, mTitleToDraw.length(), mTextPaintBounds);
    mDrawnTextBounds.set(mTextPaintBounds);

    if (USE_SCALING_TEXTURE && isClose(textSize, mExpandedTitleTextSize)) {
    ensureExpandedTexture();
    }
    mUseTexture = false;
    } else {
    // We're not close to a decimal so use our canvas scaling method
    if (mExpandedTitleTexture != null) {
    mScale = textSize / mExpandedTitleTextSize;
    } else {
    mScale = textSize / mTextPaint.getTextSize();
    }

    // Because we're scaling using canvas, we need to update the drawn text bounds too
    mDrawnTextBounds.set(mTextPaintBounds);
    mDrawnTextBounds.left *= mScale;
    mDrawnTextBounds.top *= mScale;
    mDrawnTextBounds.right *= mScale;
    mDrawnTextBounds.bottom *= mScale;

    mUseTexture = USE_SCALING_TEXTURE;
    }

    ViewCompat.postInvalidateOnAnimation(this);
    }

    private void ensureExpandedTexture() {
    if (mExpandedTitleTexture != null) return;

    mExpandedTitleTexture = Bitmap.createBitmap(mTextPaintBounds.width(),
    mTextPaintBounds.height(), Bitmap.Config.ARGB_8888);

    Canvas c = new Canvas(mExpandedTitleTexture);
    c.drawText(mTitleToDraw, -mTextPaintBounds.left, -mTextPaintBounds.top, mTextPaint);

    if (mTexturePaint == null) {
    // Make sure we have a paint
    mTexturePaint = new Paint();
    mTexturePaint.setAntiAlias(true);
    mTexturePaint.setFilterBitmap(true);
    }
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    super.onLayout(changed, left, top, right, bottom);

    mToolbarContentBounds.left = mDummyView.getLeft();
    mToolbarContentBounds.top = mDummyView.getTop();
    mToolbarContentBounds.right = mDummyView.getRight();
    mToolbarContentBounds.bottom = mDummyView.getBottom();

    if (changed && mTitle != null) {
    // If we've changed and we have a title, re-calculate everything!
    calculateTextBounds();
    calculateOffsets();
    }
    }

    /**
    * Set the title to display
    *
    * @param title
    */
    public void setTitle(String title) {
    if (title == null || !title.equals(mTitle)) {
    mTitle = title;

    clearTexture();

    if (getHeight() > 0) {
    // If we've already been laid out, calculate everything now otherwise we'll wait
    // until a layout
    calculateTextBounds();
    calculateOffsets();
    }
    }
    }

    private void clearTexture() {
    if (mExpandedTitleTexture != null) {
    mExpandedTitleTexture.recycle();
    mExpandedTitleTexture = null;
    }
    }

    /**
    * Recursive binary search to find the best size for the text
    *
    * Adapted from https://github.com/grantland/android-autofittextview
    */
    private static float getSingleLineTextSize(String text, TextPaint paint, float targetWidth,
    float low, float high, float precision, DisplayMetrics metrics) {
    final float mid = (low + high) / 2.0f;

    paint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, mid, metrics));
    final float maxLineWidth = paint.measureText(text);

    if ((high - low) < precision) {
    return low;
    } else if (maxLineWidth > targetWidth) {
    return getSingleLineTextSize(text, paint, targetWidth, low, mid, precision, metrics);
    } else if (maxLineWidth < targetWidth) {
    return getSingleLineTextSize(text, paint, targetWidth, mid, high, precision, metrics);
    } else {
    return mid;
    }
    }

    /**
    * Returns true if {@code value} is 'close' to it's closest decimal value. Close is currently
    * defined as it's difference being < 0.01.
    */
    private static boolean isClose(float value, float targetValue) {
    return Math.abs(value - targetValue) < 0.01f;
    }

    /**
    * Interpolate between {@code startValue} and {@code endValue}, using {@code progress}.
    */
    private static float interpolate(float startValue, float endValue, float progress) {
    return startValue + ((endValue - startValue) * progress);
    }
    }
    28 changes: 28 additions & 0 deletions attrs.xml
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,28 @@
    <?xml version="1.0" encoding="utf-8"?>
    <!--
    ~ Copyright 2014 Chris Banes
    ~
    ~ Licensed under the Apache License, Version 2.0 (the "License");
    ~ you may not use this file except in compliance with the License.
    ~ You may obtain a copy of the License at
    ~
    ~ http://www.apache.org/licenses/LICENSE-2.0
    ~
    ~ Unless required by applicable law or agreed to in writing, software
    ~ distributed under the License is distributed on an "AS IS" BASIS,
    ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    ~ See the License for the specific language governing permissions and
    ~ limitations under the License.
    -->

    <resources>

    <declare-styleable name="CollapsingTitleLayout">
    <attr name="expandedMargin" format="reference|dimension" />
    <attr name="expandedTextSize" format="reference|dimension" />
    <attr name="collapsedTextSize" format="reference|dimension" />
    <attr name="minTextSize" format="reference|dimension" />
    <attr name="android:textColor" />
    </declare-styleable>

    </resources>