Search code examples
androidanimationandroid-custom-viewobjectanimator

How do I animate smoothly the current progress for SendingProgressView?


I have created a repo for this so anyone can test this out themselves. The repo assumes that it takes 1 second to upload 20% so the upload will be completed after 5 seconds:

https://github.com/Winghin2517/SendingProgressViewTest.git

This is the code for instamaterial's SendingProgressView- you can find the code on github here

package io.github.froger.instamaterial.ui.view;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.RectF;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.OvershootInterpolator;

import io.github.froger.instamaterial.R;

/**
 * Created by Miroslaw Stanek on 28.02.15.
 */
public class SendingProgressView extends View {
    public static final int STATE_NOT_STARTED = 0;
    public static final int STATE_PROGRESS_STARTED = 1;
    public static final int STATE_DONE_STARTED = 2;
    public static final int STATE_FINISHED = 3;

    private static final int PROGRESS_STROKE_SIZE = 10;
    private static final int INNER_CIRCLE_PADDING = 30;
    private static final int MAX_DONE_BG_OFFSET = 800;
    private static final int MAX_DONE_IMG_OFFSET = 400;

    private int state = STATE_NOT_STARTED;
    private float currentProgress = 0;
    private float currentDoneBgOffset = MAX_DONE_BG_OFFSET;
    private float currentCheckmarkOffset = MAX_DONE_IMG_OFFSET;

    private Paint progressPaint;
    private Paint doneBgPaint;
    private Paint maskPaint;

    private RectF progressBounds;

    private Bitmap checkmarkBitmap;
    private Bitmap innerCircleMaskBitmap;

    private int checkmarkXPosition = 0;
    private int checkmarkYPosition = 0;

    private Paint checkmarkPaint;
    private Bitmap tempBitmap;
    private Canvas tempCanvas;

    private ObjectAnimator simulateProgressAnimator;
    private ObjectAnimator doneBgAnimator;
    private ObjectAnimator checkmarkAnimator;

    private OnLoadingFinishedListener onLoadingFinishedListener;

    public SendingProgressView(Context context) {
        super(context);
        init();
    }

    public SendingProgressView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public SendingProgressView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public SendingProgressView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init();
    }

    private void init() {
        setupProgressPaint();
        setupDonePaints();
        setupSimulateProgressAnimator();
        setupDoneAnimators();
    }

    private void setupProgressPaint() {
        progressPaint = new Paint();
        progressPaint.setAntiAlias(true);
        progressPaint.setStyle(Paint.Style.STROKE);
        progressPaint.setColor(0xffffffff);
        progressPaint.setStrokeWidth(PROGRESS_STROKE_SIZE);
    }

    private void setupSimulateProgressAnimator() {
        simulateProgressAnimator = ObjectAnimator.ofFloat(this, "currentProgress", 0, 100).setDuration(2000);
        simulateProgressAnimator.setInterpolator(new AccelerateInterpolator());
        simulateProgressAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                changeState(STATE_DONE_STARTED);
            }
        });
    }

    private void setupDonePaints() {
        doneBgPaint = new Paint();
        doneBgPaint.setAntiAlias(true);
        doneBgPaint.setStyle(Paint.Style.FILL);
        doneBgPaint.setColor(0xff39cb72);

        checkmarkPaint = new Paint();

        maskPaint = new Paint();
        maskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
    }

    private void setupDoneAnimators() {
        doneBgAnimator = ObjectAnimator.ofFloat(this, "currentDoneBgOffset", MAX_DONE_BG_OFFSET, 0).setDuration(300);
        doneBgAnimator.setInterpolator(new DecelerateInterpolator());

        checkmarkAnimator = ObjectAnimator.ofFloat(this, "currentCheckmarkOffset", MAX_DONE_IMG_OFFSET, 0).setDuration(300);
        checkmarkAnimator.setInterpolator(new OvershootInterpolator());
        checkmarkAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                changeState(STATE_FINISHED);
            }
        });
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        updateProgressBounds();
        setupCheckmarkBitmap();
        setupDoneMaskBitmap();
        resetTempCanvas();
    }

    private void updateProgressBounds() {
        progressBounds = new RectF(
                PROGRESS_STROKE_SIZE, PROGRESS_STROKE_SIZE,
                getWidth() - PROGRESS_STROKE_SIZE, getWidth() - PROGRESS_STROKE_SIZE
        );
    }

    private void setupCheckmarkBitmap() {
        checkmarkBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_done_white_48dp);
        checkmarkXPosition = getWidth() / 2 - checkmarkBitmap.getWidth() / 2;
        checkmarkYPosition = getWidth() / 2 - checkmarkBitmap.getHeight() / 2;
    }

    private void setupDoneMaskBitmap() {
        innerCircleMaskBitmap = Bitmap.createBitmap(getWidth(), getWidth(), Bitmap.Config.ARGB_8888);
        Canvas srcCanvas = new Canvas(innerCircleMaskBitmap);
        srcCanvas.drawCircle(getWidth() / 2, getWidth() / 2, getWidth() / 2 - INNER_CIRCLE_PADDING, new Paint());
    }

    private void resetTempCanvas() {
        tempBitmap = Bitmap.createBitmap(getWidth(), getWidth(), Bitmap.Config.ARGB_8888);
        tempCanvas = new Canvas(tempBitmap);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (state == STATE_PROGRESS_STARTED) {
            drawArcForCurrentProgress();
        } else if (state == STATE_DONE_STARTED) {
            drawFrameForDoneAnimation();
            postInvalidate();
        } else if (state == STATE_FINISHED) {
            drawFinishedState();
        }

        canvas.drawBitmap(tempBitmap, 0, 0, null);
    }

    private void drawArcForCurrentProgress() {
        tempCanvas.drawArc(progressBounds, -90f, 360 * currentProgress / 100, false, progressPaint);
    }

    private void drawFrameForDoneAnimation() {
        tempCanvas.drawCircle(getWidth() / 2, getWidth() / 2 + currentDoneBgOffset, getWidth() / 2 - INNER_CIRCLE_PADDING, doneBgPaint);
        tempCanvas.drawBitmap(checkmarkBitmap, checkmarkXPosition, checkmarkYPosition + currentCheckmarkOffset, checkmarkPaint);
        tempCanvas.drawBitmap(innerCircleMaskBitmap, 0, 0, maskPaint);
        tempCanvas.drawArc(progressBounds, 0, 360f, false, progressPaint);
    }

    private void drawFinishedState() {
        tempCanvas.drawCircle(getWidth() / 2, getWidth() / 2, getWidth() / 2 - INNER_CIRCLE_PADDING, doneBgPaint);
        tempCanvas.drawBitmap(checkmarkBitmap, checkmarkXPosition, checkmarkYPosition, checkmarkPaint);
        tempCanvas.drawArc(progressBounds, 0, 360f, false, progressPaint);
    }

    private void changeState(int state) {
        if (this.state == state) {
            return;
        }

        tempBitmap.recycle();
        resetTempCanvas();

        this.state = state;
        if (state == STATE_PROGRESS_STARTED) {
            setCurrentProgress(0);
            simulateProgressAnimator.start();
        } else if (state == STATE_DONE_STARTED) {
            setCurrentDoneBgOffset(MAX_DONE_BG_OFFSET);
            setCurrentCheckmarkOffset(MAX_DONE_IMG_OFFSET);
            AnimatorSet animatorSet = new AnimatorSet();
            animatorSet.playSequentially(doneBgAnimator, checkmarkAnimator);
            animatorSet.start();
        } else if (state == STATE_FINISHED) {
            if (onLoadingFinishedListener != null) {
                onLoadingFinishedListener.onLoadingFinished();
            }
        }
    }

    public void simulateProgress() {
        changeState(STATE_PROGRESS_STARTED);
    }

    public void setCurrentProgress(float currentProgress) {
        this.currentProgress = currentProgress;
        postInvalidate();
    }

    public void setCurrentDoneBgOffset(float currentDoneBgOffset) {
        this.currentDoneBgOffset = currentDoneBgOffset;
        postInvalidate();
    }

    public void setCurrentCheckmarkOffset(float currentCheckmarkOffset) {
        this.currentCheckmarkOffset = currentCheckmarkOffset;
        postInvalidate();
    }

    public void setOnLoadingFinishedListener(OnLoadingFinishedListener onLoadingFinishedListener) {
        this.onLoadingFinishedListener = onLoadingFinishedListener;
    }

    public interface OnLoadingFinishedListener {
        public void onLoadingFinished();
    }
}

I managed to implement it in my app and tie it to my upload api so that when the picture is uploading, the progress circle will be drawn, see the animation below:

enter image description here

You can see that the animation seems disjointed - for example, when the progress goes from like 35% to 50%, you can see it doesn't animate smoothly, it just draw more of the arc to show that it is now at 50%.

In my app, I'm using the method within SendingProgressView called setCurrentProgress to set the currentProgress of the view depending on value returned from the network for the progress of my image being uploaded. The method is displayed below:

public void setCurrentProgress(float currentProgress) {
    this.currentProgress = currentProgress;
    postInvalidate();
}

Each time the view postInvalidates itself, it draws a little more of the arc but it doesn't animate the drawing of the arc itself. I would like it to animate the progress more smoothly.

I tried to animate the drawing of the arc by changing the code of setCurrentProgress to use ObjectAnimator:

public void setCurrentProgress(float currentProgress) {

    ObjectAnimator simulateProgressAnimator =
            ObjectAnimator.ofFloat(this, "currentProgress", this.currentProgress, currentProgress).setDuration(200);
    simulateProgressAnimator.setInterpolator(new AccelerateInterpolator());
    this.currentProgress = currentProgress;
    if (!simulateProgressAnimator.isStarted()) {
        simulateProgressAnimator.start();
    }
}

but the app just ends up crashing:

04-23 17:40:35.938 14196-14196/com.myapp E/AndroidRuntime: FATAL EXCEPTION: main
                                                             Process: com.myapp, PID: 14196
                                                             java.lang.StackOverflowError: stack size 8MB
                                                                 at android.animation.PropertyValuesHolder.nCallFloatMethod(Native Method)
                                                                 at android.animation.PropertyValuesHolder.access$400(PropertyValuesHolder.java:39)
                                                                 at android.animation.PropertyValuesHolder$FloatPropertyValuesHolder.setAnimatedValue(PropertyValuesHolder.java:1298)
                                                                 at android.animation.ObjectAnimator.animateValue(ObjectAnimator.java:956)
                                                                 at android.animation.ValueAnimator.setCurrentFraction(ValueAnimator.java:602)
                                                                 at android.animation.ValueAnimator.setCurrentPlayTime(ValueAnimator.java:550)
                                                                 at android.animation.ValueAnimator.start(ValueAnimator.java:1039)
                                                                 at android.animation.ValueAnimator.start(ValueAnimator.java:1050)
                                                                 at android.animation.ObjectAnimator.start(ObjectAnimator.java:829)
                                                                 at com.myapp.customshapes.SendingProgressView.setCurrentProgress(SendingProgressView.java:222)
                                                                 at android.animation.PropertyValuesHolder.nCallFloatMethod(Native Method)
                                                                 at android.animation.PropertyValuesHolder.access$400(PropertyValuesHolder.java:39)
                                                                 at android.animation.PropertyValuesHolder$FloatPropertyValuesHolder.setAnimatedValue(PropertyValuesHolder.java:1298)
                                                                 at android.animation.ObjectAnimator.animateValue(ObjectAnimator.java:956)
                                                                 at android.animation.ValueAnimator.setCurrentFraction(ValueAnimator.java:602)
                                                                 at android.animation.ValueAnimator.setCurrentPlayTime(ValueAnimator.java:550)
                                                                 at android.animation.ValueAnimator.start(ValueAnimator.java:1039)
                                                                 at android.animation.ValueAnimator.start(ValueAnimator.java:1050)
                                                                 at android.animation.ObjectAnimator.start(ObjectAnimator.java:829)
                                                                 at com.myapp.customshapes.SendingProgressView.setCurrentProgress(SendingProgressView.java:222)
                                                                 at android.animation.PropertyValuesHolder.nCallFloatMethod(Native Method)
                                                                 at android.animation.PropertyValuesHolder.access$400(PropertyValuesHolder.java:39)
                                                                 at android.animation.PropertyValuesHolder$FloatPropertyValuesHolder.setAnimatedValue(PropertyValuesHolder.java:1298)
                                                                 at android.animation.ObjectAnimator.animateValue(ObjectAnimator.java:956)
                                                                 at android.animation.ValueAnimator.setCurrentFraction(ValueAnimator.java:602)
                                                                 at android.animation.ValueAnimator.setCurrentPlayTime(ValueAnimator.java:550)
                                                                 at android.animation.ValueAnimator.start(ValueAnimator.java:1039)
                                                                 at android.animation.ValueAnimator.start(ValueAnimator.java:1050)
                                                                 at android.animation.ObjectAnimator.start(ObjectAnimator.java:829)
                                                                 at com.myapp.customshapes.SendingProgressView.setCurrentProgress(SendingProgressView.java:222)
                                                                 at android.animation.PropertyValuesHolder.nCallFloatMethod(Native Method)
                                                                 at android.animation.PropertyValuesHolder.access$400(PropertyValuesHolder.java:39)
                                                                 at android.animation.PropertyValuesHolder$FloatPropertyValuesHolder.setAnimatedValue(PropertyValuesHolder.java:1298)
                                                                 at android.animation.ObjectAnimator.animateValue(ObjectAnimator.java:956)
                                                                 at android.animation.ValueAnimator.setCurrentFraction(ValueAnimator.java:602)
                                                                 at android.animation.ValueAnimator.setCurrentPlayTime(ValueAnimator.java:550)
                                                                 at android.animation.ValueAnimator.start(ValueAnimator.java:1039)
                                                                 at android.animation.ValueAnimator.start(ValueAnimator.java:1050)
                                                                 at android.animation.ObjectAnimator.start(ObjectAnimator.java:829)
                                                                 at com.myapp.customshapes.SendingProgressView.setCurrentProgress(SendingProgressView.java:222)
                                                                 at android.animation.PropertyValuesHolder.nCallFloatMethod(Native Method)
                                                                 at android.animation.PropertyValuesHolder.access$400(PropertyValuesHolder.java:39)
                                                                 at android.animation.PropertyValuesHolder$FloatPropertyValuesHolder.setAnimatedValue(PropertyValuesHolder.java:1298)
                                                                 at android.animation.ObjectAnimator.animateValue(ObjectAnimator.java:956)
                                                                 at android.animation.ValueAnimator.setCurrentFraction(ValueAnimator.java:602)
                                                                 at android.animation.ValueAnimator.setCurrentPlayTime(ValueAnimator.java:550)
                                                                 at android.animation.ValueAnimator.start(ValueAnimator.java:1039)
                                                                 at android.animation.ValueAnimator.start(ValueAnimator.java:1050)
                                                                 at android.animation.ObjectAnimator.start(ObjectAnimator.java:829)
                                                                at com
04-23 17:40:36.068 14196-14196/com.myapp E/JavaBinder: !!! FAILED BINDER TRANSACTION !!!
04-23 17:40:36.068 14196-14196/com.myapp E/AndroidRuntime: Error reporting crash
                                                             android.os.TransactionTooLargeException
                                                                 at android.os.BinderProxy.transactNative(Native Method)
                                                                 at android.os.BinderProxy.transact(Binder.java:496)
                                                                 at android.app.ActivityManagerProxy.handleApplicationCrash(ActivityManagerNative.java:4164)
                                                                 at com.android.internal.os.RuntimeInit$UncaughtHandler.uncaughtException(RuntimeInit.java:89)
                                                                 at com.crashlytics.android.core.CrashlyticsUncaughtExceptionHandler.uncaughtException(CrashlyticsUncaughtExceptionHandler.java:249)
                                                                 at com.myapp.activities.Application$1.uncaughtException(Application.java:56)
                                                                 at com.flurry.sdk.mc.b(SourceFile:96)
                                                                 at com.flurry.sdk.mc.b(SourceFile:19)
                                                                 at com.flurry.sdk.mc$a.uncaughtException(SourceFile:107)
                                                                 at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:693)
                                                                 at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:690)

OBJECTIVE:

The goal here is to make the progress wheel animate smoothly when it goes from 20% - 40% - 60% - 80% - 100%. It will also be great if it incorporates an overshoot interpolator so that the circle will draw with a little bit of an overshoot each time to show motion.


Solution

  • Using ValueAnimator should be a quite good solution in that case.

    private ValueAnimator drawProgressAnimator;
    
    public void setCurrentProgress(float currentProgress, boolean smoothProgress) {
        //Log.d("setCurrentProgress", "Current value = " + this.currentProgress + "; new target value = " + currentProgress);
        if (drawProgressAnimator != null) {
            drawProgressAnimator.cancel();
            drawProgressAnimator = null;
        }
        if (smoothProgress) {
            drawProgressAnimator = ValueAnimator.ofFloat(this.currentProgress, currentProgress);
            drawProgressAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
            long duration = (long) Math.abs(1500 * ((currentProgress - this.currentProgress) / 100)); // 1.5 second for 100% progress, 750ms for 50% progress and so on
            drawProgressAnimator.setDuration(duration);
            drawProgressAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    //Log.i("onAnimationUpdate", "getAnimatedValue() = " + ((float) animation.getAnimatedValue()));
                    SendingProgressView.this.currentProgress = (float) animation.getAnimatedValue();
                    postInvalidate();
                }
            });
            drawProgressAnimator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    super.onAnimationEnd(animation);
                    drawProgressAnimator = null;
                }
            });
            drawProgressAnimator.start();
        } else {
            this.currentProgress = currentProgress;
            postInvalidate();
        }
    }
    

    enter image description here

    Full git patch.