Create spiral in flutter

311 Views Asked by At

I want to create a spiral in flutter which will have a dynamic fill, something like shown below. One way to create is by using multiple semi-circles, but is there a better way to achieve this?

The dynamic fill is orange in color and will depend on a percentage value.

enter image description here

2

There are 2 best solutions below

0
On BEST ANSWER

I was able to make it from scratch using a bit of gematrical calculations. My code below:

class SpiralPainter extends CustomPainter {
  late BuildContext context;
  int fillPercent; // to show orange fill showing the target progress

  SpiralPainter(this.context, this.fillPercent);

  @override
  void paint(Canvas canvas, Size size) {
    const double radius = 180.0; //radius of outermost spiral arc
    const double strokeWidth = 10; // width of the spiral stroke
    const double radiiDecrement = 30.0; // gap in radius between the spirals
    const double centerShift = 1 +
        (radiiDecrement + strokeWidth) /
            2; // shift in center point between immediate arcs used to create the spiral
    Offset center = Offset(
        0.5 * size.width, 0.5 * size.height); // center of the outermost spiral

    // paint colors and strokes for spiral
    final Paint paintSpiralTargetMet = Paint()
      ..color = Theme.of(context).colorScheme.primary
      ..strokeWidth = strokeWidth
      ..style = PaintingStyle.stroke;
    final Paint paintSpiralTargetNotMet = Paint()
      ..color = Colors.black45
      ..strokeWidth = strokeWidth
      ..style = PaintingStyle.stroke;

    double spiralRadius = radius;
    double userProgress =
        44; //TODO: fetch it from firebase when available, use fillPercent class variable
    double arcFillPercent = (userProgress / 25);
    //this variable is used to determine which arcs will be fully filled or fully unfilled
    int arcFillCount = arcFillPercent.ceil();
    //this variable is used to partially fill an arc with orange color indicating the running progress
    arcFillPercent =
        arcFillPercent - arcFillPercent.floor(); // get fraction part

    // Spiral is drawn using the 4 arcs of reducing radius and shifting center
    // so this loop runs 4 times
    for (var i = 1; i < 5; i++) {
      double sweepAngle = pi;
      double startAngle = pi / 4 + (pi * ((i - 1) % 2));
      if (i > arcFillCount) {
        //whole arc will be unfilled
        canvas.drawArc(Rect.fromCircle(center: center, radius: spiralRadius),
            startAngle, sweepAngle, false, paintSpiralTargetNotMet);
      } else if (i < arcFillCount) {
        //whole arc will be filled
        canvas.drawArc(Rect.fromCircle(center: center, radius: spiralRadius),
            startAngle, sweepAngle, false, paintSpiralTargetMet);
      } else {
        if (arcFillPercent == 0) {
          //whole arc will be filled
          canvas.drawArc(Rect.fromCircle(center: center, radius: spiralRadius),
              startAngle, sweepAngle, false, paintSpiralTargetMet);
        } else {
          //part of arc will be filled and rest unfilled
          sweepAngle = pi * (arcFillPercent);
          startAngle += pi * (1 - arcFillPercent);
          startAngle =
              startAngle > (2 * pi) ? (startAngle - (2 * pi)) : startAngle;
          canvas.drawArc(Rect.fromCircle(center: center, radius: spiralRadius),
              startAngle, sweepAngle, false, paintSpiralTargetMet);

          startAngle = pi / 4 + (pi * ((i - 1) % 2));
          startAngle =
              startAngle > (2 * pi) ? (startAngle - (2 * pi)) : startAngle;
          sweepAngle = pi * (1 - arcFillPercent);
          canvas.drawArc(Rect.fromCircle(center: center, radius: spiralRadius),
              startAngle, sweepAngle, false, paintSpiralTargetNotMet);
        }
      }
      //reduce the radius and shift the center
      spiralRadius -= radiiDecrement;
      if (i % 2 == 0) {
        center -= const Offset(centerShift, centerShift);
      } else {
        center += const Offset(centerShift, centerShift);
      }
    }
    const double smallGoalRadius = 25; // radius of small intermediate targets
    //center of circle from top left corner

    // paint colors and strokes for circle and spiral
    final Paint paintCircleTargetMet =
        Paint() // for the circles where target is met
          ..color = Theme.of(context).colorScheme.primary;
    final Paint paintTargetNotMet =
        Paint() // for the circles where target is not met
          ..color = Colors.black;
    // do not modify the code below
    // unless you completely understand the math (geometry) behind it
    // Array containing checkpoints based on team running progress
    List<double> smallGoalCheckPoints = [0, 12.49, 24.99, 37.49, 56.34, 81.24];
    Offset smallGoalCenter =
        center - Offset(radius / sqrt(2), radius / sqrt(2));
    canvas.drawCircle(
        smallGoalCenter,
        smallGoalRadius,
        userProgress > smallGoalCheckPoints[0]
            ? paintCircleTargetMet
            : paintTargetNotMet);
    smallGoalCenter = smallGoalCenter.translate(0, 2 * radius / sqrt(2));
    canvas.drawCircle(
        smallGoalCenter,
        smallGoalRadius,
        userProgress > smallGoalCheckPoints[1]
            ? paintCircleTargetMet
            : paintTargetNotMet);
    smallGoalCenter = smallGoalCenter.translate(2 * radius / sqrt(2), 0);
    canvas.drawCircle(
        smallGoalCenter,
        smallGoalRadius,
        userProgress > smallGoalCheckPoints[2]
            ? paintCircleTargetMet
            : paintTargetNotMet);
    smallGoalCenter =
        smallGoalCenter.translate(0, -2 * (radius - radiiDecrement) / sqrt(2));
    canvas.drawCircle(
        smallGoalCenter,
        smallGoalRadius,
        userProgress > smallGoalCheckPoints[3]
            ? paintCircleTargetMet
            : paintTargetNotMet);
    smallGoalCenter = center -
        const Offset(
            radius - 2 * radiiDecrement - strokeWidth / 2, -1 * centerShift);
    canvas.drawCircle(
        smallGoalCenter,
        smallGoalRadius,
        userProgress > smallGoalCheckPoints[4]
            ? paintCircleTargetMet
            : paintTargetNotMet);
    smallGoalCenter = smallGoalCenter.translate(
        smallGoalRadius + 2 * radius - 5 * radiiDecrement - strokeWidth, 0);
    canvas.drawCircle(
        smallGoalCenter,
        smallGoalRadius,
        userProgress > smallGoalCheckPoints[5]
            ? paintCircleTargetMet
            : paintTargetNotMet);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return false;
  }
}
1
On

To create a spiral in Flutter, you can use the CustomPainter class to draw the spiral shape.

Here's an sample example code for spiral:

import 'dart:math' as math;
import 'package:flutter/material.dart';

class SpiralPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.blue
      ..strokeWidth = 2.0;

    final centerX = size.width / 2;
    final centerY = size.height / 2;
    final maxRadius = math.sqrt(centerX * centerX + centerY * centerY);

    for (var i = 0; i < 360 * 5; i += 5) {
      final angle = i * math.pi / 180;
      final radius = maxRadius * i / (360 * 5);

      final x = centerX + radius * math.cos(angle);
      final y = centerY + radius * math.sin(angle);

      canvas.drawCircle(Offset(x, y), 2, paint);
    }
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => false;
}

class SpiralPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Spiral'),
      ),
      body: Center(
        child: CustomPaint(
          size: Size(300, 300),
          painter: SpiralPainter(),
        ),
      ),
    );
  }
}