skip to Main Content

I was searching for a way to make a spinner effect with dots opacities. Are there any way on how to make correct animation?
What I’ve tried:

  1. Add rotation – not correct animation;
  2. Add animation value on dot opacity computing;
import 'dart:math' as math;

import 'package:flutter/material.dart';

class DotCircleProgressbarPainter extends CustomPainter {
  DotCircleProgressbarPainter(
      {required this.color,
      required this.strokeWidth,
      required this.animation,
      this.numberOfVisibleCircles = 9,
      this.numberOfCircles = 10})
      : super(repaint: animation);

  final Color color;
  final double strokeWidth;
  final int numberOfCircles;
  final int numberOfVisibleCircles;
  final Animation<double> animation;

  @override
  void paint(Canvas canvas, Size size) {
    final radius = size.width / 2;

    final dotSpacing = (2 * math.pi) / numberOfCircles;

    for (int pos = 0; pos < numberOfCircles; pos++) {
      final dotOpacity =
          (1.0 - pos / (numberOfVisibleCircles - 1)).clamp(0.0, 1.0);

      final dotPaint = Paint()
        ..color = color.withOpacity(dotOpacity)
        ..strokeWidth = strokeWidth
        ..style = PaintingStyle.fill;

      final double angle = pos * dotSpacing;
      final double x = radius + radius * math.cos(angle + math.pi);
      final double y = radius + radius * math.sin(angle + math.pi);

      canvas.drawCircle(Offset(x, y), strokeWidth / 2, dotPaint);
    }
  }

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

Example image:

image

Thanks for help!

2

Answers


  1. You need to adjust the opacity of the dots based on the current animation value.

    Modified paint():

    void paint(Canvas canvas, Size size) {
     final radius = size.width / 2;
     final dotSpacing = (2 * math.pi) / numberOfCircles;
     final currentAnimationValue = animation.value;
    
     for (int pos = 0; pos < numberOfCircles; pos++) {
        final dotOpacity = (currentAnimationValue - pos / numberOfCircles).abs().clamp(0.0, 1.0);
    
        final dotPaint = Paint()
          ..color = color.withOpacity(dotOpacity)
          ..strokeWidth = strokeWidth
          ..style = PaintingStyle.fill;
    
        final double angle = pos * dotSpacing;
        final double x = radius + radius * math.cos(angle + math.pi);
        final double y = radius + radius * math.sin(angle + math.pi);
    
        canvas.drawCircle(Offset(x, y), strokeWidth / 2, dotPaint);
     }
    }
    
    Login or Signup to reply.
  2. here you have a simple StatefulWidget using a CustomPaint:

    class SpinningDots extends StatefulWidget {
      const SpinningDots({
        super.key,
        required this.simulation,
        required this.numDots,
        required this.dotRadius,
        this.dotColor = Colors.black,
      });
    
      final Simulation simulation;
      final int numDots;
      final double dotRadius;
      final Color dotColor;
    
      @override
      State<SpinningDots> createState() => _SpinningDotsState();
    }
    
    class _SpinningDotsState extends State<SpinningDots> with SingleTickerProviderStateMixin {
      late final controller = AnimationController.unbounded(vsync: this)
        ..animateWith(widget.simulation);
    
      @override
      Widget build(BuildContext context) {
        return CustomPaint(
          painter: _SpinningDotsPainter(
            animation: controller,
            numDots: widget.numDots,
            dotRadius: widget.dotRadius,
            dotColor: widget.dotColor,
          ),
          child: const SizedBox.expand(),
        );
      }
    
      @override
      void dispose() {
        controller.dispose();
        super.dispose();
      }
    }
    
    class _SpinningDotsPainter extends CustomPainter {
      _SpinningDotsPainter({
        required this.animation,
        required this.numDots,
        required this.dotRadius,
        required this.dotColor,
      }) : super(repaint: animation);
    
      final Animation<double> animation;
      final int numDots;
      final double dotRadius;
      final Color dotColor;
    
      @override
      void paint(Canvas canvas, Size size) {
        final distance = size.shortestSide / 2 - dotRadius;
        for (int i = 0; i < numDots; i++) {
          final paint = Paint()
            ..color = dotColor.withOpacity((animation.value - i / numDots) % 1);
          final center = size.center(Offset.zero) + Offset.fromDirection(2 * pi * i / numDots, distance);
          canvas.drawCircle(center, dotRadius, paint);
        }
      }
    
      @override
      bool shouldRepaint(_SpinningDotsPainter oldDelegate) => false;
    }
    

    and you can use it like this (if you want infinite simulation replace 16 which is endDistance in GravitySimulation ctor with double.infinity:

    body: Center(
      child: SizedBox.fromSize(
        size: const Size.square(64),
        child: SpinningDots(
          simulation: GravitySimulation(0, 0, 16, 1.5),
          numDots: 8,
          dotColor: Colors.deepPurple,
          dotRadius: 6,
        ),
      ),
    ),
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search