Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to animate a path in flutter?

Tags:

flutter

I'd like to achieve the path animation effect as seen over here :

This animation (I couldn't include it because the gif is too big)

I only want to achieve the path on the map animation, I know I need to use a stacked, place my map, then use a Painter to paint such path, but how can I animate it ?

like image 370
Souames Avatar asked Jun 21 '18 23:06

Souames


People also ask

How do you animate a path in Flutter?

To animate a CustomPainter pass the AnimationController into its constructor and also to the super constructor. In paint use the value of the animation to decide how much of the path the draw. For example, if value is 0.25, draw just the first 25% of the path.

How do you draw a path in Flutter?

Drawing a line is probably the easiest thing to do with paths. First, move the current point of the path to the starting point using the moveTo function. Then draw the line using the lineTo function to the endpoint. That's it.

How do you slide transition in Flutter?

In the Flutter SlideTransition is a widget that animates the position of a widget relative to its normal position and slides required a little bit more interaction by using tween, in this case, offset, and flutter widget. The translation is expressed as an Offset scaled to the child's size.


3 Answers

I know this question has an accepted answer, but I'd like to show an alternate solution to this problem.

First of all, creating a custom path from individual points is not optimal for the following:

  • calculating the length of each segment is not trivial
  • animating the steps evenly at small increments is difficult and resource-heavy
  • does not work with quadratic / bezier segments

Just like in the good old Android there is this path tracing method, so does a very similar PathMetrics exist in Flutter.

Building upon the accepted answer of this question, here is a much more generic way of animating any path.


So given a path and an animation percent, we need to extract a path from the start until that percent:

Path createAnimatedPath(
  Path originalPath,
  double animationPercent,
) {
  // ComputeMetrics can only be iterated once!
  final totalLength = originalPath
      .computeMetrics()
      .fold(0.0, (double prev, PathMetric metric) => prev + metric.length);

  final currentLength = totalLength * animationPercent;

  return extractPathUntilLength(originalPath, currentLength);
}

So now I only need to extract a path until a given length (not the percent). We need to combine all existing paths until a certain distance. Then add to this existing path some part of the last path segment.

Doing that is pretty straightforward.

Path extractPathUntilLength(
  Path originalPath,
  double length,
) {
  var currentLength = 0.0;

  final path = new Path();

  var metricsIterator = originalPath.computeMetrics().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 {
      // There might be a more efficient way of extracting an entire path
      final pathSegment = metric.extractPath(0.0, metric.length);
      path.addPath(pathSegment, Offset.zero);
    }

    currentLength = nextLength;
  }

  return path;
}

The rest of the code required to an entire example:

void main() => runApp(
  new MaterialApp(
    home: new AnimatedPathDemo(),
  ),
);

class AnimatedPathPainter extends CustomPainter {
  final Animation<double> _animation;

  AnimatedPathPainter(this._animation) : super(repaint: _animation);

  Path _createAnyPath(Size size) {
    return Path()
      ..moveTo(size.height / 4, size.height / 4)
      ..lineTo(size.height, size.width / 2)
      ..lineTo(size.height / 2, size.width)
      ..quadraticBezierTo(size.height / 2, 100, size.width, size.height);
  }

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

    print("Painting + ${animationPercent} - ${size}");

    final path = createAnimatedPath(_createAnyPath(size), animationPercent);

    final Paint paint = Paint();
    paint.color = Colors.amberAccent;
    paint.style = PaintingStyle.stroke;
    paint.strokeWidth = 10.0;

    canvas.drawPath(path, paint);
  }

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

class AnimatedPathDemo extends StatefulWidget {
  @override
  _AnimatedPathDemoState createState() => _AnimatedPathDemoState();
}

class _AnimatedPathDemoState extends State<AnimatedPathDemo>
    with SingleTickerProviderStateMixin {
  AnimationController _controller;

  void _startAnimation() {
    _controller.stop();
    _controller.reset();
    _controller.repeat(
      period: Duration(seconds: 5),
    );
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(title: const Text('Animated Paint')),
      body: SizedBox(
        height: 300,
        width: 300,
        child: new CustomPaint(
          painter: new AnimatedPathPainter(_controller),
        ),
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: _startAnimation,
        child: new Icon(Icons.play_arrow),
      ),
    );
  }

  @override
  void initState() {
    super.initState();
    _controller = new AnimationController(
      vsync: this,
    );
  }

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

like image 198
andras Avatar answered Oct 10 '22 07:10

andras


I created a library for this: drawing_animation

You just have to provide the Path objects to the widget:

Resulting in this image output: imgur

import 'package:drawing_animation/drawing_animation.dart';
//...
List<Paths> dottedPathArray = ...;
bool run = true;
//...

AnimatedDrawing.paths(
    this.dottedPathArray,
    run: this.run,
    animationOrder: PathOrders.original,
    duration: new Duration(seconds: 2),
    lineAnimation: LineAnimation.oneByOne,
    animationCurve: Curves.linear,
    onFinish: () => setState(() {
      this.run = false;
    }),
)),


like image 42
biocarl Avatar answered Oct 10 '22 07:10

biocarl


You don't actually need a Stack; you could use a foregroundPainter over the map image. To animate a CustomPainter pass the AnimationController into its constructor and also to the super constructor. In paint use the value of the animation to decide how much of the path the draw. For example, if value is 0.25, draw just the first 25% of the path.

class AnimatedPainter extends CustomPainter {
  final Animation<double> _animation;

  AnimatedPainter(this._animation) : super(repaint: _animation);

  @override
  void paint(Canvas canvas, Size size) {
    // _animation.value has a value between 0.0 and 1.0
    // use this to draw the first X% of the path
  }

  @override
  bool shouldRepaint(AnimatedPainter oldDelegate) {
    return true;
  }
}

class PainterDemo extends StatefulWidget {
  @override
  PainterDemoState createState() => new PainterDemoState();
}

class PainterDemoState extends State<PainterDemo>
    with SingleTickerProviderStateMixin {
  AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = new AnimationController(
      vsync: this,
    );
  }

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

  void _startAnimation() {
    _controller.stop();
    _controller.reset();
    _controller.repeat(
      period: Duration(seconds: 5),
    );
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(title: const Text('Animated Paint')),
      body: new CustomPaint(
        foregroundPainter: new AnimatedPainter(_controller),
        child: new SizedBox(
          // doesn't have to be a SizedBox - could be the Map image
          width: 200.0,
          height: 200.0,
        ),
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: _startAnimation,
        child: new Icon(Icons.play_arrow),
      ),
    );
  }
}

void main() {
  runApp(
    new MaterialApp(
      home: new PainterDemo(),
    ),
  );
}

Presumably you will have a list of coordinates that define the path. Assuming some list of points you'd draw the complete path with something like:

if (points.isEmpty) return;
Path path = Path();
Offset origin = points[0];
path.moveTo(origin.dx, origin.dy);
for (Offset o in points) {
  path.lineTo(o.dx, o.dy);
}
canvas.drawPath(
  path,
  Paint()
    ..color = Colors.orange
    ..style = PaintingStyle.stroke
    ..strokeWidth = 4.0,
);

When value is less than 1.0 you need to devise a way to draw less than 100% of the path. For example, when value is 0.25, you might only add the first quarter of the points to the path. If your path consisted of relatively few points, you'd probably get the smoothest animation if you calculated the total length of the path and drew just the first segments of the path that added up to a quarter of the total length.

like image 8
Richard Heap Avatar answered Oct 10 '22 07:10

Richard Heap