/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package androidx.wear.ble.view; import android.animation.ArgbEvaluator; import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; import android.annotation.TargetApi; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Paint.Style; import android.graphics.RadialGradient; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Shader; import android.graphics.drawable.Drawable; import android.os.Build; import android.util.AttributeSet; import android.view.View; import java.util.Objects; import com.android.permissioncontroller.R; /** * An image view surrounded by a circle. */ @TargetApi(Build.VERSION_CODES.LOLLIPOP) public class CircledImageView extends View { private static final ArgbEvaluator ARGB_EVALUATOR = new ArgbEvaluator(); private Drawable mDrawable; private final RectF mOval; private final Paint mPaint; private ColorStateList mCircleColor; private float mCircleRadius; private float mCircleRadiusPercent; private float mCircleRadiusPressed; private float mCircleRadiusPressedPercent; private float mRadiusInset; private int mCircleBorderColor; private float mCircleBorderWidth; private float mProgress = 1f; private final float mShadowWidth; private float mShadowVisibility; private boolean mCircleHidden = false; private float mInitialCircleRadius; private boolean mPressed = false; private boolean mProgressIndeterminate; private ProgressDrawable mIndeterminateDrawable; private Rect mIndeterminateBounds = new Rect(); private long mColorChangeAnimationDurationMs = 0; private float mImageCirclePercentage = 1f; private float mImageHorizontalOffcenterPercentage = 0f; private Integer mImageTint; private final Drawable.Callback mDrawableCallback = new Drawable.Callback() { @Override public void invalidateDrawable(Drawable drawable) { invalidate(); } @Override public void scheduleDrawable(Drawable drawable, Runnable runnable, long l) { // Not needed. } @Override public void unscheduleDrawable(Drawable drawable, Runnable runnable) { // Not needed. } }; private int mCurrentColor; private final AnimatorUpdateListener mAnimationListener = new AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { int color = (int) animation.getAnimatedValue(); if (color != CircledImageView.this.mCurrentColor) { CircledImageView.this.mCurrentColor = color; CircledImageView.this.invalidate(); } } }; private ValueAnimator mColorAnimator; public CircledImageView(Context context) { this(context, null); } public CircledImageView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public CircledImageView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.CircledImageView); mDrawable = a.getDrawable(R.styleable.CircledImageView_android_src); mCircleColor = a.getColorStateList(R.styleable.CircledImageView_circle_color); if (mCircleColor == null) { mCircleColor = ColorStateList.valueOf(android.R.color.darker_gray); } mCircleRadius = a.getDimension( R.styleable.CircledImageView_circle_radius, 0); mInitialCircleRadius = mCircleRadius; mCircleRadiusPressed = a.getDimension( R.styleable.CircledImageView_circle_radius_pressed, mCircleRadius); mCircleBorderColor = a.getColor( R.styleable.CircledImageView_circle_border_color, Color.BLACK); mCircleBorderWidth = a.getDimension(R.styleable.CircledImageView_circle_border_width, 0); if (mCircleBorderWidth > 0) { mRadiusInset += mCircleBorderWidth; } float circlePadding = a.getDimension(R.styleable.CircledImageView_circle_padding, 0); if (circlePadding > 0) { mRadiusInset += circlePadding; } mShadowWidth = a.getDimension(R.styleable.CircledImageView_shadow_width, 0); mImageCirclePercentage = a.getFloat( R.styleable.CircledImageView_image_circle_percentage, 0f); mImageHorizontalOffcenterPercentage = a.getFloat( R.styleable.CircledImageView_image_horizontal_offcenter_percentage, 0f); if (a.hasValue(R.styleable.CircledImageView_image_tint)) { mImageTint = a.getColor(R.styleable.CircledImageView_image_tint, 0); } mCircleRadiusPercent = a.getFraction(R.styleable.CircledImageView_circle_radius_percent, 1, 1, 0f); mCircleRadiusPressedPercent = a.getFraction( R.styleable.CircledImageView_circle_radius_pressed_percent, 1, 1, mCircleRadiusPercent); a.recycle(); mOval = new RectF(); mPaint = new Paint(); mPaint.setAntiAlias(true); mIndeterminateDrawable = new ProgressDrawable(); // {@link #mDrawableCallback} must be retained as a member, as Drawable callback // is held by weak reference, we must retain it for it to continue to be called. mIndeterminateDrawable.setCallback(mDrawableCallback); setWillNotDraw(false); setColorForCurrentState(); } public void setCircleHidden(boolean circleHidden) { if (circleHidden != mCircleHidden) { mCircleHidden = circleHidden; invalidate(); } } @Override protected boolean onSetAlpha(int alpha) { return true; } @Override protected void onDraw(Canvas canvas) { int paddingLeft = getPaddingLeft(); int paddingTop = getPaddingTop(); float circleRadius = mPressed ? getCircleRadiusPressed() : getCircleRadius(); if (mShadowWidth > 0 && mShadowVisibility > 0) { // First let's find the center of the view. mOval.set(paddingLeft, paddingTop, getWidth() - getPaddingRight(), getHeight() - getPaddingBottom()); // Having the center, lets make the shadow start beyond the circled and possibly the // border. final float radius = circleRadius + mCircleBorderWidth + mShadowWidth * mShadowVisibility; mPaint.setColor(Color.BLACK); mPaint.setAlpha(Math.round(mPaint.getAlpha() * getAlpha())); mPaint.setStyle(Style.FILL); // TODO: precalc and pre-allocate this mPaint.setShader(new RadialGradient(mOval.centerX(), mOval.centerY(), radius, new int[]{Color.BLACK, Color.TRANSPARENT}, new float[]{0.6f, 1f}, Shader.TileMode.MIRROR)); canvas.drawCircle(mOval.centerX(), mOval.centerY(), radius, mPaint); mPaint.setShader(null); } if (mCircleBorderWidth > 0) { // First let's find the center of the view. mOval.set(paddingLeft, paddingTop, getWidth() - getPaddingRight(), getHeight() - getPaddingBottom()); // Having the center, lets make the border meet the circle. mOval.set(mOval.centerX() - circleRadius, mOval.centerY() - circleRadius, mOval.centerX() + circleRadius, mOval.centerY() + circleRadius); mPaint.setColor(mCircleBorderColor); // {@link #Paint.setAlpha} is a helper method that just sets the alpha portion of the // color. {@link #Paint.setPaint} will clear any previously set alpha value. mPaint.setAlpha(Math.round(mPaint.getAlpha() * getAlpha())); mPaint.setStyle(Style.STROKE); mPaint.setStrokeWidth(mCircleBorderWidth); if (mProgressIndeterminate) { mOval.roundOut(mIndeterminateBounds); mIndeterminateDrawable.setBounds(mIndeterminateBounds); mIndeterminateDrawable.setRingColor(mCircleBorderColor); mIndeterminateDrawable.setRingWidth(mCircleBorderWidth); mIndeterminateDrawable.draw(canvas); } else { canvas.drawArc(mOval, -90, 360 * mProgress, false, mPaint); } } if (!mCircleHidden) { mOval.set(paddingLeft, paddingTop, getWidth() - getPaddingRight(), getHeight() - getPaddingBottom()); // {@link #Paint.setAlpha} is a helper method that just sets the alpha portion of the // color. {@link #Paint.setPaint} will clear any previously set alpha value. mPaint.setColor(mCurrentColor); mPaint.setAlpha(Math.round(mPaint.getAlpha() * getAlpha())); mPaint.setStyle(Style.FILL); float centerX = mOval.centerX(); float centerY = mOval.centerY(); canvas.drawCircle(centerX, centerY, circleRadius, mPaint); } if (mDrawable != null) { mDrawable.setAlpha(Math.round(getAlpha() * 255)); if (mImageTint != null) { mDrawable.setTint(mImageTint); } mDrawable.draw(canvas); } super.onDraw(canvas); } private void setColorForCurrentState() { int newColor = mCircleColor.getColorForState(getDrawableState(), mCircleColor.getDefaultColor()); if (mColorChangeAnimationDurationMs > 0) { if (mColorAnimator != null) { mColorAnimator.cancel(); } else { mColorAnimator = new ValueAnimator(); } mColorAnimator.setIntValues(new int[] { mCurrentColor, newColor }); mColorAnimator.setEvaluator(ARGB_EVALUATOR); mColorAnimator.setDuration(mColorChangeAnimationDurationMs); mColorAnimator.addUpdateListener(this.mAnimationListener); mColorAnimator.start(); } else { if (newColor != mCurrentColor) { mCurrentColor = newColor; invalidate(); } } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final float radius = getCircleRadius() + mCircleBorderWidth + mShadowWidth * mShadowVisibility; float desiredWidth = radius * 2; float desiredHeight = radius * 2; int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int width; int height; if (widthMode == MeasureSpec.EXACTLY) { width = widthSize; } else if (widthMode == MeasureSpec.AT_MOST) { width = (int) Math.min(desiredWidth, widthSize); } else { width = (int) desiredWidth; } if (heightMode == MeasureSpec.EXACTLY) { height = heightSize; } else if (heightMode == MeasureSpec.AT_MOST) { height = (int) Math.min(desiredHeight, heightSize); } else { height = (int) desiredHeight; } super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { if (mDrawable != null) { // Retrieve the sizes of the drawable and the view. final int nativeDrawableWidth = mDrawable.getIntrinsicWidth(); final int nativeDrawableHeight = mDrawable.getIntrinsicHeight(); final int viewWidth = getMeasuredWidth(); final int viewHeight = getMeasuredHeight(); final float imageCirclePercentage = mImageCirclePercentage > 0 ? mImageCirclePercentage : 1; final float scaleFactor = Math.min(1f, Math.min( (float) nativeDrawableWidth != 0 ? imageCirclePercentage * viewWidth / nativeDrawableWidth : 1, (float) nativeDrawableHeight != 0 ? imageCirclePercentage * viewHeight / nativeDrawableHeight : 1)); // Scale the drawable down to fit the view, if needed. final int drawableWidth = Math.round(scaleFactor * nativeDrawableWidth); final int drawableHeight = Math.round(scaleFactor * nativeDrawableHeight); // Center the drawable within the view. final int drawableLeft = (viewWidth - drawableWidth) / 2 + Math.round(mImageHorizontalOffcenterPercentage * drawableWidth); final int drawableTop = (viewHeight - drawableHeight) / 2; mDrawable.setBounds(drawableLeft, drawableTop, drawableLeft + drawableWidth, drawableTop + drawableHeight); } super.onLayout(changed, left, top, right, bottom); } public void setImageDrawable(Drawable drawable) { if (drawable != mDrawable) { final Drawable existingDrawable = mDrawable; mDrawable = drawable; final boolean skipLayout = drawable != null && existingDrawable != null && existingDrawable.getIntrinsicHeight() == drawable.getIntrinsicHeight() && existingDrawable.getIntrinsicWidth() == drawable.getIntrinsicWidth(); if (skipLayout) { mDrawable.setBounds(existingDrawable.getBounds()); } else { requestLayout(); } invalidate(); } } public void setImageResource(int resId) { setImageDrawable(resId == 0 ? null : getContext().getDrawable(resId)); } public void setImageCirclePercentage(float percentage) { float clamped = Math.max(0, Math.min(1, percentage)); if (clamped != mImageCirclePercentage) { mImageCirclePercentage = clamped; invalidate(); } } public void setImageHorizontalOffcenterPercentage(float percentage) { if (percentage != mImageHorizontalOffcenterPercentage) { mImageHorizontalOffcenterPercentage = percentage; invalidate(); } } public void setImageTint(int tint) { if (tint != mImageTint) { mImageTint = tint; invalidate(); } } public float getCircleRadius() { float radius = mCircleRadius; if (mCircleRadius <= 0 && mCircleRadiusPercent > 0) { radius = Math.max(getMeasuredHeight(), getMeasuredWidth()) * mCircleRadiusPercent; } return radius - mRadiusInset; } public float getCircleRadiusPercent() { return mCircleRadiusPercent; } public float getCircleRadiusPressed() { float radius = mCircleRadiusPressed; if (mCircleRadiusPressed <= 0 && mCircleRadiusPressedPercent > 0) { radius = Math.max(getMeasuredHeight(), getMeasuredWidth()) * mCircleRadiusPressedPercent; } return radius - mRadiusInset; } public float getCircleRadiusPressedPercent() { return mCircleRadiusPressedPercent; } public void setCircleRadius(float circleRadius) { if (circleRadius != mCircleRadius) { mCircleRadius = circleRadius; invalidate(); } } /** * Sets the radius of the circle to be a percentage of the largest dimension of the view. * @param circleRadiusPercent A {@code float} from 0 to 1 representing the radius percentage. */ public void setCircleRadiusPercent(float circleRadiusPercent) { if (circleRadiusPercent != mCircleRadiusPercent) { mCircleRadiusPercent = circleRadiusPercent; invalidate(); } } public void setCircleRadiusPressed(float circleRadiusPressed) { if (circleRadiusPressed != mCircleRadiusPressed) { mCircleRadiusPressed = circleRadiusPressed; invalidate(); } } /** * Sets the radius of the circle to be a percentage of the largest dimension of the view when * pressed. * @param circleRadiusPressedPercent A {@code float} from 0 to 1 representing the radius * percentage. */ public void setCircleRadiusPressedPercent(float circleRadiusPressedPercent) { if (circleRadiusPressedPercent != mCircleRadiusPressedPercent) { mCircleRadiusPressedPercent = circleRadiusPressedPercent; invalidate(); } } @Override protected void drawableStateChanged() { super.drawableStateChanged(); setColorForCurrentState(); } public void setCircleColor(int circleColor) { setCircleColorStateList(ColorStateList.valueOf(circleColor)); } public void setCircleColorStateList(ColorStateList circleColor) { if (!Objects.equals(circleColor, mCircleColor)) { mCircleColor = circleColor; setColorForCurrentState(); invalidate(); } } public ColorStateList getCircleColorStateList() { return mCircleColor; } public int getDefaultCircleColor() { return mCircleColor.getDefaultColor(); } /** * Show the circle border as an indeterminate progress spinner. * The views circle border width and color must be set for this to have an effect. * * @param show true if the progress spinner is shown, false to hide it. */ public void showIndeterminateProgress(boolean show) { mProgressIndeterminate = show; if (show) { mIndeterminateDrawable.startAnimation(); } else { mIndeterminateDrawable.stopAnimation(); } } @Override protected void onVisibilityChanged(View changedView, int visibility) { super.onVisibilityChanged(changedView, visibility); if (visibility != View.VISIBLE) { showIndeterminateProgress(false); } else if (mProgressIndeterminate) { showIndeterminateProgress(true); } } public void setProgress(float progress) { if (progress != mProgress) { mProgress = progress; invalidate(); } } /** * Set how much of the shadow should be shown. * @param shadowVisibility Value between 0 and 1. */ public void setShadowVisibility(float shadowVisibility) { if (shadowVisibility != mShadowVisibility) { mShadowVisibility = shadowVisibility; invalidate(); } } public float getInitialCircleRadius() { return mInitialCircleRadius; } public void setCircleBorderColor(int circleBorderColor) { mCircleBorderColor = circleBorderColor; } /** * Set the border around the circle. * @param circleBorderWidth Width of the border around the circle. */ public void setCircleBorderWidth(float circleBorderWidth) { if (circleBorderWidth != mCircleBorderWidth) { mCircleBorderWidth = circleBorderWidth; invalidate(); } } @Override public void setPressed(boolean pressed) { super.setPressed(pressed); if (pressed != mPressed) { mPressed = pressed; invalidate(); } } public Drawable getImageDrawable() { return mDrawable; } /** * @return the milliseconds duration of the transition animation when the color changes. */ public long getColorChangeAnimationDuration() { return mColorChangeAnimationDurationMs; } /** * @param mColorChangeAnimationDurationMs the milliseconds duration of the color change * animation. The color change animation will run if the color changes with {@link #setCircleColor} * or as a result of the active state changing. */ public void setColorChangeAnimationDuration(long mColorChangeAnimationDurationMs) { this.mColorChangeAnimationDurationMs = mColorChangeAnimationDurationMs; } }