Home | History | Annotate | Download | only in widget
      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