Flutter Draw a circle border with 3 multiple colors and values

1,428

Solution 1

Animation can be handled by TweenAnimationBuilder and it will be played on build. To achieve desired result we must use customPainter.

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

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
      debugShowCheckedModeBanner: false,
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: TweenAnimationBuilder(
          duration: const Duration(seconds: 2),
          tween: Tween(begin: 0.0, end: 1.0),
          curve: Curves.easeOutCubic,
          builder: (BuildContext context, dynamic value, Widget child) {
            return CustomPaint(
              painter: OpenPainter(
                  totalQuestions: 300,
                  learned: 75,
                  notLearned: 75,
                  range: value),
            );
          },
        ),
      ),
    );
  }
}

class OpenPainter extends CustomPainter {
  final learned;
  final notLearned;
  final range;
  final totalQuestions;
  double pi = math.pi;

  OpenPainter({this.learned, this.totalQuestions, this.notLearned, this.range});
  @override
  void paint(Canvas canvas, Size size) {
    double strokeWidth = 7;
    Rect myRect = const Offset(-50.0, -50.0) & const Size(100.0, 100.0);
    
    var paint1 = Paint()
      ..color = Colors.red
      ..strokeWidth = strokeWidth
      ..style = PaintingStyle.stroke;
    var paint2 = Paint()
      ..color = Colors.green
      ..strokeWidth = strokeWidth
      ..style = PaintingStyle.stroke;
    var paint3 = Paint()
      ..color = Colors.yellow
      ..strokeWidth = strokeWidth
      ..style = PaintingStyle.stroke;

    double firstLineRadianStart = 0;
    double _unAnswered = (totalQuestions - notLearned - learned) * range / totalQuestions;
    double firstLineRadianEnd = (360 * _unAnswered) * math.pi / 180;
    canvas.drawArc(
        myRect, firstLineRadianStart, firstLineRadianEnd, false, paint1);

    double _learned = (learned) * range / totalQuestions;
    double secondLineRadianEnd = getRadians(_learned);
    canvas.drawArc(myRect, firstLineRadianEnd, secondLineRadianEnd, false, paint2);
    double _notLearned = (notLearned) * range / totalQuestions;
    double thirdLineRadianEnd = getRadians(_notLearned);
    canvas.drawArc(myRect, firstLineRadianEnd + secondLineRadianEnd, thirdLineRadianEnd, false, paint3);

    // drawArc(Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint)
  }

  double getRadians(double value) {
    return (360 * value) * pi / 180;
  }

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

enter image description here Hopefully someone will find this helpfull :) Feel free to improve on this! Happy coding !

Solution 2

Thanks to Paulius Greičiūnas answer, I implemented a more general way to paint a circle in different colors. You only have to specify the occurrences of the colors as a map and a size of the circle.

class MultipleColorCircle extends StatelessWidget {
  final Map<Color, int> colorOccurrences;
  final double height;
  final Widget? child;
  @override
  MultipleColorCircle(
      {required this.colorOccurrences, this.height = 20, this.child});
  Widget build(BuildContext context) => Container(
        height: height,
        width: height,
        child: CustomPaint(
            size: Size(20, 20),
            child: Center(child: child),
            painter: _MultipleColorCirclePainter(
              colorOccurrences: colorOccurrences,
              height: height,
            )),
      );
}

class _MultipleColorCirclePainter extends CustomPainter {
  final Map<Color, int> colorOccurrences;
  final double height;
  @override
  _MultipleColorCirclePainter(
      {required this.colorOccurrences, required this.height});
  double pi = math.pi;

  @override
  void paint(Canvas canvas, Size size) {
    double strokeWidth = 1;
    Rect myRect =
        Rect.fromCircle(center: Offset(height / 2, height / 2), radius: height);

    double radianStart = 0;
    double radianLength = 0;
    int allOccurrences = 0;
    //set denominator
    colorOccurrences.forEach((color, occurrence) {
      allOccurrences += occurrence;
    });
    colorOccurrences.forEach((color, occurrence) {
      double percent = occurrence / allOccurrences;
      radianLength = 2 * percent * math.pi;
      canvas.drawArc(
          myRect,
          radianStart,
          radianLength,
          false,
          Paint()
            ..color = color
            ..strokeWidth = strokeWidth
            ..style = PaintingStyle.stroke);
      radianStart += radianLength;
    });
  }

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

With a map e.g. {Colors.blue: 2, Colors.green: 1} you will get a circle with 1/3 green and 2/3 blue. Note, that you can also define a child, so that the circle has content in it. Here is an example, which I used in my calendar, of multiple circles with content in it. enter image description here

Share:
1,428
Paul Grei
Author by

Paul Grei

I am working with old/new things in web dev. Hope to help and/or learn about everything internet has to offer.

Updated on December 29, 2022

Comments

  • Paul Grei
    Paul Grei over 1 year

    How do you draw a diagram style circle border with multiple values? Also animated that each value in circle expands dynamically filling 100% of the circle?

  • Paul Grei
    Paul Grei almost 3 years
    Thank you for sharing! :) You should add animations to each one :D
  • Paul Grei
    Paul Grei almost 3 years
    You should create another thread with detailed question. If i understand correctly you could pass in offset arguments through constructor. Not sure about the "dynamic" part. Or use MediaQuery.of(context).size.width to scale with screen width .
  • Arslan Kaleem
    Arslan Kaleem almost 3 years
    Can you draw Assetimage in the middle of this circle ?
  • FrontMobe
    FrontMobe almost 3 years
    Sure, the parameter child is of type Widget?. So you can pass your AssetImage('my image.png') in it. You might set the size of the image to be sure it fits in.
  • Paul Grei
    Paul Grei about 2 years
    @ArslanKaleem use some kind of animationController and act on it e.g animationController.forward() on button click or something like that :) or at worst just create this animation as separated statefull widget and use setstate to rebuild whole widget