1 /*
2  * Copyright (C) 2015 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.wearable.view;
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.util.AttributeSet;
37 import android.view.View;
38 
39 import java.util.Objects;
40 import com.android.packageinstaller.R;
41 
42 import com.android.packageinstaller.R;
43 
44 /**
45  * An image view surrounded by a circle.
46  */
47 @TargetApi(Build.VERSION_CODES.LOLLIPOP)
48 public class CircledImageView extends View {
49 
50     private static final ArgbEvaluator ARGB_EVALUATOR = new ArgbEvaluator();
51 
52     private Drawable mDrawable;
53 
54     private final RectF mOval;
55     private final Paint mPaint;
56 
57     private ColorStateList mCircleColor;
58 
59     private float mCircleRadius;
60     private float mCircleRadiusPercent;
61 
62     private float mCircleRadiusPressed;
63     private float mCircleRadiusPressedPercent;
64 
65     private float mRadiusInset;
66 
67     private int mCircleBorderColor;
68 
69     private float mCircleBorderWidth;
70     private float mProgress = 1f;
71     private final float mShadowWidth;
72 
73     private float mShadowVisibility;
74     private boolean mCircleHidden = false;
75 
76     private float mInitialCircleRadius;
77 
78     private boolean mPressed = false;
79 
80     private boolean mProgressIndeterminate;
81     private ProgressDrawable mIndeterminateDrawable;
82     private Rect mIndeterminateBounds = new Rect();
83     private long mColorChangeAnimationDurationMs = 0;
84 
85     private float mImageCirclePercentage = 1f;
86     private float mImageHorizontalOffcenterPercentage = 0f;
87     private Integer mImageTint;
88 
89     private final Drawable.Callback mDrawableCallback = new Drawable.Callback() {
90         @Override
91         public void invalidateDrawable(Drawable drawable) {
92             invalidate();
93         }
94 
95         @Override
96         public void scheduleDrawable(Drawable drawable, Runnable runnable, long l) {
97             // Not needed.
98         }
99 
100         @Override
101         public void unscheduleDrawable(Drawable drawable, Runnable runnable) {
102             // Not needed.
103         }
104     };
105 
106     private int mCurrentColor;
107 
108     private final AnimatorUpdateListener mAnimationListener = new AnimatorUpdateListener() {
109         @Override
110         public void onAnimationUpdate(ValueAnimator animation) {
111             int color = (int) animation.getAnimatedValue();
112             if (color != CircledImageView.this.mCurrentColor) {
113                 CircledImageView.this.mCurrentColor = color;
114                 CircledImageView.this.invalidate();
115             }
116         }
117     };
118 
119     private ValueAnimator mColorAnimator;
120 
CircledImageView(Context context)121     public CircledImageView(Context context) {
122         this(context, null);
123     }
124 
CircledImageView(Context context, AttributeSet attrs)125     public CircledImageView(Context context, AttributeSet attrs) {
126         this(context, attrs, 0);
127     }
128 
CircledImageView(Context context, AttributeSet attrs, int defStyle)129     public CircledImageView(Context context, AttributeSet attrs, int defStyle) {
130         super(context, attrs, defStyle);
131 
132         TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.CircledImageView);
133         mDrawable = a.getDrawable(R.styleable.CircledImageView_android_src);
134 
135         mCircleColor = a.getColorStateList(R.styleable.CircledImageView_circle_color);
136         if (mCircleColor == null) {
137             mCircleColor = ColorStateList.valueOf(android.R.color.darker_gray);
138         }
139 
140         mCircleRadius = a.getDimension(
141                 R.styleable.CircledImageView_circle_radius, 0);
142         mInitialCircleRadius = mCircleRadius;
143         mCircleRadiusPressed = a.getDimension(
144                 R.styleable.CircledImageView_circle_radius_pressed, mCircleRadius);
145         mCircleBorderColor = a.getColor(
146                 R.styleable.CircledImageView_circle_border_color, Color.BLACK);
147         mCircleBorderWidth = a.getDimension(R.styleable.CircledImageView_circle_border_width, 0);
148 
149         if (mCircleBorderWidth > 0) {
150             mRadiusInset += mCircleBorderWidth;
151         }
152 
153         float circlePadding = a.getDimension(R.styleable.CircledImageView_circle_padding, 0);
154         if (circlePadding > 0) {
155             mRadiusInset += circlePadding;
156         }
157         mShadowWidth = a.getDimension(R.styleable.CircledImageView_shadow_width, 0);
158 
159         mImageCirclePercentage = a.getFloat(
160                 R.styleable.CircledImageView_image_circle_percentage, 0f);
161 
162         mImageHorizontalOffcenterPercentage = a.getFloat(
163                 R.styleable.CircledImageView_image_horizontal_offcenter_percentage, 0f);
164 
165         if (a.hasValue(R.styleable.CircledImageView_image_tint)) {
166             mImageTint = a.getColor(R.styleable.CircledImageView_image_tint, 0);
167         }
168 
169         mCircleRadiusPercent = a.getFraction(R.styleable.CircledImageView_circle_radius_percent,
170                 1, 1, 0f);
171 
172         mCircleRadiusPressedPercent = a.getFraction(
173                 R.styleable.CircledImageView_circle_radius_pressed_percent, 1, 1,
174                 mCircleRadiusPercent);
175 
176         a.recycle();
177 
178         mOval = new RectF();
179         mPaint = new Paint();
180         mPaint.setAntiAlias(true);
181 
182         mIndeterminateDrawable = new ProgressDrawable();
183         // {@link #mDrawableCallback} must be retained as a member, as Drawable callback
184         // is held by weak reference, we must retain it for it to continue to be called.
185         mIndeterminateDrawable.setCallback(mDrawableCallback);
186 
187         setWillNotDraw(false);
188 
189         setColorForCurrentState();
190     }
191 
setCircleHidden(boolean circleHidden)192     public void setCircleHidden(boolean circleHidden) {
193         if (circleHidden != mCircleHidden) {
194             mCircleHidden = circleHidden;
195             invalidate();
196         }
197     }
198 
199 
200     @Override
onSetAlpha(int alpha)201     protected boolean onSetAlpha(int alpha) {
202         return true;
203     }
204 
205     @Override
onDraw(Canvas canvas)206     protected void onDraw(Canvas canvas) {
207         int paddingLeft = getPaddingLeft();
208         int paddingTop = getPaddingTop();
209 
210 
211         float circleRadius = mPressed ? getCircleRadiusPressed() : getCircleRadius();
212         if (mShadowWidth > 0 && mShadowVisibility > 0) {
213             // First let's find the center of the view.
214             mOval.set(paddingLeft, paddingTop, getWidth() - getPaddingRight(),
215                     getHeight() - getPaddingBottom());
216             // Having the center, lets make the shadow start beyond the circled and possibly the
217             // border.
218             final float radius = circleRadius + mCircleBorderWidth +
219                     mShadowWidth * mShadowVisibility;
220             mPaint.setColor(Color.BLACK);
221             mPaint.setAlpha(Math.round(mPaint.getAlpha() * getAlpha()));
222             mPaint.setStyle(Style.FILL);
223             // TODO: precalc and pre-allocate this
224             mPaint.setShader(new RadialGradient(mOval.centerX(), mOval.centerY(), radius,
225                     new int[]{Color.BLACK, Color.TRANSPARENT}, new float[]{0.6f, 1f},
226                     Shader.TileMode.MIRROR));
227             canvas.drawCircle(mOval.centerX(), mOval.centerY(), radius, mPaint);
228             mPaint.setShader(null);
229         }
230         if (mCircleBorderWidth > 0) {
231             // First let's find the center of the view.
232             mOval.set(paddingLeft, paddingTop, getWidth() - getPaddingRight(),
233                     getHeight() - getPaddingBottom());
234             // Having the center, lets make the border meet the circle.
235             mOval.set(mOval.centerX() - circleRadius, mOval.centerY() - circleRadius,
236                     mOval.centerX() + circleRadius, mOval.centerY() + circleRadius);
237             mPaint.setColor(mCircleBorderColor);
238             // {@link #Paint.setAlpha} is a helper method that just sets the alpha portion of the
239             // color. {@link #Paint.setPaint} will clear any previously set alpha value.
240             mPaint.setAlpha(Math.round(mPaint.getAlpha() * getAlpha()));
241             mPaint.setStyle(Style.STROKE);
242             mPaint.setStrokeWidth(mCircleBorderWidth);
243 
244             if (mProgressIndeterminate) {
245                 mOval.roundOut(mIndeterminateBounds);
246                 mIndeterminateDrawable.setBounds(mIndeterminateBounds);
247                 mIndeterminateDrawable.setRingColor(mCircleBorderColor);
248                 mIndeterminateDrawable.setRingWidth(mCircleBorderWidth);
249                 mIndeterminateDrawable.draw(canvas);
250             } else {
251                 canvas.drawArc(mOval, -90, 360 * mProgress, false, mPaint);
252             }
253         }
254         if (!mCircleHidden) {
255             mOval.set(paddingLeft, paddingTop, getWidth() - getPaddingRight(),
256                     getHeight() - getPaddingBottom());
257             // {@link #Paint.setAlpha} is a helper method that just sets the alpha portion of the
258             // color. {@link #Paint.setPaint} will clear any previously set alpha value.
259             mPaint.setColor(mCurrentColor);
260             mPaint.setAlpha(Math.round(mPaint.getAlpha() * getAlpha()));
261 
262             mPaint.setStyle(Style.FILL);
263             float centerX = mOval.centerX();
264             float centerY = mOval.centerY();
265 
266             canvas.drawCircle(centerX, centerY, circleRadius, mPaint);
267         }
268 
269         if (mDrawable != null) {
270             mDrawable.setAlpha(Math.round(getAlpha() * 255));
271 
272             if (mImageTint != null) {
273                 mDrawable.setTint(mImageTint);
274             }
275             mDrawable.draw(canvas);
276         }
277 
278         super.onDraw(canvas);
279     }
280 
setColorForCurrentState()281     private void setColorForCurrentState() {
282         int newColor = mCircleColor.getColorForState(getDrawableState(),
283                 mCircleColor.getDefaultColor());
284         if (mColorChangeAnimationDurationMs > 0) {
285             if (mColorAnimator != null) {
286                 mColorAnimator.cancel();
287             } else {
288                 mColorAnimator = new ValueAnimator();
289             }
290             mColorAnimator.setIntValues(new int[] {
291                     mCurrentColor, newColor });
292             mColorAnimator.setEvaluator(ARGB_EVALUATOR);
293             mColorAnimator.setDuration(mColorChangeAnimationDurationMs);
294             mColorAnimator.addUpdateListener(this.mAnimationListener);
295             mColorAnimator.start();
296         } else {
297             if (newColor != mCurrentColor) {
298                 mCurrentColor = newColor;
299                 invalidate();
300             }
301         }
302     }
303 
304     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)305     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
306 
307         final float radius = getCircleRadius() + mCircleBorderWidth +
308                 mShadowWidth * mShadowVisibility;
309         float desiredWidth = radius * 2;
310         float desiredHeight = radius * 2;
311 
312         int widthMode = MeasureSpec.getMode(widthMeasureSpec);
313         int widthSize = MeasureSpec.getSize(widthMeasureSpec);
314         int heightMode = MeasureSpec.getMode(heightMeasureSpec);
315         int heightSize = MeasureSpec.getSize(heightMeasureSpec);
316 
317         int width;
318         int height;
319 
320         if (widthMode == MeasureSpec.EXACTLY) {
321             width = widthSize;
322         } else if (widthMode == MeasureSpec.AT_MOST) {
323             width = (int) Math.min(desiredWidth, widthSize);
324         } else {
325             width = (int) desiredWidth;
326         }
327 
328         if (heightMode == MeasureSpec.EXACTLY) {
329             height = heightSize;
330         } else if (heightMode == MeasureSpec.AT_MOST) {
331             height = (int) Math.min(desiredHeight, heightSize);
332         } else {
333             height = (int) desiredHeight;
334         }
335 
336         super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
337                 MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
338     }
339 
340     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)341     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
342         if (mDrawable != null) {
343             // Retrieve the sizes of the drawable and the view.
344             final int nativeDrawableWidth = mDrawable.getIntrinsicWidth();
345             final int nativeDrawableHeight = mDrawable.getIntrinsicHeight();
346             final int viewWidth = getMeasuredWidth();
347             final int viewHeight = getMeasuredHeight();
348             final float imageCirclePercentage = mImageCirclePercentage > 0
349                     ? mImageCirclePercentage : 1;
350 
351             final float scaleFactor = Math.min(1f,
352                     Math.min(
353                             (float) nativeDrawableWidth != 0
354                                     ? imageCirclePercentage * viewWidth / nativeDrawableWidth : 1,
355                             (float) nativeDrawableHeight != 0
356                                     ? imageCirclePercentage
357                                         * viewHeight / nativeDrawableHeight : 1));
358 
359             // Scale the drawable down to fit the view, if needed.
360             final int drawableWidth = Math.round(scaleFactor * nativeDrawableWidth);
361             final int drawableHeight = Math.round(scaleFactor * nativeDrawableHeight);
362 
363             // Center the drawable within the view.
364             final int drawableLeft = (viewWidth - drawableWidth) / 2
365                     + Math.round(mImageHorizontalOffcenterPercentage * drawableWidth);
366             final int drawableTop = (viewHeight - drawableHeight) / 2;
367 
368             mDrawable.setBounds(drawableLeft, drawableTop, drawableLeft + drawableWidth,
369                     drawableTop + drawableHeight);
370         }
371 
372         super.onLayout(changed, left, top, right, bottom);
373     }
374 
setImageDrawable(Drawable drawable)375     public void setImageDrawable(Drawable drawable) {
376         if (drawable != mDrawable) {
377             final Drawable existingDrawable = mDrawable;
378             mDrawable = drawable;
379 
380             final boolean skipLayout = drawable != null
381                     && existingDrawable != null
382                     && existingDrawable.getIntrinsicHeight() == drawable.getIntrinsicHeight()
383                     && existingDrawable.getIntrinsicWidth() == drawable.getIntrinsicWidth();
384 
385             if (skipLayout) {
386                 mDrawable.setBounds(existingDrawable.getBounds());
387             } else {
388                 requestLayout();
389             }
390 
391             invalidate();
392         }
393     }
394 
setImageResource(int resId)395     public void setImageResource(int resId) {
396         setImageDrawable(resId == 0 ? null : getContext().getDrawable(resId));
397     }
398 
setImageCirclePercentage(float percentage)399     public void setImageCirclePercentage(float percentage) {
400         float clamped = Math.max(0, Math.min(1, percentage));
401         if (clamped != mImageCirclePercentage) {
402             mImageCirclePercentage = clamped;
403             invalidate();
404         }
405     }
406 
setImageHorizontalOffcenterPercentage(float percentage)407     public void setImageHorizontalOffcenterPercentage(float percentage) {
408         if (percentage != mImageHorizontalOffcenterPercentage) {
409             mImageHorizontalOffcenterPercentage = percentage;
410             invalidate();
411         }
412     }
413 
setImageTint(int tint)414     public void setImageTint(int tint) {
415         if (tint != mImageTint) {
416             mImageTint = tint;
417             invalidate();
418         }
419     }
420 
getCircleRadius()421     public float getCircleRadius() {
422         float radius = mCircleRadius;
423         if (mCircleRadius <= 0 && mCircleRadiusPercent > 0) {
424             radius = Math.max(getMeasuredHeight(), getMeasuredWidth()) * mCircleRadiusPercent;
425         }
426 
427         return radius - mRadiusInset;
428     }
429 
getCircleRadiusPercent()430     public float getCircleRadiusPercent() {
431         return mCircleRadiusPercent;
432     }
433 
getCircleRadiusPressed()434     public float getCircleRadiusPressed() {
435         float radius = mCircleRadiusPressed;
436 
437         if (mCircleRadiusPressed <= 0 && mCircleRadiusPressedPercent > 0) {
438             radius = Math.max(getMeasuredHeight(), getMeasuredWidth())
439                     * mCircleRadiusPressedPercent;
440         }
441 
442         return radius - mRadiusInset;
443     }
444 
getCircleRadiusPressedPercent()445     public float getCircleRadiusPressedPercent() {
446         return mCircleRadiusPressedPercent;
447     }
448 
setCircleRadius(float circleRadius)449     public void setCircleRadius(float circleRadius) {
450         if (circleRadius != mCircleRadius) {
451             mCircleRadius = circleRadius;
452             invalidate();
453         }
454     }
455 
456     /**
457      * Sets the radius of the circle to be a percentage of the largest dimension of the view.
458      * @param circleRadiusPercent A {@code float} from 0 to 1 representing the radius percentage.
459      */
setCircleRadiusPercent(float circleRadiusPercent)460     public void setCircleRadiusPercent(float circleRadiusPercent) {
461         if (circleRadiusPercent != mCircleRadiusPercent) {
462             mCircleRadiusPercent = circleRadiusPercent;
463             invalidate();
464         }
465     }
466 
setCircleRadiusPressed(float circleRadiusPressed)467     public void setCircleRadiusPressed(float circleRadiusPressed) {
468         if (circleRadiusPressed != mCircleRadiusPressed) {
469             mCircleRadiusPressed = circleRadiusPressed;
470             invalidate();
471         }
472     }
473 
474     /**
475      * Sets the radius of the circle to be a percentage of the largest dimension of the view when
476      * pressed.
477      * @param circleRadiusPressedPercent A {@code float} from 0 to 1 representing the radius
478      *                                   percentage.
479      */
setCircleRadiusPressedPercent(float circleRadiusPressedPercent)480     public void setCircleRadiusPressedPercent(float circleRadiusPressedPercent) {
481         if (circleRadiusPressedPercent  != mCircleRadiusPressedPercent) {
482             mCircleRadiusPressedPercent = circleRadiusPressedPercent;
483             invalidate();
484         }
485     }
486 
487     @Override
drawableStateChanged()488     protected void drawableStateChanged() {
489         super.drawableStateChanged();
490         setColorForCurrentState();
491     }
492 
setCircleColor(int circleColor)493     public void setCircleColor(int circleColor) {
494         setCircleColorStateList(ColorStateList.valueOf(circleColor));
495     }
496 
setCircleColorStateList(ColorStateList circleColor)497     public void setCircleColorStateList(ColorStateList circleColor) {
498         if (!Objects.equals(circleColor, mCircleColor)) {
499             mCircleColor = circleColor;
500             setColorForCurrentState();
501             invalidate();
502         }
503     }
504 
getCircleColorStateList()505     public ColorStateList getCircleColorStateList() {
506         return mCircleColor;
507     }
508 
getDefaultCircleColor()509     public int getDefaultCircleColor() {
510         return mCircleColor.getDefaultColor();
511     }
512 
513     /**
514      * Show the circle border as an indeterminate progress spinner.
515      * The views circle border width and color must be set for this to have an effect.
516      *
517      * @param show true if the progress spinner is shown, false to hide it.
518      */
showIndeterminateProgress(boolean show)519     public void showIndeterminateProgress(boolean show) {
520         mProgressIndeterminate = show;
521         if (show) {
522             mIndeterminateDrawable.startAnimation();
523         } else {
524             mIndeterminateDrawable.stopAnimation();
525         }
526     }
527 
528     @Override
onVisibilityChanged(View changedView, int visibility)529     protected void onVisibilityChanged(View changedView, int visibility) {
530         super.onVisibilityChanged(changedView, visibility);
531         if (visibility != View.VISIBLE) {
532             showIndeterminateProgress(false);
533         } else if (mProgressIndeterminate) {
534             showIndeterminateProgress(true);
535         }
536     }
537 
setProgress(float progress)538     public void setProgress(float progress) {
539         if (progress != mProgress) {
540             mProgress = progress;
541             invalidate();
542         }
543     }
544 
545     /**
546      * Set how much of the shadow should be shown.
547      * @param shadowVisibility Value between 0 and 1.
548      */
setShadowVisibility(float shadowVisibility)549     public void setShadowVisibility(float shadowVisibility) {
550         if (shadowVisibility != mShadowVisibility) {
551             mShadowVisibility = shadowVisibility;
552             invalidate();
553         }
554     }
555 
getInitialCircleRadius()556     public float getInitialCircleRadius() {
557         return mInitialCircleRadius;
558     }
559 
setCircleBorderColor(int circleBorderColor)560     public void setCircleBorderColor(int circleBorderColor) {
561         mCircleBorderColor = circleBorderColor;
562     }
563 
564     /**
565      * Set the border around the circle.
566      * @param circleBorderWidth Width of the border around the circle.
567      */
setCircleBorderWidth(float circleBorderWidth)568     public void setCircleBorderWidth(float circleBorderWidth) {
569         if (circleBorderWidth != mCircleBorderWidth) {
570             mCircleBorderWidth = circleBorderWidth;
571             invalidate();
572         }
573     }
574 
575     @Override
setPressed(boolean pressed)576     public void setPressed(boolean pressed) {
577         super.setPressed(pressed);
578         if (pressed != mPressed) {
579             mPressed = pressed;
580             invalidate();
581         }
582     }
583 
getImageDrawable()584     public Drawable getImageDrawable() {
585         return mDrawable;
586     }
587 
588     /**
589      * @return the milliseconds duration of the transition animation when the color changes.
590      */
getColorChangeAnimationDuration()591     public long getColorChangeAnimationDuration() {
592         return mColorChangeAnimationDurationMs;
593     }
594 
595     /**
596      * @param mColorChangeAnimationDurationMs the milliseconds duration of the color change
597      *            animation. The color change animation will run if the color changes with {@link #setCircleColor}
598      *            or as a result of the active state changing.
599      */
setColorChangeAnimationDuration(long mColorChangeAnimationDurationMs)600     public void setColorChangeAnimationDuration(long mColorChangeAnimationDurationMs) {
601         this.mColorChangeAnimationDurationMs = mColorChangeAnimationDurationMs;
602     }
603 }
604