How to animate a path in flutter?
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();
}
}
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;
}),
)),