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