| /* | |
| * 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)); | |
| } | |
| } | |
| } | |
| } |