../path-animation-1

Path animation - 1


Table of contents


This is the very first blog of my animation series.

In this tutorial, I will focus only on how to create animated line using custom path. Let’s make a simple animated horizontal line using CustomPaint, AnimationController and a bit of computation.

Don’t worry! I’ll start from the simple one.


Project Setup

You will need to have Flutter installed on your local machine.
See flutter installation here

Clone this project source code and run command flutter pub get. Then, you are ready to get started.



Line Path creation with CustomPainter

Before we dive into path animation, let's start creating a simple line path that extends `CustomPainter` class.
class LinePathPainter extends CustomPainter {
  final Color color;
  final double strokeWidth;
  const LinePathPainter({
    required this.color,
    this.strokeWidth = 3.0,
  });
  Path _createLinePath(Size size) {
    var path = Path();
    path.lineTo(size.width,0.0);
    return path;
  }

  @override
  void paint(Canvas canvas, Size size) {
    final path = _createLinePath(size);

    final Paint paint = Paint();
    paint.color = color;
    paint.style = PaintingStyle.stroke;
    paint.strokeWidth = strokeWidth;
    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => true;
}

Line path creation occurs inside _createLinePath function. Currently, we only have a line that starts from (0.0,0.0) to (size.width, 0.0).

Learn more about Path class



Path Tracing

Okay now, let’s write the magic code ( not really tho :3 )

Flutter has computeMetrics method that returns PathMetric object which describes various properties about the contours of the Path.

Learn more about computeMetrics here

I’ll use extension methods for path animation logic to provide scalability and memoization technique for better performaance.

extension PathEx on Path {
  static List<PathMetric>? _metricsCache;
  List<PathMetric> getMetrics() {
    _metricsCache ??= computeMetrics().toList();
    return _metricsCache!;
  }

  Path createAnimatedPath(
    double animationPercent,
  ) {
    // If animation is completed, return the original path directly.
    if (animationPercent == 1.0) {
      return this; 
    }

    final totalLength = getMetrics()
        .fold(0.0, (double prev, PathMetric metric) => prev + metric.length);
    final currentLength = totalLength * animationPercent;
    return _extractPathUntilLength(currentLength);
  }

  Path _extractPathUntilLength(
    double length,
  ) {
    var currentLength = 0.0;
    final path = Path();

    var metricsIterator = getMetrics().iterator;
    while (metricsIterator.moveNext()) {
      var metric = metricsIterator.current;
      var nextLength = currentLength + metric.length;
      final isLastSegment = nextLength > length;
      if (isLastSegment) {
        final remainingLength = length - currentLength;
        final pathSegment = metric.extractPath(0.0, remainingLength);
        path.addPath(pathSegment, Offset.zero);
        break;
      } else {
        final pathSegment = metric.extractPath(0.0, metric.length);
        path.addPath(pathSegment, Offset.zero);
      }

      currentLength = nextLength;
    }
    return path;
  }
}

Let’s break down the code.

_metricsCache will be computed once and stored for further use. Total metric length and current metric length based on animation percentage is computed in createAnimatedPath function.

PathMetric has extractPath method takes start, end values and returns the path segment of the given length.

This is it. Computation for path animation is completed.

Now, we just need to add AnimationController and actually animate the path.

Here’s updated LinePathPainter class with animation value. For clarity, only updated part will be shown.

class LinePathPainter extends CustomPainter {
  final Animation<double> _animation;
  final Color color;
  final double strokeWidth;
  const LinePathPainter(
    this._animation, {
    required this.color,
    this.strokeWidth = 3.0,
  });

  // same old code

  @override
  void paint(Canvas canvas, Size size) {
    final animationPercent = _animation.value;
    // call path animation extension method based on current animation value [0.0 to 1.0]
    final path = _createLinePath(size)
                  .createAnimatedPath(animationPercent);
    // same old code
  }
  // same old code
}

After that, you’ll only need to create a StatefulWidget with AnimationController.


class AnimatedWave extends StatefulWidget {
  const AnimatedWave({super.key});

  @override
  AnimatedWaveState createState() => AnimatedWaveState();
}

class AnimatedWaveState extends State<AnimatedWave>
    with SingleTickerProviderStateMixin {
  late final AnimationController pathController;

  @override
  void initState() {
    super.initState();
    pathController = AnimationController(
      vsync: this,
      duration: const Duration(
        milliseconds: 1000,
      ),
    )
    ..repeat(reverse: true)
    // without `setState`, path animation won't show
    ..addListener(
        () => setState(() {}),
      );
  }

  @override
  void dispose() {
    pathController.dispose();
    super.dispose();
  }

  Animation<double> get pathTween => CurvedAnimation(
        parent: pathController,
        curve: Curves.easeInOut,
      );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: SizedBox(
              width: 300,
              height: 5,
              child: CustomPaint(
                painter: LinePathPainter(
                  pathTween,
                  color: Colors.teal,
                ),
              ),
            ),
      ),
    );
  }
}

Everything is ready now. Here’s the result.


With this, we can create more complex animated paths. Here’s another example of animated wave path.

class WavePathPainter extends CustomPainter {
  final Animation<double> _animation;
  final Color color;
  final double strokeWidth;
  const WavePathPainter(
    this._animation, {
    required this.color,
    this.strokeWidth = 3.0,
  });

  Path _createWavePath(Size size) {
      var path = Path();
      // wave path code here
      return path;
  }

  @override
  void paint(Canvas canvas, Size size) {
    final animationPercent = _animation.value;
    final path = _createWavePath(size)
                  .createAnimatedPath(animationPercent);

    final Paint paint = Paint();
    paint.color = color;
    paint.style = PaintingStyle.stroke;
    paint.strokeWidth = strokeWidth;
    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => true;
}
}

Every computation related to wave path creation is inside the _createWavePath function.

We can assume that a wave is made of two cubic bezier segments in Flutter. One is drawn from top to bottom and the other is from bottom to top.

Learn more about path.cubicTo method

Here’s the code for 5 cubic bezier segments that form a wave.

var x = 0.0;
var segments = 5;
var increment = size.width / segments;
bool startFromTop = true;
while (x < size.width) {
  if (startFromTop) {
    path.moveTo(x, 0);
    path.cubicTo(
      x + increment / 2, 0, 
      x + increment / 2, size.height,
      x + increment, size.height,
      );
  } else {
    path.moveTo(x, size.height);
    path.cubicTo(
      x + increment / 2, size.height, 
      x + increment / 2, 0,
      x + increment, 0,
      );
  }
  x += increment;
  startFromTop = !startFromTop;
}

And the result is …



It’s beautiful, isn’t it? We can now animate any Path efficiently without adding extra overhead.

Note: Using path animation for very complex Path may lead to performance degradation and frame drops.

[flutter] [dart] [animation]