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