Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to animate a line with multiple offset points using CustomPainter in Flutter?

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;
  }
}


like image 942
Kdon Avatar asked Oct 21 '25 20:10

Kdon


1 Answers

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.sstatic.net/ayoHn.gif
like image 135
Kdon Avatar answered Oct 24 '25 09:10

Kdon



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!