44

Is possible to attach an animator to a path?

Is there any other way to draw animated lines on the Canvas?

I searched this before I posted but I couldn’t find anything. In two other posts Draw a path as animation on Canvas in Android and How to draw a path on an Android Canvas with animation there are workaround solutions which does not work for me.

I post my code inside the onDraw method to specify what exactly I want.

paint.setStyle(Paint.Style.STROKE);
    paint.setStrokeWidth(2);
    paint.setColor(Color.BLACK);

    Path path = new Path();
    path.moveTo(10, 50);   // THIS TRANSFORMATIONS TO BE ANIMATED!!!!!!!!
    path.lineTo(40, 50);
    path.moveTo(40, 50);
    path.lineTo(50, 40);
    // and so on...

    canvas.drawPath(path, paint);
Ola Ström
  • 4,136
  • 5
  • 22
  • 41
karvoynistas
  • 1,295
  • 1
  • 14
  • 30

3 Answers3

54

You can transform your canvas by time, i.e:

class MyView extends View {

    int framesPerSecond = 60;
    long animationDuration = 10000; // 10 seconds

    Matrix matrix = new Matrix(); // transformation matrix

    Path path = new Path();       // your path
    Paint paint = new Paint();    // your paint

    long startTime;

    public MyView(Context context) {
        super(context);

        // start the animation:
        this.startTime = System.currentTimeMillis();
        this.postInvalidate(); 
    }

    @Override
    protected void onDraw(Canvas canvas) {

        long elapsedTime = System.currentTimeMillis() - startTime;

        matrix.postRotate(30 * elapsedTime/1000);        // rotate 30° every second
        matrix.postTranslate(100 * elapsedTime/1000, 0); // move 100 pixels to the right
        // other transformations...

        canvas.concat(matrix);        // call this before drawing on the canvas!!

        canvas.drawPath(path, paint); // draw on canvas

        if(elapsedTime < animationDuration)
            this.postInvalidateDelayed( 1000 / framesPerSecond);
    }

}
Simon
  • 556
  • 5
  • 5
  • What about circle? I mean, how i can animate draw arc or oval? – user3111850 Sep 16 '14 at 12:52
  • Made 2 changes. 1: added invalidate(); 2. matrix.postRotate(30.0f); The line matrix.postRotate(30 * elapsedTime/1000); will cause your animation too much fast. – Faizan Mubasher Sep 19 '14 at 06:25
  • 5
    The framerate approach is not that good an idea if you want timing exact animation - but it works perfect for small effects. – slott Sep 25 '14 at 08:44
  • @slott good point, but how'd you do a more precise animation? – ggorlen Oct 22 '21 at 04:55
  • 1
    For more complex animations I'd use a texture view to ensure best draw performance. Then you could have a thread running timing perfect and direct the primary render thread. – slott Oct 23 '21 at 07:50
26

try this:

class PathDrawable extends Drawable implements AnimatorUpdateListener  {
    private Path mPath;
    private Paint mPaint;
    private ValueAnimator mAnimator;

    public PathDrawable() {
        mPath = new Path();
        mPaint = new Paint();
        mPaint.setColor(0xffffffff);
        mPaint.setStrokeWidth(5);
        mPaint.setStyle(Style.STROKE);
    }

    public void startAnimating() {
        Rect b = getBounds();
        mAnimator = ValueAnimator.ofInt(-b.bottom, b.bottom);
        mAnimator.setDuration(1000);
        mAnimator.addUpdateListener(this);
        mAnimator.start();
    }

    @Override
    public void draw(Canvas canvas) {
        canvas.drawPath(mPath, mPaint);
    }

    @Override
    public void setAlpha(int alpha) {
    }

    @Override
    public void setColorFilter(ColorFilter cf) {
    }

    @Override
    public int getOpacity() {
        return PixelFormat.TRANSLUCENT;
    }

    @Override
    public void onAnimationUpdate(ValueAnimator animator) {
        mPath.reset();
        Rect b = getBounds();
        mPath.moveTo(b.left, b.bottom);
        mPath.quadTo((b.right-b.left)/2, (Integer) animator.getAnimatedValue(), b.right, b.bottom);
        invalidateSelf();
    }
}

to test it add in your onCreate method:

TextView view = new TextView(this);
view.setText("click me");
view.setTextColor(0xffcccccc);
view.setGravity(Gravity.CENTER);
view.setTextSize(48);
final PathDrawable d = new PathDrawable();
view.setBackgroundDrawable(d);
OnClickListener l = new OnClickListener() {
    @Override
    public void onClick(View v) {
        d.startAnimating();
    }
};
view.setOnClickListener(l);
setContentView(view);
pskink
  • 23,874
  • 6
  • 66
  • 77
  • 2
    Draws the line immediately, without animation – azizbekian Sep 22 '15 at 10:45
  • code works.. but i wanted to set circular background of any color along with stroke as u mentioned. Have any idea? – Sagar Pujari Oct 27 '15 at 04:54
  • This should be the accepted answer... can be tweaked to affect all draws.. like; mAnimator = ValueAnimator.ofInt(0.0f, 1.0f); then you can multiply all values by animated val – Jessicardo May 13 '16 at 14:05
  • Could you elaborate? I mean have you benchmarked and found this one favorable over an accepted answer or you just tried this and it worked for you, that's why you think this should be the answer? – Farid Jul 22 '19 at 09:33
  • @FARID because ObjectAnimator is used for any animations and ObjectAnimator extends ValueAnimator so use one of them – pskink Jul 23 '19 at 05:57
  • Ohh, I was actually replying to @Jessicardo forgot to tag the user. He said this should be the answer that's why I just asked if he says so just because this works for him or his comment is based on some benchmark (or documentation) – Farid Jul 23 '19 at 07:23
  • @FARID Because this answer is dynamic and can be expanded to all kinds of animations. It is also the right way to do it. – Jessicardo Jul 24 '19 at 09:28
4

You can create a PathMeasure for your path and determine the length of it:

private PathMeasure pathMeasure; // field

// After you've created your path
pathMeasure = new PathMeasure(path, false); 
pathLength = pathMeasure.getLength();

You can then get get a specific point on the path using getPosTan() inside, say, a ValueAnimator's update listener:

final float[] position = new float[2]; // field

// Inside your animation's update method, where dt is your 0..1 animated fraction
final float distance = dt * pathLength;
pathMeasure.getPosTan(distance, position, null);

// If using onDraw you'll need to tell the view to redraw using the new position
invalidate(); 

You can then make use of the position in onDraw (or whatever).

canvas.drawCircle(position[0], position[1], radius, paint);

The advantages of this approach is that it is straightforward, doesn't require chunky maths, and works on all APIs.

If using API 21+, you can use a ValueAnimator and pass in a Path to use its positions, which is simpler. Example SO question.

Community
  • 1
  • 1
Tom
  • 6,946
  • 2
  • 47
  • 63