diff options
Diffstat (limited to 'src/android/support/wearable/view/CircledImageView.java')
-rw-r--r-- | src/android/support/wearable/view/CircledImageView.java | 603 |
1 files changed, 603 insertions, 0 deletions
diff --git a/src/android/support/wearable/view/CircledImageView.java b/src/android/support/wearable/view/CircledImageView.java new file mode 100644 index 00000000..53cb78cf --- /dev/null +++ b/src/android/support/wearable/view/CircledImageView.java @@ -0,0 +1,603 @@ +/* + * 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 android.support.wearable.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.packageinstaller.R; + +import com.android.packageinstaller.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; + } +} |