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