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