Home | History | Annotate | Download | only in view
      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 
    121     public CircledImageView(Context context) {
    122         this(context, null);
    123     }
    124 
    125     public CircledImageView(Context context, AttributeSet attrs) {
    126         this(context, attrs, 0);
    127     }
    128 
    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 
    192     public void setCircleHidden(boolean circleHidden) {
    193         if (circleHidden != mCircleHidden) {
    194             mCircleHidden = circleHidden;
    195             invalidate();
    196         }
    197     }
    198 
    199 
    200     @Override
    201     protected boolean onSetAlpha(int alpha) {
    202         return true;
    203     }
    204 
    205     @Override
    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 
    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
    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
    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 
    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 
    395     public void setImageResource(int resId) {
    396         setImageDrawable(resId == 0 ? null : getContext().getDrawable(resId));
    397     }
    398 
    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 
    407     public void setImageHorizontalOffcenterPercentage(float percentage) {
    408         if (percentage != mImageHorizontalOffcenterPercentage) {
    409             mImageHorizontalOffcenterPercentage = percentage;
    410             invalidate();
    411         }
    412     }
    413 
    414     public void setImageTint(int tint) {
    415         if (tint != mImageTint) {
    416             mImageTint = tint;
    417             invalidate();
    418         }
    419     }
    420 
    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 
    430     public float getCircleRadiusPercent() {
    431         return mCircleRadiusPercent;
    432     }
    433 
    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 
    445     public float getCircleRadiusPressedPercent() {
    446         return mCircleRadiusPressedPercent;
    447     }
    448 
    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      */
    460     public void setCircleRadiusPercent(float circleRadiusPercent) {
    461         if (circleRadiusPercent != mCircleRadiusPercent) {
    462             mCircleRadiusPercent = circleRadiusPercent;
    463             invalidate();
    464         }
    465     }
    466 
    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      */
    480     public void setCircleRadiusPressedPercent(float circleRadiusPressedPercent) {
    481         if (circleRadiusPressedPercent  != mCircleRadiusPressedPercent) {
    482             mCircleRadiusPressedPercent = circleRadiusPressedPercent;
    483             invalidate();
    484         }
    485     }
    486 
    487     @Override
    488     protected void drawableStateChanged() {
    489         super.drawableStateChanged();
    490         setColorForCurrentState();
    491     }
    492 
    493     public void setCircleColor(int circleColor) {
    494         setCircleColorStateList(ColorStateList.valueOf(circleColor));
    495     }
    496 
    497     public void setCircleColorStateList(ColorStateList circleColor) {
    498         if (!Objects.equals(circleColor, mCircleColor)) {
    499             mCircleColor = circleColor;
    500             setColorForCurrentState();
    501             invalidate();
    502         }
    503     }
    504 
    505     public ColorStateList getCircleColorStateList() {
    506         return mCircleColor;
    507     }
    508 
    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      */
    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
    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 
    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      */
    549     public void setShadowVisibility(float shadowVisibility) {
    550         if (shadowVisibility != mShadowVisibility) {
    551             mShadowVisibility = shadowVisibility;
    552             invalidate();
    553         }
    554     }
    555 
    556     public float getInitialCircleRadius() {
    557         return mInitialCircleRadius;
    558     }
    559 
    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      */
    568     public void setCircleBorderWidth(float circleBorderWidth) {
    569         if (circleBorderWidth != mCircleBorderWidth) {
    570             mCircleBorderWidth = circleBorderWidth;
    571             invalidate();
    572         }
    573     }
    574 
    575     @Override
    576     public void setPressed(boolean pressed) {
    577         super.setPressed(pressed);
    578         if (pressed != mPressed) {
    579             mPressed = pressed;
    580             invalidate();
    581         }
    582     }
    583 
    584     public Drawable getImageDrawable() {
    585         return mDrawable;
    586     }
    587 
    588     /**
    589      * @return the milliseconds duration of the transition animation when the color changes.
    590      */
    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      */
    600     public void setColorChangeAnimationDuration(long mColorChangeAnimationDurationMs) {
    601         this.mColorChangeAnimationDurationMs = mColorChangeAnimationDurationMs;
    602     }
    603 }
    604