skip to Main Content

How can I create an animation like this in Flutter? I attempted to use AnimatedPositioned,but I encountered difficulty animating more than one circular container (represented by the electrons in the GIF).i want to use AnimatedPositioned to animate a circular container over an SVG image. Specifically, I aim to create an animation of an electrical circuit. but i can’t make electrons animation like GIF electrons animation

2

Answers


  1. After Edit:

    Credits to pskink for this solution. It works for any square/rectangle sizes.

    See DartPad Demo

    class MyHomePage extends StatefulWidget {
      @override
      State<MyHomePage> createState() => _MyHomePageState();
    }
    
    class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
      late AnimationController _animationController;
      late FooPainter _fooPainter;
    
      @override
      void initState() {
        super.initState();
        _animationController = AnimationController(
          vsync: this,
          duration: const Duration(milliseconds: 500), //Animation speed
        )..repeat();
        _fooPainter = FooPainter(_animationController);
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: const Text('Animated Dots'),
          ),
          body: Center(
              child: CustomPaint(
              painter: _fooPainter,
              size: const Size(400, 300),
                ),  //Animation square/rectangle shape
          ),
        );
      }
    
      @override
      void dispose() {
        _animationController.dispose();
        super.dispose();
      }
    }
    
    class FooPainter extends CustomPainter {
      FooPainter(this.animation) : super(repaint: animation);
    
      final Animation animation;
      PathMetric? metric;
      final p = Paint()
        ..color = Colors.blue;
    
      @override
      void paint(Canvas canvas, Size size) {
        // Draw the background shape
        final backgroundPaint = Paint()
          ..color = Colors.transparent; // Set your desired background color
        canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), backgroundPaint);
    
        const double borderWidth = 22.0; // Width of the border
    
      // Draw borders along the edges
      final borderPaint = Paint()
        ..color = Colors.grey
        ..style = PaintingStyle.stroke
        ..strokeWidth = borderWidth;
    
      final borderRect = Rect.fromLTWH(
          borderWidth / 2, borderWidth / 2,
          size.width - borderWidth, size.height - borderWidth);
      canvas.drawRect(borderRect, borderPaint);
    
      if (metric == null) {
        final r = borderRect.deflate(0); // Adjust padding here
        final path = Path()..addRRect(RRect.fromRectAndRadius(r, const Radius.circular(0)));
        metric = path.computeMetrics().first;
      }
      final m = metric!;
      final numDots = (m.length / 20.0).floor();
      final distanceBetweenDots = m.length / numDots;
      for (int i = 0; i < numDots; i++) {
        final distance = distanceBetweenDots * (i + animation.value);
        canvas.drawCircle(m.getTangentForOffset(distance)!.position, 6, p);
      }
      }
    
      @override
      bool shouldRepaint(FooPainter oldDelegate) => false;
    }
    

    Before Edit:

    Solution without CustomPaint that works for square only.

    See DartPad Demo.

    class AnimatedDotMovingContainer extends StatefulWidget {
      const AnimatedDotMovingContainer({super.key});
    
      @override
      State<AnimatedDotMovingContainer> createState() =>
          _AnimatedDotMovingContainerState();
    }
    
    class _AnimatedDotMovingContainerState extends State<AnimatedDotMovingContainer>
        with TickerProviderStateMixin {
      late AnimationController _controller;
      double dotSize = 20.0; 
      double containerSize = 250.0;
      int numBalls = 10; // Number of balls
    
      @override
      void initState() {
        super.initState();
        _controller = AnimationController(
          vsync: this,
          duration: const Duration(seconds: 10), //Speed of balls
        )..addListener(() {
            setState(() {});
          });
    
        _controller.repeat();
      }
    
      double _getSquarePositionX(double value) {
        value = (value % 1); // Use % to keep value within [0, 1]
        if (value < 0.25) {
          return 1 - value * 4;
        } else if (value < 0.5) {
          return 0;
        } else if (value < 0.75) {
          return (value - 0.5) * 4;
        } else {
          return 1;
        }
      }
    
      double _getSquarePositionY(double value) {
        value = (value % 1);
        if (value < 0.25) {
          return 0;
        } else if (value < 0.5) {
          return (value - 0.25) * 4;
        } else if (value < 0.75) {
          return 1;
        } else {
          return 1 - (value - 0.75) * 4;
        }
      }
    
      @override
      void dispose() {
        _controller.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: const Text('Animated Dot Moving Container'),
          ),
          body: Center(
            child: SizedBox(
              width: containerSize,
              height: containerSize,
              child: Stack(
                children: [
                  Container(
                    decoration: BoxDecoration(
                      border: Border.all(color: Colors.grey, width: dotSize),
                      borderRadius: BorderRadius.circular(5),
                    ),
                  ),
                  for (int i = 0; i < numBalls; i++)
                    Positioned(
                      left: (containerSize - dotSize) *
                          _getSquarePositionX(
                              _controller.value - i / numBalls), // Adjust position
                      top: (containerSize - dotSize) *
                          _getSquarePositionY(
                              _controller.value - i / numBalls), // Adjust position
                      child: Container(
                        width: dotSize,
                        height: dotSize,
                        decoration: const BoxDecoration(
                          shape: BoxShape.circle,
                          color: Colors.red,
                        ),
                      ),
                    ),
                  const Align(
                    alignment: Alignment.centerRight,
                    child: SizedBox(
                      height: 30.0,
                      width: 30.0,
                      child: Placeholder(),
                    ),
                  ),
                ],
              ),
            ),
          ),
        );
      }
    }
    
    The formula with the AnimationController controls the counter-clockwise movement. If you want the dots to move to the opposite direction, simply adjust the formula:
    
    
    double _getSquarePositionX(double value) {
      value = (value % 1); // Use % to keep value within [0, 1]
      if (value < 0.25) {
        return value * 4;
      } else if (value < 0.5) {
        return 1;
      } else if (value < 0.75) {
        return 1 - (value - 0.5) * 4;
      } else {
        return 0;
      }
    }
    Login or Signup to reply.
  2. the easiest and the most efficient solution (since no setState is used at all and no rebuild is needed for every animation frame) is to use CustomPaint widget with passing an AnimationController as repaint parameter to CustomPainter (this is done in FooPainter constructor: super(repaint: ctrl)), to make the drawing logic easier AnimationController.unbounded is used and to make infinite animation animateWith() method is called

    class Foo extends StatefulWidget {
      @override
      State<Foo> createState() => _FooState();
    }
    
    class _FooState extends State<Foo> with TickerProviderStateMixin {
      late final ctrl = AnimationController.unbounded(vsync: this);
      bool running = false;
    
      @override
      Widget build(BuildContext context) {
        return CustomPaint(
          painter: FooPainter(ctrl),
          child: Center(
            child: OutlinedButton(
              onPressed: () {
                running = !running;
                // const endDistance = 100.0 * 60; // 60 sec animation
                const endDistance = double.infinity; // infinite animation
                final simulation = GravitySimulation(0, ctrl.value, endDistance, 100);
                running? ctrl.animateWith(simulation) : ctrl.stop();
              },
              child: const Text('start / stopnanimation', textScaleFactor: 1.5),
            ),
          ),
        );
      }
    }
    
    class FooPainter extends CustomPainter {
      FooPainter(this.ctrl) : super(repaint: ctrl);
    
      final AnimationController ctrl;
      late PathMetric metric;
      late Path path;
      Size size = Size.zero;
      final p0 = Paint();
      final p1 = Paint()
        ..style = PaintingStyle.stroke
        ..strokeWidth = 1
        ..color = Colors.black87;
    
      @override
      void paint(Canvas canvas, Size size) {
        if (this.size != size) {
          this.size = size;
          final r = Offset.zero & size;
          path = Path()..addRRect(RRect.fromRectAndRadius(r.deflate(24), const Radius.circular(32)));
          metric = path.computeMetrics().first;
        }
        final numDots = (metric.length / 25.0).floor();
        final distanceBetweenDots = metric.length / numDots;
        canvas.drawPath(path, p1);
        for (int i = 0; i < numDots; i++) {
          final distance = (distanceBetweenDots * i + ctrl.value) % metric.length;
          final color = HSVColor.fromAHSV(0.85, 360 * (i / numDots), 1, 1).toColor();
          final tangent = metric.getTangentForOffset(distance)!;
          canvas
            ..drawCircle(tangent.position, 8, p0..color = color)
            ..drawCircle(tangent.position, 8 - p1.strokeWidth / 2, p1);
        }
      }
    
      @override
      bool shouldRepaint(FooPainter oldDelegate) => false;
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search