| /* |
| * Copyright (C) 2017 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.widget; |
| |
| import android.animation.ArgbEvaluator; |
| import android.animation.ValueAnimator; |
| import android.animation.ValueAnimator.AnimatorUpdateListener; |
| 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.util.AttributeSet; |
| import android.view.View; |
| |
| import androidx.annotation.Px; |
| import androidx.annotation.RestrictTo; |
| import androidx.annotation.RestrictTo.Scope; |
| import androidx.wear.R; |
| |
| import java.util.Objects; |
| |
| /** |
| * An image view surrounded by a circle. |
| * |
| * @hide |
| */ |
| @RestrictTo(Scope.LIBRARY) |
| public class CircledImageView extends View { |
| |
| private static final ArgbEvaluator ARGB_EVALUATOR = new ArgbEvaluator(); |
| |
| private static final int SQUARE_DIMEN_NONE = 0; |
| private static final int SQUARE_DIMEN_HEIGHT = 1; |
| private static final int SQUARE_DIMEN_WIDTH = 2; |
| |
| private final RectF mOval; |
| private final Paint mPaint; |
| private final OvalShadowPainter mShadowPainter; |
| private final float mInitialCircleRadius; |
| private final ProgressDrawable mIndeterminateDrawable; |
| private final Rect mIndeterminateBounds = new Rect(); |
| 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 ColorStateList mCircleColor; |
| private Drawable mDrawable; |
| private float mCircleRadius; |
| private float mCircleRadiusPercent; |
| private float mCircleRadiusPressed; |
| private float mCircleRadiusPressedPercent; |
| private float mRadiusInset; |
| private int mCircleBorderColor; |
| private Paint.Cap mCircleBorderCap; |
| private float mCircleBorderWidth; |
| private boolean mCircleHidden = false; |
| private float mProgress = 1f; |
| private boolean mPressed = false; |
| private boolean mProgressIndeterminate; |
| private boolean mVisible; |
| private boolean mWindowVisible; |
| private long mColorChangeAnimationDurationMs = 0; |
| private float mImageCirclePercentage = 1f; |
| private float mImageHorizontalOffcenterPercentage = 0f; |
| private Integer mImageTint; |
| private Integer mSquareDimen; |
| 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); |
| if (mDrawable != null && mDrawable.getConstantState() != null) { |
| // The provided Drawable may be used elsewhere, so make a mutable clone before setTint() |
| // or setAlpha() is called on it. |
| mDrawable = |
| mDrawable.getConstantState() |
| .newDrawable(context.getResources(), context.getTheme()); |
| mDrawable = mDrawable.mutate(); |
| } |
| |
| mCircleColor = a.getColorStateList(R.styleable.CircledImageView_background_color); |
| if (mCircleColor == null) { |
| mCircleColor = ColorStateList.valueOf(context.getColor(android.R.color.darker_gray)); |
| } |
| |
| mCircleRadius = a.getDimension(R.styleable.CircledImageView_background_radius, 0); |
| mInitialCircleRadius = mCircleRadius; |
| mCircleRadiusPressed = a.getDimension( |
| R.styleable.CircledImageView_background_radius_pressed, mCircleRadius); |
| mCircleBorderColor = a |
| .getColor(R.styleable.CircledImageView_background_border_color, Color.BLACK); |
| mCircleBorderCap = |
| Paint.Cap.values()[a.getInt(R.styleable.CircledImageView_background_border_cap, 0)]; |
| mCircleBorderWidth = a.getDimension( |
| R.styleable.CircledImageView_background_border_width, 0); |
| |
| if (mCircleBorderWidth > 0) { |
| // The border arc is drawn from the middle of the arc - take that into account. |
| mRadiusInset += mCircleBorderWidth / 2; |
| } |
| |
| float circlePadding = a.getDimension(R.styleable.CircledImageView_img_padding, 0); |
| if (circlePadding > 0) { |
| mRadiusInset += circlePadding; |
| } |
| |
| mImageCirclePercentage = a |
| .getFloat(R.styleable.CircledImageView_img_circle_percentage, 0f); |
| |
| mImageHorizontalOffcenterPercentage = |
| a.getFloat(R.styleable.CircledImageView_img_horizontal_offset_percentage, 0f); |
| |
| if (a.hasValue(R.styleable.CircledImageView_img_tint)) { |
| mImageTint = a.getColor(R.styleable.CircledImageView_img_tint, 0); |
| } |
| |
| if (a.hasValue(R.styleable.CircledImageView_clip_dimen)) { |
| mSquareDimen = a.getInt(R.styleable.CircledImageView_clip_dimen, SQUARE_DIMEN_NONE); |
| } |
| |
| mCircleRadiusPercent = |
| a.getFraction(R.styleable.CircledImageView_background_radius_percent, 1, 1, 0f); |
| |
| mCircleRadiusPressedPercent = |
| a.getFraction( |
| R.styleable.CircledImageView_background_radius_pressed_percent, 1, 1, |
| mCircleRadiusPercent); |
| |
| float shadowWidth = a.getDimension(R.styleable.CircledImageView_background_shadow_width, 0); |
| |
| a.recycle(); |
| |
| mOval = new RectF(); |
| mPaint = new Paint(); |
| mPaint.setAntiAlias(true); |
| mShadowPainter = new OvalShadowPainter(shadowWidth, 0, getCircleRadius(), |
| mCircleBorderWidth); |
| |
| 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(); |
| } |
| |
| /** Sets the circle to be hidden. */ |
| 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(); |
| |
| // Maybe draw the shadow |
| mShadowPainter.draw(canvas, getAlpha()); |
| 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); |
| mPaint.setStrokeCap(mCircleBorderCap); |
| |
| 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 |
| + mShadowPainter.mShadowWidth * mShadowPainter.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; |
| } |
| |
| if (mSquareDimen != null) { |
| switch (mSquareDimen) { |
| case SQUARE_DIMEN_HEIGHT: |
| width = height; |
| break; |
| case SQUARE_DIMEN_WIDTH: |
| height = width; |
| break; |
| } |
| } |
| |
| 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); |
| } |
| |
| /** Sets the image given a resource. */ |
| public void setImageResource(int resId) { |
| setImageDrawable(resId == 0 ? null : getContext().getDrawable(resId)); |
| } |
| |
| /** Sets the size of the image based on a percentage in [0, 1]. */ |
| public void setImageCirclePercentage(float percentage) { |
| float clamped = Math.max(0, Math.min(1, percentage)); |
| if (clamped != mImageCirclePercentage) { |
| mImageCirclePercentage = clamped; |
| invalidate(); |
| } |
| } |
| |
| /** Sets the horizontal offset given a percentage in [0, 1]. */ |
| public void setImageHorizontalOffcenterPercentage(float percentage) { |
| if (percentage != mImageHorizontalOffcenterPercentage) { |
| mImageHorizontalOffcenterPercentage = percentage; |
| invalidate(); |
| } |
| } |
| |
| /** Sets the tint. */ |
| public void setImageTint(int tint) { |
| if (mImageTint == null || tint != mImageTint) { |
| mImageTint = tint; |
| invalidate(); |
| } |
| } |
| |
| /** Returns the circle radius. */ |
| public float getCircleRadius() { |
| float radius = mCircleRadius; |
| if (mCircleRadius <= 0 && mCircleRadiusPercent > 0) { |
| radius = Math.max(getMeasuredHeight(), getMeasuredWidth()) * mCircleRadiusPercent; |
| } |
| |
| return radius - mRadiusInset; |
| } |
| |
| /** Sets the circle radius. */ |
| public void setCircleRadius(float circleRadius) { |
| if (circleRadius != mCircleRadius) { |
| mCircleRadius = circleRadius; |
| mShadowPainter |
| .setInnerCircleRadius(mPressed ? getCircleRadiusPressed() : getCircleRadius()); |
| invalidate(); |
| } |
| } |
| |
| /** Gets the circle radius percent. */ |
| public float getCircleRadiusPercent() { |
| return mCircleRadiusPercent; |
| } |
| |
| /** |
| * 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; |
| mShadowPainter |
| .setInnerCircleRadius(mPressed ? getCircleRadiusPressed() : getCircleRadius()); |
| invalidate(); |
| } |
| } |
| |
| /** Gets the circle radius when pressed. */ |
| public float getCircleRadiusPressed() { |
| float radius = mCircleRadiusPressed; |
| |
| if (mCircleRadiusPressed <= 0 && mCircleRadiusPressedPercent > 0) { |
| radius = |
| Math.max(getMeasuredHeight(), getMeasuredWidth()) * mCircleRadiusPressedPercent; |
| } |
| |
| return radius - mRadiusInset; |
| } |
| |
| /** Sets the circle radius when pressed. */ |
| public void setCircleRadiusPressed(float circleRadiusPressed) { |
| if (circleRadiusPressed != mCircleRadiusPressed) { |
| mCircleRadiusPressed = circleRadiusPressed; |
| invalidate(); |
| } |
| } |
| |
| /** Gets the circle radius when pressed as a percent. */ |
| public float getCircleRadiusPressedPercent() { |
| return mCircleRadiusPressedPercent; |
| } |
| |
| /** |
| * 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; |
| mShadowPainter |
| .setInnerCircleRadius(mPressed ? getCircleRadiusPressed() : getCircleRadius()); |
| invalidate(); |
| } |
| } |
| |
| @Override |
| protected void drawableStateChanged() { |
| super.drawableStateChanged(); |
| setColorForCurrentState(); |
| } |
| |
| /** Sets the circle color. */ |
| public void setCircleColor(int circleColor) { |
| setCircleColorStateList(ColorStateList.valueOf(circleColor)); |
| } |
| |
| /** Gets the circle color. */ |
| public ColorStateList getCircleColorStateList() { |
| return mCircleColor; |
| } |
| |
| /** Sets the circle color. */ |
| public void setCircleColorStateList(ColorStateList circleColor) { |
| if (!Objects.equals(circleColor, mCircleColor)) { |
| mCircleColor = circleColor; |
| setColorForCurrentState(); |
| invalidate(); |
| } |
| } |
| |
| /** Gets the default circle color. */ |
| 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 (mIndeterminateDrawable != null) { |
| if (show && mVisible && mWindowVisible) { |
| mIndeterminateDrawable.startAnimation(); |
| } else { |
| mIndeterminateDrawable.stopAnimation(); |
| } |
| } |
| } |
| |
| @Override |
| protected void onVisibilityChanged(View changedView, int visibility) { |
| super.onVisibilityChanged(changedView, visibility); |
| mVisible = (visibility == View.VISIBLE); |
| showIndeterminateProgress(mProgressIndeterminate); |
| } |
| |
| @Override |
| protected void onWindowVisibilityChanged(int visibility) { |
| super.onWindowVisibilityChanged(visibility); |
| mWindowVisible = (visibility == View.VISIBLE); |
| showIndeterminateProgress(mProgressIndeterminate); |
| } |
| |
| /** Sets the progress. */ |
| 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 != mShadowPainter.mShadowVisibility) { |
| mShadowPainter.setShadowVisibility(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; |
| mShadowPainter.setInnerCircleBorderWidth(circleBorderWidth); |
| invalidate(); |
| } |
| } |
| |
| /** |
| * Set the stroke cap for the border around the circle. |
| * |
| * @param circleBorderCap Stroke cap for the border around the circle. |
| */ |
| public void setCircleBorderCap(Paint.Cap circleBorderCap) { |
| if (circleBorderCap != mCircleBorderCap) { |
| mCircleBorderCap = circleBorderCap; |
| invalidate(); |
| } |
| } |
| |
| @Override |
| public void setPressed(boolean pressed) { |
| super.setPressed(pressed); |
| if (pressed != mPressed) { |
| mPressed = pressed; |
| mShadowPainter |
| .setInnerCircleRadius(mPressed ? getCircleRadiusPressed() : getCircleRadius()); |
| invalidate(); |
| } |
| } |
| |
| @Override |
| public void setPadding(@Px int left, @Px int top, @Px int right, @Px int bottom) { |
| if (left != getPaddingLeft() |
| || top != getPaddingTop() |
| || right != getPaddingRight() |
| || bottom != getPaddingBottom()) { |
| mShadowPainter.setBounds(left, top, getWidth() - right, getHeight() - bottom); |
| } |
| super.setPadding(left, top, right, bottom); |
| } |
| |
| @Override |
| public void onSizeChanged(int newWidth, int newHeight, int oldWidth, int oldHeight) { |
| if (newWidth != oldWidth || newHeight != oldHeight) { |
| mShadowPainter.setBounds( |
| getPaddingLeft(), |
| getPaddingTop(), |
| newWidth - getPaddingRight(), |
| newHeight - getPaddingBottom()); |
| } |
| } |
| |
| public Drawable getImageDrawable() { |
| return mDrawable; |
| } |
| |
| /** Sets the image drawable. */ |
| public void setImageDrawable(Drawable drawable) { |
| if (drawable != mDrawable) { |
| final Drawable existingDrawable = mDrawable; |
| mDrawable = drawable; |
| if (mDrawable != null && mDrawable.getConstantState() != null) { |
| // The provided Drawable may be used elsewhere, so make a mutable clone before |
| // setTint() or setAlpha() is called on it. |
| mDrawable = |
| mDrawable |
| .getConstantState() |
| .newDrawable(getResources(), getContext().getTheme()) |
| .mutate(); |
| } |
| |
| final boolean skipLayout = |
| drawable != null |
| && existingDrawable != null |
| && existingDrawable.getIntrinsicHeight() == drawable |
| .getIntrinsicHeight() |
| && existingDrawable.getIntrinsicWidth() == drawable.getIntrinsicWidth(); |
| |
| if (skipLayout) { |
| mDrawable.setBounds(existingDrawable.getBounds()); |
| } else { |
| requestLayout(); |
| } |
| |
| invalidate(); |
| } |
| } |
| |
| /** |
| * @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; |
| } |
| |
| /** |
| * Helper class taking care of painting a shadow behind the displayed image. TODO(amad): Replace |
| * this with elevation, when moving to support/wearable? |
| */ |
| private static class OvalShadowPainter { |
| |
| private final int[] mShaderColors = new int[]{Color.BLACK, Color.TRANSPARENT}; |
| private final float[] mShaderStops = new float[]{0.6f, 1f}; |
| private final RectF mBounds = new RectF(); |
| private final float mShadowWidth; |
| private final Paint mShadowPaint = new Paint(); |
| |
| private float mShadowRadius; |
| private float mShadowVisibility; |
| private float mInnerCircleRadius; |
| private float mInnerCircleBorderWidth; |
| |
| OvalShadowPainter( |
| float shadowWidth, |
| float shadowVisibility, |
| float innerCircleRadius, |
| float innerCircleBorderWidth) { |
| mShadowWidth = shadowWidth; |
| mShadowVisibility = shadowVisibility; |
| mInnerCircleRadius = innerCircleRadius; |
| mInnerCircleBorderWidth = innerCircleBorderWidth; |
| mShadowRadius = |
| mInnerCircleRadius + mInnerCircleBorderWidth + mShadowWidth * mShadowVisibility; |
| mShadowPaint.setColor(Color.BLACK); |
| mShadowPaint.setStyle(Style.FILL); |
| mShadowPaint.setAntiAlias(true); |
| updateRadialGradient(); |
| } |
| |
| void draw(Canvas canvas, float alpha) { |
| if (mShadowWidth > 0 && mShadowVisibility > 0) { |
| mShadowPaint.setAlpha(Math.round(mShadowPaint.getAlpha() * alpha)); |
| canvas.drawCircle(mBounds.centerX(), mBounds.centerY(), mShadowRadius, |
| mShadowPaint); |
| } |
| } |
| |
| void setBounds(@Px int left, @Px int top, @Px int right, @Px int bottom) { |
| mBounds.set(left, top, right, bottom); |
| updateRadialGradient(); |
| } |
| |
| void setInnerCircleRadius(float newInnerCircleRadius) { |
| mInnerCircleRadius = newInnerCircleRadius; |
| updateRadialGradient(); |
| } |
| |
| void setInnerCircleBorderWidth(float newInnerCircleBorderWidth) { |
| mInnerCircleBorderWidth = newInnerCircleBorderWidth; |
| updateRadialGradient(); |
| } |
| |
| void setShadowVisibility(float newShadowVisibility) { |
| mShadowVisibility = newShadowVisibility; |
| updateRadialGradient(); |
| } |
| |
| private void updateRadialGradient() { |
| // Make the shadow start beyond the circled and possibly the border. |
| mShadowRadius = |
| mInnerCircleRadius + mInnerCircleBorderWidth + mShadowWidth * mShadowVisibility; |
| // This may happen if the innerCircleRadius has not been correctly computed yet while |
| // the view has already been inflated, but not yet measured. In this case, if the view |
| // specifies the radius as a percentage of the screen width, then that evaluates to 0 |
| // and will be corrected after measuring, through onSizeChanged(). |
| if (mShadowRadius > 0) { |
| mShadowPaint.setShader( |
| new RadialGradient( |
| mBounds.centerX(), |
| mBounds.centerY(), |
| mShadowRadius, |
| mShaderColors, |
| mShaderStops, |
| Shader.TileMode.MIRROR)); |
| } |
| } |
| } |
| } |