1

I want to create a line that animates to multiple offset points until the full line is painted out, using CustomPainter in Flutter.

I have almost achieved this effect, by using an animation object to tween to each new point, and an index to track the line progress.

Then, in CustomPainter, I paint 2 lines. One to line animates to the new position, and a second which draws the existing path based off the index.

However, there is a small UI error as the GIF shows, where the corners 'fill out' after a new point is added.

Note, a I tried using a TweenSequence borrowing the idea mentioned in this recent video but couldn't get it to work. FlutterForward, Youtube Video - around 14:40

Example of current line

import 'package:flutter/material.dart';

class LinePainterAnimation extends StatefulWidget {
  const LinePainterAnimation({Key? key}) : super(key: key);

  @override
  State<LinePainterAnimation> createState() => _LinePainterAnimationState();
}

class _LinePainterAnimationState extends State<LinePainterAnimation>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;

  final List<Offset> _offsets = [
    const Offset(50, 300),
    const Offset(150, 100),
    const Offset(300, 300),
    const Offset(200, 300),
  ];

  int _index = 0;
  Offset _begin = const Offset(0, 0);
  Offset _end = const Offset(0, 0);

  @override
  void initState() {
    _begin = _offsets[0];
    _end = _offsets[1];

    _controller = AnimationController(
        duration: const Duration(seconds: 1), vsync: this)
      ..addStatusListener((status) {
        if (status == AnimationStatus.completed) {
          _index++;
          if (_index < _offsets.length - 1) {
            _begin = _offsets[_index];
            _end = _offsets[_index + 1];
            _controller.reset();
            _controller.forward();
            setState(() {});
          }
        }
      });

    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    Animation<Offset> animation =
        Tween<Offset>(begin: _begin, end: _end).animate(_controller);

    return Scaffold(
      body: AnimatedBuilder(
        animation: _controller,
        builder: (context, child) => CustomPaint(
          painter: LinePainter(
            startOffset: _begin,
            endOffset: animation.value,
            offsets: _offsets,
            index: _index,
          ),
          child: Container(),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          _controller.reset();
          _controller.forward();
          _begin = _offsets[0];
          _end = _offsets[1];
          _index = 0;
          setState(() {});
        },
        child: const Text('Play'),
      ),
    );
  }
}

class LinePainter extends CustomPainter {
  final Offset startOffset;
  final Offset endOffset;
  final List<Offset> offsets;
  final int index;

  LinePainter({
    required this.startOffset,
    required this.endOffset,
    required this.offsets,
    required this.index,
  });

  @override
  void paint(Canvas canvas, Size size) {
    var paint = Paint()
      ..color = Colors.red
      ..strokeWidth = 20
      ..strokeCap = StrokeCap.butt
      ..style = PaintingStyle.stroke;

    var pathExisting = Path();

    pathExisting.moveTo(offsets[0].dx, offsets[0].dy);
    for (int i = 0; i < index + 1; i++) {
      pathExisting.lineTo(offsets[i].dx, offsets[i].dy);
    }

    var pathNew = Path();
    pathNew.moveTo(startOffset.dx, startOffset.dy);
    pathNew.lineTo(endOffset.dx, endOffset.dy);

    canvas.drawPath(pathNew, paint);
    canvas.drawPath(pathExisting, paint);
  }

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


Kdon
  • 892
  • 6
  • 19

1 Answers1

0

OK - so I finally came up with a solution after a bit more research, which is perfect for this use case and animating complex paths - [PathMetrics][1]

So to get this working the basic steps are 1) define any path 2) calculate & extract this path using PathMetrics 3) then animate this path over any duration, based on the 0.0 to 1.0 value produced by the animation controller, and voilĂ  it works like magic!

Note, the references I found to get this working: [Moving along a curved path in flutter][2] & [Medium article][3]

Updated code pasted below if this is helpful to anyone.

[![Solution][4]]

import 'dart:ui';
import 'package:flutter/material.dart';

class LinePainterAnimation extends StatefulWidget {
  const LinePainterAnimation({Key? key}) : super(key: key);

  @override
  State<LinePainterAnimation> createState() => _LinePainterAnimationState();
}

class _LinePainterAnimationState extends State<LinePainterAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller =
        AnimationController(duration: const Duration(seconds: 1), vsync: this);

    _controller.forward();
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: AnimatedBuilder(
        animation: _controller,
        builder: (context, child) => CustomPaint(
          painter: LinePainter(_controller.value),
          child: Container(),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          _controller.reset();
          _controller.forward();
        },
        child: const Text('Play'),
      ),
    );
  }
}

class LinePainter extends CustomPainter {
  final double percent;

  LinePainter(this.percent);

  @override
  void paint(Canvas canvas, Size size) {
    var paint = Paint()
      ..color = Colors.red
      ..strokeWidth = 20
      ..strokeCap = StrokeCap.butt
      ..style = PaintingStyle.stroke;

    var path = getPath();
    PathMetrics pathMetrics = path.computeMetrics();
    PathMetric pathMetric = pathMetrics.elementAt(0);
    Path extracted = pathMetric.extractPath(0.0, pathMetric.length * percent);

    canvas.drawPath(extracted, paint);
  }

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

Path getPath() {
  return Path()
    ..lineTo(50, 300)
    ..lineTo(150, 100)
    ..lineTo(300, 300)
    ..lineTo(200, 300);
}


  [1]: https://api.flutter.dev/flutter/dart-ui/PathMetrics-class.html
  [2]: https://stackoverflow.com/questions/60203515/moving-along-a-curved-path-in-flutter
  [3]: https://medium.com/flutter-community/playing-with-paths-in-flutter-97198ba046c8
  [4]: https://i.stack.imgur.com/ayoHn.gif
Kdon
  • 892
  • 6
  • 19