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