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