/*
* Copyright (C) 2015 Jorge Castillo Pérez
*
* 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 com.github.jorgecastilloprz.progressarc;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.view.animation.DecelerateInterpolator;
import com.github.jorgecastilloprz.progressarc.animations.ArcAnimationFactory;
import static com.github.jorgecastilloprz.utils.AnimationUtils.getAnimatedFraction;
/**
* This view is used to draw the progress circle animated arc
* Canvas and angles will be our best friends here.
*
* @author Jorge Castillo Pérez
*/
final class ProgressArcDrawable extends Drawable implements Animatable {
private final RectF arcBounds = new RectF();
private float currentSweepAngle;
private float currentRotationAngleOffset;
private float currentRotationAngle;
private ArcAnimationFactory animationFactory;
private ValueAnimator rotateAnim;
private ValueAnimator growAnim;
private ValueAnimator shrinkAnim;
private ValueAnimator completeAnim;
private boolean animationPlaying;
private boolean growing;
private boolean completeAnimOnNextCycle;
private Paint paint;
private float strokeWidth;
private int arcColor;
private int minSweepAngle;
private int maxSweepAngle;
private ArcListener internalListener;
ProgressArcDrawable(float strokeWidth, int arcColor, boolean roundedStroke) {
this.strokeWidth = strokeWidth;
this.arcColor = arcColor;
initPaint(roundedStroke);
setupAnimations();
}
private void initPaint(boolean roundedStroke) {
paint = new Paint();
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(strokeWidth);
paint.setStrokeCap(roundedStroke ? Paint.Cap.ROUND : Paint.Cap.BUTT);
paint.setColor(arcColor);
}
private void setupAnimations() {
animationFactory = new ArcAnimationFactory();
minSweepAngle = ArcAnimationFactory.MINIMUM_SWEEP_ANGLE;
maxSweepAngle = ArcAnimationFactory.MAXIMUM_SWEEP_ANGLE;
setupRotateAnimation();
setupGrowAnimation();
setupShrinkAnimation();
setupCompleteAnimation();
}
private void setupRotateAnimation() {
rotateAnim = animationFactory.buildAnimation(ArcAnimationFactory.Type.ROTATE,
new ValueAnimator.AnimatorUpdateListener() {
@Override public void onAnimationUpdate(ValueAnimator animation) {
float angle = getAnimatedFraction(animation) * 360f;
updateCurrentRotationAngle(angle);
}
}, null);
}
private void setupGrowAnimation() {
growAnim = animationFactory.buildAnimation(ArcAnimationFactory.Type.GROW,
new ValueAnimator.AnimatorUpdateListener() {
@Override public void onAnimationUpdate(ValueAnimator animation) {
float animatedFraction = getAnimatedFraction(animation);
float angle = minSweepAngle + animatedFraction * (maxSweepAngle - minSweepAngle);
updateCurrentSweepAngle(angle);
}
}, new Animator.AnimatorListener() {
boolean cancelled = false;
@Override public void onAnimationStart(Animator animation) {
cancelled = false;
growing = true;
}
@Override public void onAnimationEnd(Animator animation) {
if (!cancelled) {
setShrinking();
shrinkAnim.start();
}
}
@Override public void onAnimationCancel(Animator animation) {
cancelled = true;
}
@Override public void onAnimationRepeat(Animator animation) {
}
});
}
private void setupShrinkAnimation() {
shrinkAnim = animationFactory.buildAnimation(ArcAnimationFactory.Type.SHRINK,
new ValueAnimator.AnimatorUpdateListener() {
@Override public void onAnimationUpdate(ValueAnimator animation) {
float animatedFraction = getAnimatedFraction(animation);
updateCurrentSweepAngle(
maxSweepAngle - animatedFraction * (maxSweepAngle - minSweepAngle));
}
}, new Animator.AnimatorListener() {
boolean cancelled;
@Override public void onAnimationStart(Animator animation) {
cancelled = false;
}
@Override public void onAnimationEnd(Animator animation) {
if (!cancelled) {
setGrowing();
if (completeAnimOnNextCycle) {
completeAnimOnNextCycle = false;
completeAnim.start();
} else {
growAnim.start();
}
}
}
@Override public void onAnimationCancel(Animator animation) {
cancelled = true;
}
@Override public void onAnimationRepeat(Animator animation) {
}
});
}
private void setupCompleteAnimation() {
completeAnim = animationFactory.buildAnimation(ArcAnimationFactory.Type.COMPLETE,
new ValueAnimator.AnimatorUpdateListener() {
@Override public void onAnimationUpdate(ValueAnimator animation) {
float animatedFraction = getAnimatedFraction(animation);
float angle = minSweepAngle + animatedFraction * 360;
updateCurrentSweepAngle(angle);
}
}, new Animator.AnimatorListener() {
boolean cancelled = false;
@Override public void onAnimationStart(Animator animation) {
cancelled = false;
growing = true;
rotateAnim.setInterpolator(new DecelerateInterpolator());
rotateAnim.setDuration(ArcAnimationFactory.COMPLETE_ROTATE_DURATION);
}
@Override public void onAnimationEnd(Animator animation) {
if (!cancelled) {
stop();
}
completeAnim.removeListener(this);
internalListener.onArcAnimationComplete();
}
@Override public void onAnimationCancel(Animator animation) {
cancelled = true;
}
@Override public void onAnimationRepeat(Animator animation) {
}
});
}
@Override public void draw(Canvas canvas) {
float startAngle = currentRotationAngle - currentRotationAngleOffset;
float sweepAngle = currentSweepAngle;
if (!growing) {
startAngle = startAngle + (360 - sweepAngle);
}
canvas.drawArc(arcBounds, startAngle, sweepAngle, false, paint);
}
public void reset() {
stop();
resetProperties();
setupAnimations();
start();
}
private void resetProperties() {
currentSweepAngle = 0;
currentRotationAngle = 0;
currentRotationAngleOffset = 0;
}
@Override protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
arcBounds.left = bounds.left;
arcBounds.right = bounds.right;
arcBounds.top = bounds.top;
arcBounds.bottom = bounds.bottom;
}
private void setGrowing() {
growing = true;
currentRotationAngleOffset += minSweepAngle;
}
private void setShrinking() {
growing = false;
currentRotationAngleOffset = currentRotationAngleOffset + (360 - maxSweepAngle);
}
@Override public void start() {
animationPlaying = true;
resetProperties();
rotateAnim.start();
growAnim.start();
invalidateSelf();
}
@Override public void stop() {
animationPlaying = false;
stopAnimators();
invalidateSelf();
}
private void stopAnimators() {
rotateAnim.cancel();
growAnim.cancel();
shrinkAnim.cancel();
completeAnim.cancel();
}
void requestCompleteAnimation(final ArcListener internalListener) {
if (!isRunning() || completeAnim.isRunning()) {
return;
}
this.internalListener = internalListener;
startCompleteAnimationOnNextCycle();
}
private void startCompleteAnimationOnNextCycle() {
completeAnimOnNextCycle = true;
}
void updateCurrentRotationAngle(float currentRotationAngle) {
this.currentRotationAngle = currentRotationAngle;
invalidateSelf();
}
void updateCurrentSweepAngle(float currentSweepAngle) {
this.currentSweepAngle = currentSweepAngle;
invalidateSelf();
}
@Override public boolean isRunning() {
return animationPlaying;
}
@Override public void setAlpha(int alpha) {
paint.setAlpha(alpha);
}
@Override public void setColorFilter(ColorFilter colorFilter) {
paint.setColorFilter(colorFilter);
}
@Override public int getOpacity() {
return PixelFormat.RGB_565;
}
}