8

I'd like to animate the drawing of a path, i.e. to have it progressively appear on the screen. I am using the canvas and my best guess so far is to use an ObjectAnimator to take care of the animation. However, I cannot figure out how to actually draw the corresponding segment of the path in the onDraw() method. Is there a method that would allow to do this? Would I need to involve path effects for that?

Edit: Using a DashPathEffect and setting its "on" and "off" intervals in the animation to cover the part of the path we want to draw for that step seems to work here, but it requires allocating a new DashPathEffect for every step of the animation. I will leave the question open in case there is a better way.

Gnurou
  • 7,923
  • 3
  • 22
  • 32

3 Answers3

13

Answering my own question, as I figured out a satisfying way to do that.

The trick is to use an ObjectAnimator to progressively change the current length of the stroke, and a DashPathEffect to control the length of the current stroke. The DashPathEffect will have its dashes parameter initially set to the following:

float[] dashes = { 0.0f, Float.MAX_VALUE };

First float is the length of the visible stroke, second length of non-visible part. Second length is chosen to be extremely high. Initial settings thus correspond to a totally invisible stroke.

Then everytime the object animator updates the stroke length value, a new DashPathEffect is created with the new visible part and set to the Painter object, and the view is invalidated:

dashes[0] = newValue;
mPaint.setPathEffect(new DashPathEffect(dashes, 0));
invalidate();

Finally, the onDraw() method uses this painter to draw the path, which will only comprise the portion we want:

canvas.drawPath(path, mPaint);

The only drawback I see is that we must create a new DashPathEffect at every animation step (as they cannot be reused), but globally this is satisfying - the animation is nice and smooth.

Gnurou
  • 7,923
  • 3
  • 22
  • 32
1
package com.nexoslav.dashlineanimatedcanvasdemo;

import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.animation.LinearInterpolator;

import androidx.annotation.Nullable;

public class CustomView extends View {
    float[] dashes = {30, 20};
    Paint mPaint;
    private Path mPath;

    private void init() {

        mPaint = new Paint();
        mPaint.setColor(Color.BLACK);
        mPaint.setStrokeWidth(10f);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setPathEffect(new DashPathEffect(dashes, 0));

        mPath = new Path();
        mPath.moveTo(200, 200);
        mPath.lineTo(300, 100);
        mPath.lineTo(400, 400);
        mPath.lineTo(1000, 200);
        mPath.lineTo(1000, 1000);
        mPath.lineTo(200, 400);

        ValueAnimator animation = ValueAnimator.ofInt(0, 100);
        animation.setInterpolator(new LinearInterpolator());
        animation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                Log.d("bla", "bla: " + valueAnimator.getAnimatedValue());
                mPaint.setPathEffect(new DashPathEffect(dashes, (Integer) valueAnimator.getAnimatedValue()));
                invalidate();
            }
        });
        animation.setDuration(1000);
        animation.setRepeatMode(ValueAnimator.RESTART);
        animation.setRepeatCount(ValueAnimator.INFINITE);
        animation.start();
    }

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


    public CustomView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.drawPath(mPath, mPaint);
    }
}
uberchilly
  • 159
  • 1
  • 7
0

As far as I know, the only way is to start with an empty path and have a runnable that appends points to the path at defined intervals, until it is completed.

Luis
  • 11,978
  • 3
  • 27
  • 35
  • That would make a very rough animation. Say the path only consists of one long curve - it only has one start and stop point and two control points. There would be no way to animate this smoothly using this method. – Gnurou Oct 07 '12 at 15:12
  • Then, you would get a better result using PathEffect. You would still need the runnable to change the PathEffect between redraws. – Luis Oct 07 '12 at 16:51
  • Yep, that is taken care of by ObjectAnimator. DashPathEffect seems to be the best method so far, but it requires allocating a new effect instance at each step - I wonder if we can avoid that. – Gnurou Oct 08 '12 at 07:42