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