CollapsingTextHelper.java revision ad1b0e82100ee31e70040d77bfa4d847b2bf0864
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.res.ColorStateList;
20import android.content.res.TypedArray;
21import android.graphics.Bitmap;
22import android.graphics.Canvas;
23import android.graphics.Color;
24import android.graphics.Paint;
25import android.graphics.Rect;
26import android.graphics.RectF;
27import android.graphics.Typeface;
28import android.os.Build;
29import android.support.annotation.ColorInt;
30import android.support.v4.math.MathUtils;
31import android.support.v4.text.TextDirectionHeuristicsCompat;
32import android.support.v4.view.GravityCompat;
33import android.support.v4.view.ViewCompat;
34import android.support.v7.widget.TintTypedArray;
35import android.text.TextPaint;
36import android.text.TextUtils;
37import android.view.Gravity;
38import android.view.View;
39import android.view.animation.Interpolator;
40
41final class CollapsingTextHelper {
42
43    // Pre-JB-MR2 doesn't support HW accelerated canvas scaled text so we will workaround it
44    // by using our own texture
45    private static final boolean USE_SCALING_TEXTURE = Build.VERSION.SDK_INT < 18;
46
47    private static final boolean DEBUG_DRAW = false;
48    private static final Paint DEBUG_DRAW_PAINT;
49    static {
50        DEBUG_DRAW_PAINT = DEBUG_DRAW ? new Paint() : null;
51        if (DEBUG_DRAW_PAINT != null) {
52            DEBUG_DRAW_PAINT.setAntiAlias(true);
53            DEBUG_DRAW_PAINT.setColor(Color.MAGENTA);
54        }
55    }
56
57    private final View mView;
58
59    private boolean mDrawTitle;
60    private float mExpandedFraction;
61
62    private final Rect mExpandedBounds;
63    private final Rect mCollapsedBounds;
64    private final RectF mCurrentBounds;
65    private int mExpandedTextGravity = Gravity.CENTER_VERTICAL;
66    private int mCollapsedTextGravity = Gravity.CENTER_VERTICAL;
67    private float mExpandedTextSize = 15;
68    private float mCollapsedTextSize = 15;
69    private ColorStateList mExpandedTextColor;
70    private ColorStateList mCollapsedTextColor;
71
72    private float mExpandedDrawY;
73    private float mCollapsedDrawY;
74    private float mExpandedDrawX;
75    private float mCollapsedDrawX;
76    private float mCurrentDrawX;
77    private float mCurrentDrawY;
78    private Typeface mCollapsedTypeface;
79    private Typeface mExpandedTypeface;
80    private Typeface mCurrentTypeface;
81
82    private CharSequence mText;
83    private CharSequence mTextToDraw;
84    private boolean mIsRtl;
85
86    private boolean mUseTexture;
87    private Bitmap mExpandedTitleTexture;
88    private Paint mTexturePaint;
89    private float mTextureAscent;
90    private float mTextureDescent;
91
92    private float mScale;
93    private float mCurrentTextSize;
94
95    private int[] mState;
96
97    private boolean mBoundsChanged;
98
99    private final TextPaint mTextPaint;
100
101    private Interpolator mPositionInterpolator;
102    private Interpolator mTextSizeInterpolator;
103
104    private float mCollapsedShadowRadius, mCollapsedShadowDx, mCollapsedShadowDy;
105    private int mCollapsedShadowColor;
106
107    private float mExpandedShadowRadius, mExpandedShadowDx, mExpandedShadowDy;
108    private int mExpandedShadowColor;
109
110    public CollapsingTextHelper(View view) {
111        mView = view;
112
113        mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.SUBPIXEL_TEXT_FLAG);
114
115        mCollapsedBounds = new Rect();
116        mExpandedBounds = new Rect();
117        mCurrentBounds = new RectF();
118    }
119
120    void setTextSizeInterpolator(Interpolator interpolator) {
121        mTextSizeInterpolator = interpolator;
122        recalculate();
123    }
124
125    void setPositionInterpolator(Interpolator interpolator) {
126        mPositionInterpolator = interpolator;
127        recalculate();
128    }
129
130    void setExpandedTextSize(float textSize) {
131        if (mExpandedTextSize != textSize) {
132            mExpandedTextSize = textSize;
133            recalculate();
134        }
135    }
136
137    void setCollapsedTextSize(float textSize) {
138        if (mCollapsedTextSize != textSize) {
139            mCollapsedTextSize = textSize;
140            recalculate();
141        }
142    }
143
144    void setCollapsedTextColor(ColorStateList textColor) {
145        if (mCollapsedTextColor != textColor) {
146            mCollapsedTextColor = textColor;
147            recalculate();
148        }
149    }
150
151    void setExpandedTextColor(ColorStateList textColor) {
152        if (mExpandedTextColor != textColor) {
153            mExpandedTextColor = textColor;
154            recalculate();
155        }
156    }
157
158    void setExpandedBounds(int left, int top, int right, int bottom) {
159        if (!rectEquals(mExpandedBounds, left, top, right, bottom)) {
160            mExpandedBounds.set(left, top, right, bottom);
161            mBoundsChanged = true;
162            onBoundsChanged();
163        }
164    }
165
166    void setCollapsedBounds(int left, int top, int right, int bottom) {
167        if (!rectEquals(mCollapsedBounds, left, top, right, bottom)) {
168            mCollapsedBounds.set(left, top, right, bottom);
169            mBoundsChanged = true;
170            onBoundsChanged();
171        }
172    }
173
174    void onBoundsChanged() {
175        mDrawTitle = mCollapsedBounds.width() > 0 && mCollapsedBounds.height() > 0
176                && mExpandedBounds.width() > 0 && mExpandedBounds.height() > 0;
177    }
178
179    void setExpandedTextGravity(int gravity) {
180        if (mExpandedTextGravity != gravity) {
181            mExpandedTextGravity = gravity;
182            recalculate();
183        }
184    }
185
186    int getExpandedTextGravity() {
187        return mExpandedTextGravity;
188    }
189
190    void setCollapsedTextGravity(int gravity) {
191        if (mCollapsedTextGravity != gravity) {
192            mCollapsedTextGravity = gravity;
193            recalculate();
194        }
195    }
196
197    int getCollapsedTextGravity() {
198        return mCollapsedTextGravity;
199    }
200
201    void setCollapsedTextAppearance(int resId) {
202        TintTypedArray a = TintTypedArray.obtainStyledAttributes(mView.getContext(), resId,
203                android.support.v7.appcompat.R.styleable.TextAppearance);
204        if (a.hasValue(android.support.v7.appcompat.R.styleable.TextAppearance_android_textColor)) {
205            mCollapsedTextColor = a.getColorStateList(
206                    android.support.v7.appcompat.R.styleable.TextAppearance_android_textColor);
207        }
208        if (a.hasValue(android.support.v7.appcompat.R.styleable.TextAppearance_android_textSize)) {
209            mCollapsedTextSize = a.getDimensionPixelSize(
210                    android.support.v7.appcompat.R.styleable.TextAppearance_android_textSize,
211                    (int) mCollapsedTextSize);
212        }
213        mCollapsedShadowColor = a.getInt(
214                android.support.v7.appcompat.R.styleable.TextAppearance_android_shadowColor, 0);
215        mCollapsedShadowDx = a.getFloat(
216                android.support.v7.appcompat.R.styleable.TextAppearance_android_shadowDx, 0);
217        mCollapsedShadowDy = a.getFloat(
218                android.support.v7.appcompat.R.styleable.TextAppearance_android_shadowDy, 0);
219        mCollapsedShadowRadius = a.getFloat(
220                android.support.v7.appcompat.R.styleable.TextAppearance_android_shadowRadius, 0);
221        a.recycle();
222
223        if (Build.VERSION.SDK_INT >= 16) {
224            mCollapsedTypeface = readFontFamilyTypeface(resId);
225        }
226
227        recalculate();
228    }
229
230    void setExpandedTextAppearance(int resId) {
231        TintTypedArray a = TintTypedArray.obtainStyledAttributes(mView.getContext(), resId,
232                android.support.v7.appcompat.R.styleable.TextAppearance);
233        if (a.hasValue(android.support.v7.appcompat.R.styleable.TextAppearance_android_textColor)) {
234            mExpandedTextColor = a.getColorStateList(
235                    android.support.v7.appcompat.R.styleable.TextAppearance_android_textColor);
236        }
237        if (a.hasValue(android.support.v7.appcompat.R.styleable.TextAppearance_android_textSize)) {
238            mExpandedTextSize = a.getDimensionPixelSize(
239                    android.support.v7.appcompat.R.styleable.TextAppearance_android_textSize,
240                    (int) mExpandedTextSize);
241        }
242        mExpandedShadowColor = a.getInt(
243                android.support.v7.appcompat.R.styleable.TextAppearance_android_shadowColor, 0);
244        mExpandedShadowDx = a.getFloat(
245                android.support.v7.appcompat.R.styleable.TextAppearance_android_shadowDx, 0);
246        mExpandedShadowDy = a.getFloat(
247                android.support.v7.appcompat.R.styleable.TextAppearance_android_shadowDy, 0);
248        mExpandedShadowRadius = a.getFloat(
249                android.support.v7.appcompat.R.styleable.TextAppearance_android_shadowRadius, 0);
250        a.recycle();
251
252        if (Build.VERSION.SDK_INT >= 16) {
253            mExpandedTypeface = readFontFamilyTypeface(resId);
254        }
255
256        recalculate();
257    }
258
259    private Typeface readFontFamilyTypeface(int resId) {
260        final TypedArray a = mView.getContext().obtainStyledAttributes(resId,
261                new int[]{android.R.attr.fontFamily});
262        try {
263            final String family = a.getString(0);
264            if (family != null) {
265                return Typeface.create(family, Typeface.NORMAL);
266            }
267        } finally {
268            a.recycle();
269        }
270        return null;
271    }
272
273    void setCollapsedTypeface(Typeface typeface) {
274        if (areTypefacesDifferent(mCollapsedTypeface, typeface)) {
275            mCollapsedTypeface = typeface;
276            recalculate();
277        }
278    }
279
280    void setExpandedTypeface(Typeface typeface) {
281        if (areTypefacesDifferent(mExpandedTypeface, typeface)) {
282            mExpandedTypeface = typeface;
283            recalculate();
284        }
285    }
286
287    void setTypefaces(Typeface typeface) {
288        mCollapsedTypeface = mExpandedTypeface = typeface;
289        recalculate();
290    }
291
292    Typeface getCollapsedTypeface() {
293        return mCollapsedTypeface != null ? mCollapsedTypeface : Typeface.DEFAULT;
294    }
295
296    Typeface getExpandedTypeface() {
297        return mExpandedTypeface != null ? mExpandedTypeface : Typeface.DEFAULT;
298    }
299
300    /**
301     * Set the value indicating the current scroll value. This decides how much of the
302     * background will be displayed, as well as the title metrics/positioning.
303     *
304     * A value of {@code 0.0} indicates that the layout is fully expanded.
305     * A value of {@code 1.0} indicates that the layout is fully collapsed.
306     */
307    void setExpansionFraction(float fraction) {
308        fraction = MathUtils.clamp(fraction, 0f, 1f);
309
310        if (fraction != mExpandedFraction) {
311            mExpandedFraction = fraction;
312            calculateCurrentOffsets();
313        }
314    }
315
316    final boolean setState(final int[] state) {
317        mState = state;
318
319        if (isStateful()) {
320            recalculate();
321            return true;
322        }
323
324        return false;
325    }
326
327    final boolean isStateful() {
328        return (mCollapsedTextColor != null && mCollapsedTextColor.isStateful())
329                || (mExpandedTextColor != null && mExpandedTextColor.isStateful());
330    }
331
332    float getExpansionFraction() {
333        return mExpandedFraction;
334    }
335
336    float getCollapsedTextSize() {
337        return mCollapsedTextSize;
338    }
339
340    float getExpandedTextSize() {
341        return mExpandedTextSize;
342    }
343
344    private void calculateCurrentOffsets() {
345        calculateOffsets(mExpandedFraction);
346    }
347
348    private void calculateOffsets(final float fraction) {
349        interpolateBounds(fraction);
350        mCurrentDrawX = lerp(mExpandedDrawX, mCollapsedDrawX, fraction,
351                mPositionInterpolator);
352        mCurrentDrawY = lerp(mExpandedDrawY, mCollapsedDrawY, fraction,
353                mPositionInterpolator);
354
355        setInterpolatedTextSize(lerp(mExpandedTextSize, mCollapsedTextSize,
356                fraction, mTextSizeInterpolator));
357
358        if (mCollapsedTextColor != mExpandedTextColor) {
359            // If the collapsed and expanded text colors are different, blend them based on the
360            // fraction
361            mTextPaint.setColor(blendColors(
362                    getCurrentExpandedTextColor(), getCurrentCollapsedTextColor(), fraction));
363        } else {
364            mTextPaint.setColor(getCurrentCollapsedTextColor());
365        }
366
367        mTextPaint.setShadowLayer(
368                lerp(mExpandedShadowRadius, mCollapsedShadowRadius, fraction, null),
369                lerp(mExpandedShadowDx, mCollapsedShadowDx, fraction, null),
370                lerp(mExpandedShadowDy, mCollapsedShadowDy, fraction, null),
371                blendColors(mExpandedShadowColor, mCollapsedShadowColor, fraction));
372
373        ViewCompat.postInvalidateOnAnimation(mView);
374    }
375
376    @ColorInt
377    private int getCurrentExpandedTextColor() {
378        if (mState != null) {
379            return mExpandedTextColor.getColorForState(mState, 0);
380        } else {
381            return mExpandedTextColor.getDefaultColor();
382        }
383    }
384
385    @ColorInt
386    private int getCurrentCollapsedTextColor() {
387        if (mState != null) {
388            return mCollapsedTextColor.getColorForState(mState, 0);
389        } else {
390            return mCollapsedTextColor.getDefaultColor();
391        }
392    }
393
394    private void calculateBaseOffsets() {
395        final float currentTextSize = mCurrentTextSize;
396
397        // We then calculate the collapsed text size, using the same logic
398        calculateUsingTextSize(mCollapsedTextSize);
399        float width = mTextToDraw != null ?
400                mTextPaint.measureText(mTextToDraw, 0, mTextToDraw.length()) : 0;
401        final int collapsedAbsGravity = GravityCompat.getAbsoluteGravity(mCollapsedTextGravity,
402                mIsRtl ? ViewCompat.LAYOUT_DIRECTION_RTL : ViewCompat.LAYOUT_DIRECTION_LTR);
403        switch (collapsedAbsGravity & Gravity.VERTICAL_GRAVITY_MASK) {
404            case Gravity.BOTTOM:
405                mCollapsedDrawY = mCollapsedBounds.bottom;
406                break;
407            case Gravity.TOP:
408                mCollapsedDrawY = mCollapsedBounds.top - mTextPaint.ascent();
409                break;
410            case Gravity.CENTER_VERTICAL:
411            default:
412                float textHeight = mTextPaint.descent() - mTextPaint.ascent();
413                float textOffset = (textHeight / 2) - mTextPaint.descent();
414                mCollapsedDrawY = mCollapsedBounds.centerY() + textOffset;
415                break;
416        }
417        switch (collapsedAbsGravity & GravityCompat.RELATIVE_HORIZONTAL_GRAVITY_MASK) {
418            case Gravity.CENTER_HORIZONTAL:
419                mCollapsedDrawX = mCollapsedBounds.centerX() - (width / 2);
420                break;
421            case Gravity.RIGHT:
422                mCollapsedDrawX = mCollapsedBounds.right - width;
423                break;
424            case Gravity.LEFT:
425            default:
426                mCollapsedDrawX = mCollapsedBounds.left;
427                break;
428        }
429
430        calculateUsingTextSize(mExpandedTextSize);
431        width = mTextToDraw != null
432                ? mTextPaint.measureText(mTextToDraw, 0, mTextToDraw.length()) : 0;
433        final int expandedAbsGravity = GravityCompat.getAbsoluteGravity(mExpandedTextGravity,
434                mIsRtl ? ViewCompat.LAYOUT_DIRECTION_RTL : ViewCompat.LAYOUT_DIRECTION_LTR);
435        switch (expandedAbsGravity & Gravity.VERTICAL_GRAVITY_MASK) {
436            case Gravity.BOTTOM:
437                mExpandedDrawY = mExpandedBounds.bottom;
438                break;
439            case Gravity.TOP:
440                mExpandedDrawY = mExpandedBounds.top - mTextPaint.ascent();
441                break;
442            case Gravity.CENTER_VERTICAL:
443            default:
444                float textHeight = mTextPaint.descent() - mTextPaint.ascent();
445                float textOffset = (textHeight / 2) - mTextPaint.descent();
446                mExpandedDrawY = mExpandedBounds.centerY() + textOffset;
447                break;
448        }
449        switch (expandedAbsGravity & GravityCompat.RELATIVE_HORIZONTAL_GRAVITY_MASK) {
450            case Gravity.CENTER_HORIZONTAL:
451                mExpandedDrawX = mExpandedBounds.centerX() - (width / 2);
452                break;
453            case Gravity.RIGHT:
454                mExpandedDrawX = mExpandedBounds.right - width;
455                break;
456            case Gravity.LEFT:
457            default:
458                mExpandedDrawX = mExpandedBounds.left;
459                break;
460        }
461
462        // The bounds have changed so we need to clear the texture
463        clearTexture();
464        // Now reset the text size back to the original
465        setInterpolatedTextSize(currentTextSize);
466    }
467
468    private void interpolateBounds(float fraction) {
469        mCurrentBounds.left = lerp(mExpandedBounds.left, mCollapsedBounds.left,
470                fraction, mPositionInterpolator);
471        mCurrentBounds.top = lerp(mExpandedDrawY, mCollapsedDrawY,
472                fraction, mPositionInterpolator);
473        mCurrentBounds.right = lerp(mExpandedBounds.right, mCollapsedBounds.right,
474                fraction, mPositionInterpolator);
475        mCurrentBounds.bottom = lerp(mExpandedBounds.bottom, mCollapsedBounds.bottom,
476                fraction, mPositionInterpolator);
477    }
478
479    public void draw(Canvas canvas) {
480        final int saveCount = canvas.save();
481
482        if (mTextToDraw != null && mDrawTitle) {
483            float x = mCurrentDrawX;
484            float y = mCurrentDrawY;
485
486            final boolean drawTexture = mUseTexture && mExpandedTitleTexture != null;
487
488            final float ascent;
489            final float descent;
490            if (drawTexture) {
491                ascent = mTextureAscent * mScale;
492                descent = mTextureDescent * mScale;
493            } else {
494                ascent = mTextPaint.ascent() * mScale;
495                descent = mTextPaint.descent() * mScale;
496            }
497
498            if (DEBUG_DRAW) {
499                // Just a debug tool, which drawn a magenta rect in the text bounds
500                canvas.drawRect(mCurrentBounds.left, y + ascent, mCurrentBounds.right, y + descent,
501                        DEBUG_DRAW_PAINT);
502            }
503
504            if (drawTexture) {
505                y += ascent;
506            }
507
508            if (mScale != 1f) {
509                canvas.scale(mScale, mScale, x, y);
510            }
511
512            if (drawTexture) {
513                // If we should use a texture, draw it instead of text
514                canvas.drawBitmap(mExpandedTitleTexture, x, y, mTexturePaint);
515            } else {
516                canvas.drawText(mTextToDraw, 0, mTextToDraw.length(), x, y, mTextPaint);
517            }
518        }
519
520        canvas.restoreToCount(saveCount);
521    }
522
523    private boolean calculateIsRtl(CharSequence text) {
524        final boolean defaultIsRtl = ViewCompat.getLayoutDirection(mView)
525                == ViewCompat.LAYOUT_DIRECTION_RTL;
526        return (defaultIsRtl
527                ? TextDirectionHeuristicsCompat.FIRSTSTRONG_RTL
528                : TextDirectionHeuristicsCompat.FIRSTSTRONG_LTR).isRtl(text, 0, text.length());
529    }
530
531    private void setInterpolatedTextSize(float textSize) {
532        calculateUsingTextSize(textSize);
533
534        // Use our texture if the scale isn't 1.0
535        mUseTexture = USE_SCALING_TEXTURE && mScale != 1f;
536
537        if (mUseTexture) {
538            // Make sure we have an expanded texture if needed
539            ensureExpandedTexture();
540        }
541
542        ViewCompat.postInvalidateOnAnimation(mView);
543    }
544
545    private boolean areTypefacesDifferent(Typeface first, Typeface second) {
546        return (first != null && !first.equals(second)) || (first == null && second != null);
547    }
548
549    private void calculateUsingTextSize(final float textSize) {
550        if (mText == null) return;
551
552        final float collapsedWidth = mCollapsedBounds.width();
553        final float expandedWidth = mExpandedBounds.width();
554
555        final float availableWidth;
556        final float newTextSize;
557        boolean updateDrawText = false;
558
559        if (isClose(textSize, mCollapsedTextSize)) {
560            newTextSize = mCollapsedTextSize;
561            mScale = 1f;
562            if (areTypefacesDifferent(mCurrentTypeface, mCollapsedTypeface)) {
563                mCurrentTypeface = mCollapsedTypeface;
564                updateDrawText = true;
565            }
566            availableWidth = collapsedWidth;
567        } else {
568            newTextSize = mExpandedTextSize;
569            if (areTypefacesDifferent(mCurrentTypeface, mExpandedTypeface)) {
570                mCurrentTypeface = mExpandedTypeface;
571                updateDrawText = true;
572            }
573            if (isClose(textSize, mExpandedTextSize)) {
574                // If we're close to the expanded text size, snap to it and use a scale of 1
575                mScale = 1f;
576            } else {
577                // Else, we'll scale down from the expanded text size
578                mScale = textSize / mExpandedTextSize;
579            }
580
581            final float textSizeRatio = mCollapsedTextSize / mExpandedTextSize;
582            // This is the size of the expanded bounds when it is scaled to match the
583            // collapsed text size
584            final float scaledDownWidth = expandedWidth * textSizeRatio;
585
586            if (scaledDownWidth > collapsedWidth) {
587                // If the scaled down size is larger than the actual collapsed width, we need to
588                // cap the available width so that when the expanded text scales down, it matches
589                // the collapsed width
590                availableWidth = Math.min(collapsedWidth / textSizeRatio, expandedWidth);
591            } else {
592                // Otherwise we'll just use the expanded width
593                availableWidth = expandedWidth;
594            }
595        }
596
597        if (availableWidth > 0) {
598            updateDrawText = (mCurrentTextSize != newTextSize) || mBoundsChanged || updateDrawText;
599            mCurrentTextSize = newTextSize;
600            mBoundsChanged = false;
601        }
602
603        if (mTextToDraw == null || updateDrawText) {
604            mTextPaint.setTextSize(mCurrentTextSize);
605            mTextPaint.setTypeface(mCurrentTypeface);
606            // Use linear text scaling if we're scaling the canvas
607            mTextPaint.setLinearText(mScale != 1f);
608
609            // If we don't currently have text to draw, or the text size has changed, ellipsize...
610            final CharSequence title = TextUtils.ellipsize(mText, mTextPaint,
611                    availableWidth, TextUtils.TruncateAt.END);
612            if (!TextUtils.equals(title, mTextToDraw)) {
613                mTextToDraw = title;
614                mIsRtl = calculateIsRtl(mTextToDraw);
615            }
616        }
617    }
618
619    private void ensureExpandedTexture() {
620        if (mExpandedTitleTexture != null || mExpandedBounds.isEmpty()
621                || TextUtils.isEmpty(mTextToDraw)) {
622            return;
623        }
624
625        calculateOffsets(0f);
626        mTextureAscent = mTextPaint.ascent();
627        mTextureDescent = mTextPaint.descent();
628
629        final int w = Math.round(mTextPaint.measureText(mTextToDraw, 0, mTextToDraw.length()));
630        final int h = Math.round(mTextureDescent - mTextureAscent);
631
632        if (w <= 0 || h <= 0) {
633            return; // If the width or height are 0, return
634        }
635
636        mExpandedTitleTexture = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
637
638        Canvas c = new Canvas(mExpandedTitleTexture);
639        c.drawText(mTextToDraw, 0, mTextToDraw.length(), 0, h - mTextPaint.descent(), mTextPaint);
640
641        if (mTexturePaint == null) {
642            // Make sure we have a paint
643            mTexturePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
644        }
645    }
646
647    public void recalculate() {
648        if (mView.getHeight() > 0 && mView.getWidth() > 0) {
649            // If we've already been laid out, calculate everything now otherwise we'll wait
650            // until a layout
651            calculateBaseOffsets();
652            calculateCurrentOffsets();
653        }
654    }
655
656    /**
657     * Set the title to display
658     *
659     * @param text
660     */
661    void setText(CharSequence text) {
662        if (text == null || !text.equals(mText)) {
663            mText = text;
664            mTextToDraw = null;
665            clearTexture();
666            recalculate();
667        }
668    }
669
670    CharSequence getText() {
671        return mText;
672    }
673
674    private void clearTexture() {
675        if (mExpandedTitleTexture != null) {
676            mExpandedTitleTexture.recycle();
677            mExpandedTitleTexture = null;
678        }
679    }
680
681    /**
682     * Returns true if {@code value} is 'close' to it's closest decimal value. Close is currently
683     * defined as it's difference being < 0.001.
684     */
685    private static boolean isClose(float value, float targetValue) {
686        return Math.abs(value - targetValue) < 0.001f;
687    }
688
689    ColorStateList getExpandedTextColor() {
690        return mExpandedTextColor;
691    }
692
693    ColorStateList getCollapsedTextColor() {
694        return mCollapsedTextColor;
695    }
696
697    /**
698     * Blend {@code color1} and {@code color2} using the given ratio.
699     *
700     * @param ratio of which to blend. 0.0 will return {@code color1}, 0.5 will give an even blend,
701     *              1.0 will return {@code color2}.
702     */
703    private static int blendColors(int color1, int color2, float ratio) {
704        final float inverseRatio = 1f - ratio;
705        float a = (Color.alpha(color1) * inverseRatio) + (Color.alpha(color2) * ratio);
706        float r = (Color.red(color1) * inverseRatio) + (Color.red(color2) * ratio);
707        float g = (Color.green(color1) * inverseRatio) + (Color.green(color2) * ratio);
708        float b = (Color.blue(color1) * inverseRatio) + (Color.blue(color2) * ratio);
709        return Color.argb((int) a, (int) r, (int) g, (int) b);
710    }
711
712    private static float lerp(float startValue, float endValue, float fraction,
713            Interpolator interpolator) {
714        if (interpolator != null) {
715            fraction = interpolator.getInterpolation(fraction);
716        }
717        return AnimationUtils.lerp(startValue, endValue, fraction);
718    }
719
720    private static boolean rectEquals(Rect r, int left, int top, int right, int bottom) {
721        return !(r.left != left || r.top != top || r.right != right || r.bottom != bottom);
722    }
723}
724