/* * 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.ObjectAnimator; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.annotation.TargetApi; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.os.Build; import android.util.Property; import android.view.animation.LinearInterpolator; /** * Drawable for showing an indeterminate progress indicator. * * TODO: When Material progress drawable is available in the support library stop using this. * * @hide */ @TargetApi(Build.VERSION_CODES.KITKAT_WATCH) class ProgressDrawable extends Drawable { private static Property LEVEL = new Property(Integer.class, "level") { @Override public Integer get(ProgressDrawable drawable) { return drawable.getLevel(); } @Override public void set(ProgressDrawable drawable, Integer value) { drawable.setLevel(value); drawable.invalidateSelf(); } }; /** Max level for a level drawable, as specified in developer docs for {@link Drawable}. */ private static final int MAX_LEVEL = 10000; /** How many different sections are there, five gives us the material style star. **/ private static final int NUMBER_OF_SEGMENTS = 5; private static final int LEVELS_PER_SEGMENT = MAX_LEVEL / NUMBER_OF_SEGMENTS; private static final float STARTING_ANGLE = -90f; private static final long ANIMATION_DURATION = 6000; private static final int FULL_CIRCLE = 360; private static final int MAX_SWEEP = 306; private static final int CORRECTION_ANGLE = FULL_CIRCLE - MAX_SWEEP; /** How far through each cycle does the bar stop growing and start shrinking, half way. **/ private static final float GROW_SHRINK_RATIO = 0.5f; // TODO: replace this with BakedBezierInterpolator when its available in support library. private static final TimeInterpolator mInterpolator = Gusterpolator.INSTANCE; private final RectF mInnerCircleBounds = new RectF(); private final Paint mPaint = new Paint(); private final ObjectAnimator mAnimator; private float mCircleBorderWidth; private int mCircleBorderColor; public ProgressDrawable() { mPaint.setAntiAlias(true); mPaint.setStyle(Paint.Style.STROKE); mAnimator = ObjectAnimator.ofInt(this, LEVEL, 0, MAX_LEVEL); mAnimator.setRepeatCount(ValueAnimator.INFINITE); mAnimator.setRepeatMode(ValueAnimator.RESTART); mAnimator.setDuration(ANIMATION_DURATION); mAnimator.setInterpolator(new LinearInterpolator()); } public void setRingColor(int color) { mCircleBorderColor = color; } public void setRingWidth(float width) { mCircleBorderWidth = width; } public void startAnimation() { mAnimator.start(); } public void stopAnimation() { mAnimator.cancel(); } @Override public void draw(Canvas canvas) { canvas.save(); mInnerCircleBounds.set(getBounds()); mInnerCircleBounds.inset(mCircleBorderWidth / 2.0f, mCircleBorderWidth / 2.0f); mPaint.setStrokeWidth(mCircleBorderWidth); mPaint.setColor(mCircleBorderColor); float sweepAngle = FULL_CIRCLE; boolean growing = false; float correctionAngle = 0; int level = getLevel(); int currentSegment = level / LEVELS_PER_SEGMENT; int offset = currentSegment * LEVELS_PER_SEGMENT; float progress = (level - offset) / (float) LEVELS_PER_SEGMENT; growing = progress < GROW_SHRINK_RATIO; correctionAngle = CORRECTION_ANGLE * progress; if (growing) { sweepAngle = MAX_SWEEP * mInterpolator.getInterpolation( lerpInv(0f, GROW_SHRINK_RATIO, progress)); } else { sweepAngle = MAX_SWEEP * (1.0f - mInterpolator.getInterpolation( lerpInv(GROW_SHRINK_RATIO, 1.0f, progress))); } sweepAngle = Math.max(1, sweepAngle); canvas.rotate( level * (1.0f / MAX_LEVEL) * 2 * FULL_CIRCLE + STARTING_ANGLE + correctionAngle, mInnerCircleBounds.centerX(), mInnerCircleBounds.centerY()); canvas.drawArc(mInnerCircleBounds, growing ? 0 : MAX_SWEEP - sweepAngle, sweepAngle, false, mPaint); canvas.restore(); } @Override public void setAlpha(int i) { // Not supported. } @Override public void setColorFilter(ColorFilter colorFilter) { // Not supported. } @Override public int getOpacity() { return PixelFormat.OPAQUE; } @Override protected boolean onLevelChange(int level) { return true; // Changing the level of this drawable does change its appearance. } /** * Returns the interpolation scalar (s) that satisfies the equation: * {@code value = }lerp(a, b, s) * *

If {@code a == b}, then this function will return 0. */ private static float lerpInv(float a, float b, float value) { return a != b ? ((value - a) / (b - a)) : 0.0f; } }