如何流畅地动画化发送 ProgressView 的当前进度?

How do I animate smoothly the current progress for SendingProgressView?

我为此创建了一个存储库,这样任何人都可以自己测试。 repo 假定上传 20% 需要 1 秒,所以上传将在 5 秒后完成:

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

这是 instamaterial 的 SendingProgressView 的代码 - 您可以在 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();
    }
}

我设法在我的应用程序中实现它并将其绑定到我的上传 api 这样当图片上传时,就会绘制进度圈,请参见下面的动画:

您可以看到动画似乎脱节了 - 例如,当进度从 35% 变为 50% 时,您可以看到它的动画效果不流畅,它只是画了更多的弧线来表明它现在是 50%。

在我的应用程序中,我使用 SendingProgressView 中称为 setCurrentProgress 的方法根据网络返回的值设置视图的 currentProgress图片正在上传。方法如下所示:

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

每次视图 postInvalidates 本身,它都会绘制多一点的圆弧,但不会为圆弧本身的绘制设置动画。我希望它能更流畅地动画进度。

我试图通过将 setCurrentProgress 的代码更改为使用 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();
    }
}

但应用程序最终崩溃了:

04-23 17:40:35.938 14196-14196/com.myapp E/AndroidRuntime: FATAL EXCEPTION: main
                                                             Process: com.myapp, PID: 14196
                                                             java.lang.WhosebugError: stack size 8MB
                                                                 at android.animation.PropertyValuesHolder.nCallFloatMethod(Native Method)
                                                                 at android.animation.PropertyValuesHolder.access0(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.access0(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.access0(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.access0(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.access0(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.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:

这里的目标是让进度轮在 20% - 40% - 60% - 80% - 100% 时平滑地动画。如果它包含一个过冲插值器,那么每次绘制圆时都会有一点过冲以显示运动,这也会很棒。

在这种情况下,使用 ValueAnimator 应该是一个很好的解决方案。

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();
    }
}

Full git patch.