1 /* 2 * Copyright (C) 2017 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 17 package androidx.wear.widget; 18 19 import android.animation.ArgbEvaluator; 20 import android.animation.ValueAnimator; 21 import android.animation.ValueAnimator.AnimatorUpdateListener; 22 import android.content.Context; 23 import android.content.res.ColorStateList; 24 import android.content.res.TypedArray; 25 import android.graphics.Canvas; 26 import android.graphics.Color; 27 import android.graphics.Paint; 28 import android.graphics.Paint.Style; 29 import android.graphics.RadialGradient; 30 import android.graphics.Rect; 31 import android.graphics.RectF; 32 import android.graphics.Shader; 33 import android.graphics.drawable.Drawable; 34 import android.util.AttributeSet; 35 import android.view.View; 36 37 import androidx.annotation.Px; 38 import androidx.annotation.RestrictTo; 39 import androidx.annotation.RestrictTo.Scope; 40 import androidx.wear.R; 41 42 import java.util.Objects; 43 44 /** 45 * An image view surrounded by a circle. 46 * 47 * @hide 48 */ 49 @RestrictTo(Scope.LIBRARY) 50 public class CircledImageView extends View { 51 52 private static final ArgbEvaluator ARGB_EVALUATOR = new ArgbEvaluator(); 53 54 private static final int SQUARE_DIMEN_NONE = 0; 55 private static final int SQUARE_DIMEN_HEIGHT = 1; 56 private static final int SQUARE_DIMEN_WIDTH = 2; 57 58 private final RectF mOval; 59 private final Paint mPaint; 60 private final OvalShadowPainter mShadowPainter; 61 private final float mInitialCircleRadius; 62 private final ProgressDrawable mIndeterminateDrawable; 63 private final Rect mIndeterminateBounds = new Rect(); 64 private final Drawable.Callback mDrawableCallback = 65 new Drawable.Callback() { 66 @Override 67 public void invalidateDrawable(Drawable drawable) { 68 invalidate(); 69 } 70 71 @Override 72 public void scheduleDrawable(Drawable drawable, Runnable runnable, long l) { 73 // Not needed. 74 } 75 76 @Override 77 public void unscheduleDrawable(Drawable drawable, Runnable runnable) { 78 // Not needed. 79 } 80 }; 81 private ColorStateList mCircleColor; 82 private Drawable mDrawable; 83 private float mCircleRadius; 84 private float mCircleRadiusPercent; 85 private float mCircleRadiusPressed; 86 private float mCircleRadiusPressedPercent; 87 private float mRadiusInset; 88 private int mCircleBorderColor; 89 private Paint.Cap mCircleBorderCap; 90 private float mCircleBorderWidth; 91 private boolean mCircleHidden = false; 92 private float mProgress = 1f; 93 private boolean mPressed = false; 94 private boolean mProgressIndeterminate; 95 private boolean mVisible; 96 private boolean mWindowVisible; 97 private long mColorChangeAnimationDurationMs = 0; 98 private float mImageCirclePercentage = 1f; 99 private float mImageHorizontalOffcenterPercentage = 0f; 100 private Integer mImageTint; 101 private Integer mSquareDimen; 102 private int mCurrentColor; 103 104 private final AnimatorUpdateListener mAnimationListener = 105 new AnimatorUpdateListener() { 106 @Override 107 public void onAnimationUpdate(ValueAnimator animation) { 108 int color = (int) animation.getAnimatedValue(); 109 if (color != CircledImageView.this.mCurrentColor) { 110 CircledImageView.this.mCurrentColor = color; 111 CircledImageView.this.invalidate(); 112 } 113 } 114 }; 115 116 private ValueAnimator mColorAnimator; 117 118 public CircledImageView(Context context) { 119 this(context, null); 120 } 121 122 public CircledImageView(Context context, AttributeSet attrs) { 123 this(context, attrs, 0); 124 } 125 126 public CircledImageView(Context context, AttributeSet attrs, int defStyle) { 127 super(context, attrs, defStyle); 128 129 TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.CircledImageView); 130 mDrawable = a.getDrawable(R.styleable.CircledImageView_android_src); 131 if (mDrawable != null && mDrawable.getConstantState() != null) { 132 // The provided Drawable may be used elsewhere, so make a mutable clone before setTint() 133 // or setAlpha() is called on it. 134 mDrawable = 135 mDrawable.getConstantState() 136 .newDrawable(context.getResources(), context.getTheme()); 137 mDrawable = mDrawable.mutate(); 138 } 139 140 mCircleColor = a.getColorStateList(R.styleable.CircledImageView_background_color); 141 if (mCircleColor == null) { 142 mCircleColor = ColorStateList.valueOf(context.getColor(android.R.color.darker_gray)); 143 } 144 145 mCircleRadius = a.getDimension(R.styleable.CircledImageView_background_radius, 0); 146 mInitialCircleRadius = mCircleRadius; 147 mCircleRadiusPressed = a.getDimension( 148 R.styleable.CircledImageView_background_radius_pressed, mCircleRadius); 149 mCircleBorderColor = a 150 .getColor(R.styleable.CircledImageView_background_border_color, Color.BLACK); 151 mCircleBorderCap = 152 Paint.Cap.values()[a.getInt(R.styleable.CircledImageView_background_border_cap, 0)]; 153 mCircleBorderWidth = a.getDimension( 154 R.styleable.CircledImageView_background_border_width, 0); 155 156 if (mCircleBorderWidth > 0) { 157 // The border arc is drawn from the middle of the arc - take that into account. 158 mRadiusInset += mCircleBorderWidth / 2; 159 } 160 161 float circlePadding = a.getDimension(R.styleable.CircledImageView_img_padding, 0); 162 if (circlePadding > 0) { 163 mRadiusInset += circlePadding; 164 } 165 166 mImageCirclePercentage = a 167 .getFloat(R.styleable.CircledImageView_img_circle_percentage, 0f); 168 169 mImageHorizontalOffcenterPercentage = 170 a.getFloat(R.styleable.CircledImageView_img_horizontal_offset_percentage, 0f); 171 172 if (a.hasValue(R.styleable.CircledImageView_img_tint)) { 173 mImageTint = a.getColor(R.styleable.CircledImageView_img_tint, 0); 174 } 175 176 if (a.hasValue(R.styleable.CircledImageView_clip_dimen)) { 177 mSquareDimen = a.getInt(R.styleable.CircledImageView_clip_dimen, SQUARE_DIMEN_NONE); 178 } 179 180 mCircleRadiusPercent = 181 a.getFraction(R.styleable.CircledImageView_background_radius_percent, 1, 1, 0f); 182 183 mCircleRadiusPressedPercent = 184 a.getFraction( 185 R.styleable.CircledImageView_background_radius_pressed_percent, 1, 1, 186 mCircleRadiusPercent); 187 188 float shadowWidth = a.getDimension(R.styleable.CircledImageView_background_shadow_width, 0); 189 190 a.recycle(); 191 192 mOval = new RectF(); 193 mPaint = new Paint(); 194 mPaint.setAntiAlias(true); 195 mShadowPainter = new OvalShadowPainter(shadowWidth, 0, getCircleRadius(), 196 mCircleBorderWidth); 197 198 mIndeterminateDrawable = new ProgressDrawable(); 199 // {@link #mDrawableCallback} must be retained as a member, as Drawable callback 200 // is held by weak reference, we must retain it for it to continue to be called. 201 mIndeterminateDrawable.setCallback(mDrawableCallback); 202 203 setWillNotDraw(false); 204 205 setColorForCurrentState(); 206 } 207 208 /** Sets the circle to be hidden. */ 209 public void setCircleHidden(boolean circleHidden) { 210 if (circleHidden != mCircleHidden) { 211 mCircleHidden = circleHidden; 212 invalidate(); 213 } 214 } 215 216 @Override 217 protected boolean onSetAlpha(int alpha) { 218 return true; 219 } 220 221 @Override 222 protected void onDraw(Canvas canvas) { 223 int paddingLeft = getPaddingLeft(); 224 int paddingTop = getPaddingTop(); 225 226 float circleRadius = mPressed ? getCircleRadiusPressed() : getCircleRadius(); 227 228 // Maybe draw the shadow 229 mShadowPainter.draw(canvas, getAlpha()); 230 if (mCircleBorderWidth > 0) { 231 // First let's find the center of the view. 232 mOval.set( 233 paddingLeft, 234 paddingTop, 235 getWidth() - getPaddingRight(), 236 getHeight() - getPaddingBottom()); 237 // Having the center, lets make the border meet the circle. 238 mOval.set( 239 mOval.centerX() - circleRadius, 240 mOval.centerY() - circleRadius, 241 mOval.centerX() + circleRadius, 242 mOval.centerY() + circleRadius); 243 mPaint.setColor(mCircleBorderColor); 244 // {@link #Paint.setAlpha} is a helper method that just sets the alpha portion of the 245 // color. {@link #Paint.setPaint} will clear any previously set alpha value. 246 mPaint.setAlpha(Math.round(mPaint.getAlpha() * getAlpha())); 247 mPaint.setStyle(Style.STROKE); 248 mPaint.setStrokeWidth(mCircleBorderWidth); 249 mPaint.setStrokeCap(mCircleBorderCap); 250 251 if (mProgressIndeterminate) { 252 mOval.roundOut(mIndeterminateBounds); 253 mIndeterminateDrawable.setBounds(mIndeterminateBounds); 254 mIndeterminateDrawable.setRingColor(mCircleBorderColor); 255 mIndeterminateDrawable.setRingWidth(mCircleBorderWidth); 256 mIndeterminateDrawable.draw(canvas); 257 } else { 258 canvas.drawArc(mOval, -90, 360 * mProgress, false, mPaint); 259 } 260 } 261 if (!mCircleHidden) { 262 mOval.set( 263 paddingLeft, 264 paddingTop, 265 getWidth() - getPaddingRight(), 266 getHeight() - getPaddingBottom()); 267 // {@link #Paint.setAlpha} is a helper method that just sets the alpha portion of the 268 // color. {@link #Paint.setPaint} will clear any previously set alpha value. 269 mPaint.setColor(mCurrentColor); 270 mPaint.setAlpha(Math.round(mPaint.getAlpha() * getAlpha())); 271 272 mPaint.setStyle(Style.FILL); 273 float centerX = mOval.centerX(); 274 float centerY = mOval.centerY(); 275 276 canvas.drawCircle(centerX, centerY, circleRadius, mPaint); 277 } 278 279 if (mDrawable != null) { 280 mDrawable.setAlpha(Math.round(getAlpha() * 255)); 281 282 if (mImageTint != null) { 283 mDrawable.setTint(mImageTint); 284 } 285 mDrawable.draw(canvas); 286 } 287 288 super.onDraw(canvas); 289 } 290 291 private void setColorForCurrentState() { 292 int newColor = 293 mCircleColor.getColorForState(getDrawableState(), mCircleColor.getDefaultColor()); 294 if (mColorChangeAnimationDurationMs > 0) { 295 if (mColorAnimator != null) { 296 mColorAnimator.cancel(); 297 } else { 298 mColorAnimator = new ValueAnimator(); 299 } 300 mColorAnimator.setIntValues(new int[]{mCurrentColor, newColor}); 301 mColorAnimator.setEvaluator(ARGB_EVALUATOR); 302 mColorAnimator.setDuration(mColorChangeAnimationDurationMs); 303 mColorAnimator.addUpdateListener(this.mAnimationListener); 304 mColorAnimator.start(); 305 } else { 306 if (newColor != mCurrentColor) { 307 mCurrentColor = newColor; 308 invalidate(); 309 } 310 } 311 } 312 313 @Override 314 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 315 316 final float radius = 317 getCircleRadius() 318 + mCircleBorderWidth 319 + mShadowPainter.mShadowWidth * mShadowPainter.mShadowVisibility; 320 float desiredWidth = radius * 2; 321 float desiredHeight = radius * 2; 322 323 int widthMode = MeasureSpec.getMode(widthMeasureSpec); 324 int widthSize = MeasureSpec.getSize(widthMeasureSpec); 325 int heightMode = MeasureSpec.getMode(heightMeasureSpec); 326 int heightSize = MeasureSpec.getSize(heightMeasureSpec); 327 328 int width; 329 int height; 330 331 if (widthMode == MeasureSpec.EXACTLY) { 332 width = widthSize; 333 } else if (widthMode == MeasureSpec.AT_MOST) { 334 width = (int) Math.min(desiredWidth, widthSize); 335 } else { 336 width = (int) desiredWidth; 337 } 338 339 if (heightMode == MeasureSpec.EXACTLY) { 340 height = heightSize; 341 } else if (heightMode == MeasureSpec.AT_MOST) { 342 height = (int) Math.min(desiredHeight, heightSize); 343 } else { 344 height = (int) desiredHeight; 345 } 346 347 if (mSquareDimen != null) { 348 switch (mSquareDimen) { 349 case SQUARE_DIMEN_HEIGHT: 350 width = height; 351 break; 352 case SQUARE_DIMEN_WIDTH: 353 height = width; 354 break; 355 } 356 } 357 358 super.onMeasure( 359 MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), 360 MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); 361 } 362 363 @Override 364 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 365 if (mDrawable != null) { 366 // Retrieve the sizes of the drawable and the view. 367 final int nativeDrawableWidth = mDrawable.getIntrinsicWidth(); 368 final int nativeDrawableHeight = mDrawable.getIntrinsicHeight(); 369 final int viewWidth = getMeasuredWidth(); 370 final int viewHeight = getMeasuredHeight(); 371 final float imageCirclePercentage = 372 mImageCirclePercentage > 0 ? mImageCirclePercentage : 1; 373 374 final float scaleFactor = 375 Math.min( 376 1f, 377 Math.min( 378 (float) nativeDrawableWidth != 0 379 ? imageCirclePercentage * viewWidth 380 / nativeDrawableWidth 381 : 1, 382 (float) nativeDrawableHeight != 0 383 ? imageCirclePercentage * viewHeight 384 / nativeDrawableHeight 385 : 1)); 386 387 // Scale the drawable down to fit the view, if needed. 388 final int drawableWidth = Math.round(scaleFactor * nativeDrawableWidth); 389 final int drawableHeight = Math.round(scaleFactor * nativeDrawableHeight); 390 391 // Center the drawable within the view. 392 final int drawableLeft = 393 (viewWidth - drawableWidth) / 2 394 + Math.round(mImageHorizontalOffcenterPercentage * drawableWidth); 395 final int drawableTop = (viewHeight - drawableHeight) / 2; 396 397 mDrawable.setBounds( 398 drawableLeft, drawableTop, drawableLeft + drawableWidth, 399 drawableTop + drawableHeight); 400 } 401 402 super.onLayout(changed, left, top, right, bottom); 403 } 404 405 /** Sets the image given a resource. */ 406 public void setImageResource(int resId) { 407 setImageDrawable(resId == 0 ? null : getContext().getDrawable(resId)); 408 } 409 410 /** Sets the size of the image based on a percentage in [0, 1]. */ 411 public void setImageCirclePercentage(float percentage) { 412 float clamped = Math.max(0, Math.min(1, percentage)); 413 if (clamped != mImageCirclePercentage) { 414 mImageCirclePercentage = clamped; 415 invalidate(); 416 } 417 } 418 419 /** Sets the horizontal offset given a percentage in [0, 1]. */ 420 public void setImageHorizontalOffcenterPercentage(float percentage) { 421 if (percentage != mImageHorizontalOffcenterPercentage) { 422 mImageHorizontalOffcenterPercentage = percentage; 423 invalidate(); 424 } 425 } 426 427 /** Sets the tint. */ 428 public void setImageTint(int tint) { 429 if (mImageTint == null || tint != mImageTint) { 430 mImageTint = tint; 431 invalidate(); 432 } 433 } 434 435 /** Returns the circle radius. */ 436 public float getCircleRadius() { 437 float radius = mCircleRadius; 438 if (mCircleRadius <= 0 && mCircleRadiusPercent > 0) { 439 radius = Math.max(getMeasuredHeight(), getMeasuredWidth()) * mCircleRadiusPercent; 440 } 441 442 return radius - mRadiusInset; 443 } 444 445 /** Sets the circle radius. */ 446 public void setCircleRadius(float circleRadius) { 447 if (circleRadius != mCircleRadius) { 448 mCircleRadius = circleRadius; 449 mShadowPainter 450 .setInnerCircleRadius(mPressed ? getCircleRadiusPressed() : getCircleRadius()); 451 invalidate(); 452 } 453 } 454 455 /** Gets the circle radius percent. */ 456 public float getCircleRadiusPercent() { 457 return mCircleRadiusPercent; 458 } 459 460 /** 461 * Sets the radius of the circle to be a percentage of the largest dimension of the view. 462 * 463 * @param circleRadiusPercent A {@code float} from 0 to 1 representing the radius percentage. 464 */ 465 public void setCircleRadiusPercent(float circleRadiusPercent) { 466 if (circleRadiusPercent != mCircleRadiusPercent) { 467 mCircleRadiusPercent = circleRadiusPercent; 468 mShadowPainter 469 .setInnerCircleRadius(mPressed ? getCircleRadiusPressed() : getCircleRadius()); 470 invalidate(); 471 } 472 } 473 474 /** Gets the circle radius when pressed. */ 475 public float getCircleRadiusPressed() { 476 float radius = mCircleRadiusPressed; 477 478 if (mCircleRadiusPressed <= 0 && mCircleRadiusPressedPercent > 0) { 479 radius = 480 Math.max(getMeasuredHeight(), getMeasuredWidth()) * mCircleRadiusPressedPercent; 481 } 482 483 return radius - mRadiusInset; 484 } 485 486 /** Sets the circle radius when pressed. */ 487 public void setCircleRadiusPressed(float circleRadiusPressed) { 488 if (circleRadiusPressed != mCircleRadiusPressed) { 489 mCircleRadiusPressed = circleRadiusPressed; 490 invalidate(); 491 } 492 } 493 494 /** Gets the circle radius when pressed as a percent. */ 495 public float getCircleRadiusPressedPercent() { 496 return mCircleRadiusPressedPercent; 497 } 498 499 /** 500 * Sets the radius of the circle to be a percentage of the largest dimension of the view when 501 * pressed. 502 * 503 * @param circleRadiusPressedPercent A {@code float} from 0 to 1 representing the radius 504 * percentage. 505 */ 506 public void setCircleRadiusPressedPercent(float circleRadiusPressedPercent) { 507 if (circleRadiusPressedPercent != mCircleRadiusPressedPercent) { 508 mCircleRadiusPressedPercent = circleRadiusPressedPercent; 509 mShadowPainter 510 .setInnerCircleRadius(mPressed ? getCircleRadiusPressed() : getCircleRadius()); 511 invalidate(); 512 } 513 } 514 515 @Override 516 protected void drawableStateChanged() { 517 super.drawableStateChanged(); 518 setColorForCurrentState(); 519 } 520 521 /** Sets the circle color. */ 522 public void setCircleColor(int circleColor) { 523 setCircleColorStateList(ColorStateList.valueOf(circleColor)); 524 } 525 526 /** Gets the circle color. */ 527 public ColorStateList getCircleColorStateList() { 528 return mCircleColor; 529 } 530 531 /** Sets the circle color. */ 532 public void setCircleColorStateList(ColorStateList circleColor) { 533 if (!Objects.equals(circleColor, mCircleColor)) { 534 mCircleColor = circleColor; 535 setColorForCurrentState(); 536 invalidate(); 537 } 538 } 539 540 /** Gets the default circle color. */ 541 public int getDefaultCircleColor() { 542 return mCircleColor.getDefaultColor(); 543 } 544 545 /** 546 * Show the circle border as an indeterminate progress spinner. The views circle border width 547 * and color must be set for this to have an effect. 548 * 549 * @param show true if the progress spinner is shown, false to hide it. 550 */ 551 public void showIndeterminateProgress(boolean show) { 552 mProgressIndeterminate = show; 553 if (mIndeterminateDrawable != null) { 554 if (show && mVisible && mWindowVisible) { 555 mIndeterminateDrawable.startAnimation(); 556 } else { 557 mIndeterminateDrawable.stopAnimation(); 558 } 559 } 560 } 561 562 @Override 563 protected void onVisibilityChanged(View changedView, int visibility) { 564 super.onVisibilityChanged(changedView, visibility); 565 mVisible = (visibility == View.VISIBLE); 566 showIndeterminateProgress(mProgressIndeterminate); 567 } 568 569 @Override 570 protected void onWindowVisibilityChanged(int visibility) { 571 super.onWindowVisibilityChanged(visibility); 572 mWindowVisible = (visibility == View.VISIBLE); 573 showIndeterminateProgress(mProgressIndeterminate); 574 } 575 576 /** Sets the progress. */ 577 public void setProgress(float progress) { 578 if (progress != mProgress) { 579 mProgress = progress; 580 invalidate(); 581 } 582 } 583 584 /** 585 * Set how much of the shadow should be shown. 586 * 587 * @param shadowVisibility Value between 0 and 1. 588 */ 589 public void setShadowVisibility(float shadowVisibility) { 590 if (shadowVisibility != mShadowPainter.mShadowVisibility) { 591 mShadowPainter.setShadowVisibility(shadowVisibility); 592 invalidate(); 593 } 594 } 595 596 public float getInitialCircleRadius() { 597 return mInitialCircleRadius; 598 } 599 600 public void setCircleBorderColor(int circleBorderColor) { 601 mCircleBorderColor = circleBorderColor; 602 } 603 604 /** 605 * Set the border around the circle. 606 * 607 * @param circleBorderWidth Width of the border around the circle. 608 */ 609 public void setCircleBorderWidth(float circleBorderWidth) { 610 if (circleBorderWidth != mCircleBorderWidth) { 611 mCircleBorderWidth = circleBorderWidth; 612 mShadowPainter.setInnerCircleBorderWidth(circleBorderWidth); 613 invalidate(); 614 } 615 } 616 617 /** 618 * Set the stroke cap for the border around the circle. 619 * 620 * @param circleBorderCap Stroke cap for the border around the circle. 621 */ 622 public void setCircleBorderCap(Paint.Cap circleBorderCap) { 623 if (circleBorderCap != mCircleBorderCap) { 624 mCircleBorderCap = circleBorderCap; 625 invalidate(); 626 } 627 } 628 629 @Override 630 public void setPressed(boolean pressed) { 631 super.setPressed(pressed); 632 if (pressed != mPressed) { 633 mPressed = pressed; 634 mShadowPainter 635 .setInnerCircleRadius(mPressed ? getCircleRadiusPressed() : getCircleRadius()); 636 invalidate(); 637 } 638 } 639 640 @Override 641 public void setPadding(@Px int left, @Px int top, @Px int right, @Px int bottom) { 642 if (left != getPaddingLeft() 643 || top != getPaddingTop() 644 || right != getPaddingRight() 645 || bottom != getPaddingBottom()) { 646 mShadowPainter.setBounds(left, top, getWidth() - right, getHeight() - bottom); 647 } 648 super.setPadding(left, top, right, bottom); 649 } 650 651 @Override 652 public void onSizeChanged(int newWidth, int newHeight, int oldWidth, int oldHeight) { 653 if (newWidth != oldWidth || newHeight != oldHeight) { 654 mShadowPainter.setBounds( 655 getPaddingLeft(), 656 getPaddingTop(), 657 newWidth - getPaddingRight(), 658 newHeight - getPaddingBottom()); 659 } 660 } 661 662 public Drawable getImageDrawable() { 663 return mDrawable; 664 } 665 666 /** Sets the image drawable. */ 667 public void setImageDrawable(Drawable drawable) { 668 if (drawable != mDrawable) { 669 final Drawable existingDrawable = mDrawable; 670 mDrawable = drawable; 671 if (mDrawable != null && mDrawable.getConstantState() != null) { 672 // The provided Drawable may be used elsewhere, so make a mutable clone before 673 // setTint() or setAlpha() is called on it. 674 mDrawable = 675 mDrawable 676 .getConstantState() 677 .newDrawable(getResources(), getContext().getTheme()) 678 .mutate(); 679 } 680 681 final boolean skipLayout = 682 drawable != null 683 && existingDrawable != null 684 && existingDrawable.getIntrinsicHeight() == drawable 685 .getIntrinsicHeight() 686 && existingDrawable.getIntrinsicWidth() == drawable.getIntrinsicWidth(); 687 688 if (skipLayout) { 689 mDrawable.setBounds(existingDrawable.getBounds()); 690 } else { 691 requestLayout(); 692 } 693 694 invalidate(); 695 } 696 } 697 698 /** 699 * @return the milliseconds duration of the transition animation when the color changes. 700 */ 701 public long getColorChangeAnimationDuration() { 702 return mColorChangeAnimationDurationMs; 703 } 704 705 /** 706 * @param mColorChangeAnimationDurationMs the milliseconds duration of the color change 707 * animation. The color change animation will run if the color changes with {@link 708 * #setCircleColor} or as a result of the active state changing. 709 */ 710 public void setColorChangeAnimationDuration(long mColorChangeAnimationDurationMs) { 711 this.mColorChangeAnimationDurationMs = mColorChangeAnimationDurationMs; 712 } 713 714 /** 715 * Helper class taking care of painting a shadow behind the displayed image. TODO(amad): Replace 716 * this with elevation, when moving to support/wearable? 717 */ 718 private static class OvalShadowPainter { 719 720 private final int[] mShaderColors = new int[]{Color.BLACK, Color.TRANSPARENT}; 721 private final float[] mShaderStops = new float[]{0.6f, 1f}; 722 private final RectF mBounds = new RectF(); 723 private final float mShadowWidth; 724 private final Paint mShadowPaint = new Paint(); 725 726 private float mShadowRadius; 727 private float mShadowVisibility; 728 private float mInnerCircleRadius; 729 private float mInnerCircleBorderWidth; 730 731 OvalShadowPainter( 732 float shadowWidth, 733 float shadowVisibility, 734 float innerCircleRadius, 735 float innerCircleBorderWidth) { 736 mShadowWidth = shadowWidth; 737 mShadowVisibility = shadowVisibility; 738 mInnerCircleRadius = innerCircleRadius; 739 mInnerCircleBorderWidth = innerCircleBorderWidth; 740 mShadowRadius = 741 mInnerCircleRadius + mInnerCircleBorderWidth + mShadowWidth * mShadowVisibility; 742 mShadowPaint.setColor(Color.BLACK); 743 mShadowPaint.setStyle(Style.FILL); 744 mShadowPaint.setAntiAlias(true); 745 updateRadialGradient(); 746 } 747 748 void draw(Canvas canvas, float alpha) { 749 if (mShadowWidth > 0 && mShadowVisibility > 0) { 750 mShadowPaint.setAlpha(Math.round(mShadowPaint.getAlpha() * alpha)); 751 canvas.drawCircle(mBounds.centerX(), mBounds.centerY(), mShadowRadius, 752 mShadowPaint); 753 } 754 } 755 756 void setBounds(@Px int left, @Px int top, @Px int right, @Px int bottom) { 757 mBounds.set(left, top, right, bottom); 758 updateRadialGradient(); 759 } 760 761 void setInnerCircleRadius(float newInnerCircleRadius) { 762 mInnerCircleRadius = newInnerCircleRadius; 763 updateRadialGradient(); 764 } 765 766 void setInnerCircleBorderWidth(float newInnerCircleBorderWidth) { 767 mInnerCircleBorderWidth = newInnerCircleBorderWidth; 768 updateRadialGradient(); 769 } 770 771 void setShadowVisibility(float newShadowVisibility) { 772 mShadowVisibility = newShadowVisibility; 773 updateRadialGradient(); 774 } 775 776 private void updateRadialGradient() { 777 // Make the shadow start beyond the circled and possibly the border. 778 mShadowRadius = 779 mInnerCircleRadius + mInnerCircleBorderWidth + mShadowWidth * mShadowVisibility; 780 // This may happen if the innerCircleRadius has not been correctly computed yet while 781 // the view has already been inflated, but not yet measured. In this case, if the view 782 // specifies the radius as a percentage of the screen width, then that evaluates to 0 783 // and will be corrected after measuring, through onSizeChanged(). 784 if (mShadowRadius > 0) { 785 mShadowPaint.setShader( 786 new RadialGradient( 787 mBounds.centerX(), 788 mBounds.centerY(), 789 mShadowRadius, 790 mShaderColors, 791 mShaderStops, 792 Shader.TileMode.MIRROR)); 793 } 794 } 795 } 796 } 797