What I have is a play button that plays back user recorded message. Once the user hits the play button it changes into a stop button that displays a circular progress indicator that progresses based on a percentage of the recorded message total time and the current track time.
What I have somewhat worked, but isn't exact enough, depending on how long the track is (4 seconds vs 6.5 seconds) the track will run a bit longer after the circular progress indicator is done or the track will end before the progress indicator is done.
I would also love the progression to be smooth and not jump in intervals.
Here is the code for the stop button which takes a double totalTime which is the total time of the track playing and then starts a timer and AnimationController.
Also full transparency, the custom painter is something I found online and don't fully understand how it works, so even if there isn't an issue there if someone could break that down it would be seriously appreciated :D
import 'dart:async';
import 'dart:math';
import 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_icons/flutter_icons.dart';
class StopButton extends StatefulWidget {
#override
_StopButtonState createState() => _StopButtonState();
final double totalTime;
final dynamic onClickFunction;
StopButton(this.totalTime, this.onClickFunction);
}
class _StopButtonState extends State<StopButton> with TickerProviderStateMixin {
double percentage = 0.0;
double newPercentage = 0.0;
AnimationController percentageAnimationController;
Timer timer;
#override
void initState() {
// TODO: implement initState
super.initState();
setState(() {
percentage = 0.0;
});
percentageAnimationController =
AnimationController(vsync: this, duration: Duration(milliseconds: 1000))
..addListener(() {
setState(() {
percentage = lerpDouble(percentage, newPercentage,
percentageAnimationController.value);
});
});
startTime();
}
#override
void dispose() {
// TODO: implement dispose
timer.cancel();
percentageAnimationController.dispose();
super.dispose();
}
void startTime() {
setState(() {
percentage = newPercentage;
newPercentage += 0.0;
if (newPercentage > widget.totalTime) {
percentage = 0.0;
newPercentage = 0.0;
timer.cancel();
}
percentageAnimationController.forward(from: 0.0);
});
timer = Timer.periodic(Duration(seconds: 1), (timer) {
print(timer.tick);
setState(() {
percentage = newPercentage;
newPercentage += 1.0;
if (newPercentage > widget.totalTime) {
percentage = 0.0;
newPercentage = 0.0;
timer.cancel();
}
percentageAnimationController.forward(from: 0.0);
});
});
}
#override
Widget build(BuildContext context) {
return CustomPaint(
foregroundPainter: MyPainter(
lineColor: Colors.transparent,
completeColor: Color(0xFF133343),
completePercent: percentage,
width: 3.0,
totalTime: widget.totalTime,
),
child: Padding(
padding: EdgeInsets.all(0.0),
child: FloatingActionButton(
onPressed: () async {
await widget.onClickFunction();
},
backgroundColor: Colors.white,
child: Icon(
MaterialCommunityIcons.stop,
color: Color(0xFF133343),
),
),
),
);
}
}
class MyPainter extends CustomPainter {
Color lineColor;
Color completeColor;
double completePercent;
double width;
double totalTime;
MyPainter(
{this.lineColor,
this.completeColor,
this.completePercent,
this.width,
this.totalTime});
#override
void paint(Canvas canvas, Size size) {
Paint line = Paint()
..color = lineColor
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke
..strokeWidth = width;
Paint complete = Paint()
..color = completeColor
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke
..strokeWidth = width;
Offset center = Offset(size.width / 2, size.height / 2);
double radius = min(size.width / 2, size.height / 2);
canvas.drawCircle(center, radius, line);
double arcAngle = 2 * pi * (completePercent / totalTime);
canvas.drawArc(Rect.fromCircle(center: center, radius: radius), -pi / 2,
arcAngle, false, complete);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
you have add below custom CustomTimerPainter for create circular indicator
class CustomTimerPainter extends CustomPainter {
CustomTimerPainter({
this.animation,
this.backgroundColor,
this.color,
}) : super(repaint: animation);
final Animation<double> animation;
final Color backgroundColor, color;
#override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = backgroundColor
..strokeWidth = 6.0
..strokeCap = StrokeCap.butt
..style = PaintingStyle.stroke;
canvas.drawCircle(size.center(Offset.zero), size.width / 2.0, paint);
paint.color = color;
double progress = (1.0 - animation.value) * 2 * math.pi;
canvas.drawArc(Offset.zero & size, math.pi * 1.5, progress, false, paint);
}
#override
bool shouldRepaint(CustomTimerPainter old) {
return animation.value != old.animation.value ||
color != old.color ||
backgroundColor != old.backgroundColor;
}
}
after adding indicator define controller
AnimationController controller;
#override
void initState() {
super.initState();
controller = AnimationController(
vsync: this,
duration: Duration(seconds: 5),
);
}
last step is add our custom painter
floatingActionButton: Container(
height: 60,
width: 60,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white
),
child: GestureDetector(
child: CustomPaint(
painter: CustomTimerPainter(
animation: controller,
backgroundColor: Colors.white,
color: themeData.indicatorColor,
)),
onTap: (){
controller.reverse(
from: controller.value == 0.0
? 1.0
: controller.value);
},
),
),
Related
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
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;
}
}
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.stack.imgur.com/ayoHn.gif
The point where I tap creates a line from the center to that point. But currently I can't get the coordinates of the tap that match the canvas coordinates. Is there any way to change the scaffold origin (0,0) and set its origin to the same as the container origin?
If bastman hits run then I want to show the direction where he/she runs with a black line.
import 'dart:ffi';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'dart:ui' as ui;
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final GlobalKey _cardkey = GlobalKey();
double posx = 100.0;
double posy = 100.0;
Size? cardSize;
Offset? cardPosition;
var height = AppBar().preferredSize.height;
ui.Image? image;
void onTapDown(BuildContext context, TapDownDetails details) {
// print('${details.globalPosition}');
final RenderBox box = context.findRenderObject() as RenderBox;
final Offset localOffset = box.globalToLocal(details.globalPosition);
setState(() {
posx = localOffset.dx;
posy = localOffset.dy;
});
print(posx);
print(posy);
}
#override
void initState() {
// TODO: implement initState
super.initState();
// WidgetsBinding.instance!.addPostFrameCallback((_) {getSizeAndPosition(); });
loadImage('assets/images/wheel.png');
}
Future loadImage(String path) async {
final data = await rootBundle.load(path);
final bytes = data.buffer.asUint8List();
final image = await decodeImageFromList(bytes);
setState(() {
this.image = image;
});
}
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
// appBar: AppBar(
// title: Text("Canvas"),
// ),
body: Center(
child: Container(
decoration: BoxDecoration(
border: Border.all(),
shape: BoxShape.circle,
color: Colors.green),
height: double.infinity,
width: double.infinity,
child: GestureDetector(
key: _cardkey,
// onTapDown: (TapDownDetails details){
// onTapDown(context, details);
// },
onTap: (){},
onPanStart: (details) {
Offset position = details.localPosition;
setState(() {
posx = position.dx;
posy = position.dy;
});
print(posx);
print(posy);
},
onPanUpdate: (DragUpdateDetails details) {
Offset position = details.localPosition;
setState(() {
posx = position.dx;
posy = position.dy;
});
},
child: FittedBox(
child: SizedBox(
width: image!.width.toDouble(),
height: image!.height.toDouble(),
child: CustomPaint(
// key: _cardkey,
painter: ImagePainter(posx, posy, image),
),
),
),
),
),
),
),
);
}
}
class ImagePainter extends CustomPainter {
double? posX;
double? posY;
ui.Image? images;
ImagePainter(double posx, double posy, this.images) {
posX = posx;
posY = posy;
}
#override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final CenterX = size.width / 2;
final CenterY = size.height / 2;
final paintCircle = Paint()
..strokeWidth = 5
..color = Colors.green
..style = PaintingStyle.stroke;
// canvas.translate(size.width/2, -size.height/2);
// canvas.scale(1,-1);
final paintImage = Paint()
..strokeWidth = 5
..color = Colors.white
..style = PaintingStyle.stroke;
// canvas.translate(size.width/2, -size.height/2);
// canvas.scale(1,-1);
final paintLine = Paint()
..strokeWidth = 5
..color = Colors.black
..style = PaintingStyle.stroke;
canvas.drawCircle(center, size.width / 2, paintCircle);
var circleCenter = Offset(size.width / 2, size.height / 2);
var radius = size.width / 8;
canvas.drawImage(images!, Offset(0, 0), paintImage);
var line = canvas.drawLine(center, Offset(posX!, posY!), paintLine);
// print("$line");
}
#override
bool shouldRepaint(CustomPainter olddelegate) => true;
}
Dont use a GestureDetector. It won't give you the position of a single tap. If you want the position of a onTap you need to use a Listener:
Listener(
onPointerDown: (event) {
setState(() {
posx = event.localPosition.dx;
posy = event.localPosition.dy;
}
}
),
The localPosition property should match the coordinates of the container IF you don't resize the child of the container. If they are not correct for some reason use event.position instead which will give you coordinates in the global coordinate space.
In my flutter application I want to animate the color change (the color is a gradient) of two elements that inherit from a custom painter, this happens when a button is clicked.
My problem is that I can't find how to do it, since I understand that to animate an element a Tween is needed.
I cannot find a Tween that is correct for my case, since the color change is done in the CustomPainter conditionally.
The TweenColor class works, however I can't integrate it with a gradient and this doesn't work for me.
This effect is the one I want:
https://vimeo.com/578534813/e0f2d4ed73
This is what I have without using the Tween Color, it is fine, however, it is not the animated effect that I want.
https://vimeo.com/578538292/6e63a51060
As you can see, it is close to what is required, but it is not.
Thanks for your help, I leave the code.
ScreenClass
class AuthScreen extends StatefulWidget {
#override
_AuthScreenState createState() => _AuthScreenState();
}
class _AuthScreenState extends State<AuthScreen> with SingleTickerProviderStateMixin
{
late AnimationController controller;
late Animation<double> opacity;
#override
void initState() {
controller = new AnimationController(vsync: this,duration: Duration(seconds:2 ));
opacity = Tween(begin: 0.1 ,end: 1.0).animate(controller);
super.initState();
}
#override
void dispose() {
controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
controller.forward();
final authProvider = Provider.of<AuthProvider>(context,listen: false);
return Scaffold(
body: Stack(
children: [
Container(
width: double.infinity,
height: double.infinity,
child: AnimatedBuilder(
animation: controller,
builder: (_,Widget? child){
return Opacity(
opacity: opacity.value,
child: CustomPaint(
painter: BackgroundPainter(provider:authProvider),
),
);
},
),
),
Center(
child: MaterialButton(
onPressed: (){
controller.reset();
controller.forward();
authProvider.flag = !authProvider.flag;
},
shape: RoundedRectangleBorder(),
elevation: 12,
color: Colors.white,
child: Text('Ingresar'),
),
),
])
);
}
}
ProviderClass
class AuthProvider extends ChangeNotifier {
bool _flag = false;
bool get flag {
print(_flag);
return this._flag;
}
set flag(bool state) => this._flag = state;
}
CustomPaintClass
class BackgroundPainter extends CustomPainter {
AuthProvider provider;
BackgroundPainter({required this.provider});
final Gradient redColor = new LinearGradient(colors: <Color>[
Colors.white12,
Color(0xffF7A2A3),
]);
final Gradient blueColor = new LinearGradient(colors: <Color>[
Colors.white12,
Color(0xff74BDD7),
]);
#override
void paint(Canvas canvas, Size size) {
final Rect rect = Rect.fromCircle(
center: Offset(55.0, 155.0),
radius: 180);
/**
* *Header*
*/
Gradient color ;
if(!provider.flag)
color = redColor;
else
color = blueColor;
final paint = new Paint()..shader = color.createShader(rect);
paint.style = PaintingStyle.fill; // .fill .stroke
paint.strokeWidth = 20;
final path = new Path();
path.moveTo(size.width, 0);
path.lineTo(size.width, size.height * 0.20);
path.quadraticBezierTo(
size.width * 0.95, size.height * 0.05, 0, size.height * 0.06);
path.lineTo(0, 0);
canvas.drawPath(path, paint);
final Rect rect2 = Rect.fromCircle(
center: Offset(55.0, 155.0), // x = horizontal y = vertical
radius: 180);
/**
* *Footer*
*/
Gradient color2 ;
if(!provider.flag)
color2 = blueColor;
else
color2 = redColor;
final paint2 = new Paint()..shader = color2.createShader(rect2);
paint2.style = PaintingStyle.fill;
paint2.strokeWidth = 30;
final path2 = new Path();
path2.moveTo(0, size.height);
path2.lineTo(0, size.height * 0.80);
path2.quadraticBezierTo(
size.width * 0.05, size.height * 0.95, size.width, size.height * 0.94);
path2.lineTo(size.width, size.height);
canvas.drawPath(path2, paint2);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
I got a problem with this code and the speed on the animation controller isnt working. When it have to pass from the previous value to the next, it makes the speeds too fast, how can I solve it?
Its a radial counter that when you add a task it fills calculating the percentage of tasks completed on all the day.
class RadialProgress extends StatefulWidget {
final porcentaje;
final Color colorprimario;
final Color colorsecundario;
final double grosorsecundario;
final double grosorprimario;
RadialProgress({
#required this.porcentaje,
this.colorprimario = Colors.blue,
this.colorsecundario = Colors.grey,
this.grosorsecundario = 4,
this.grosorprimario = 10
});
#override
_RadialProgressState createState() => _RadialProgressState();
}
double porcentaje;
class _RadialProgressState extends State<RadialProgress> with SingleTickerProviderStateMixin{
AnimationController controller;
double porcentajeAnterior;
#override
void initState() {
porcentajeAnterior = widget.porcentaje;
controller = new AnimationController(vsync: this, duration: Duration( milliseconds: 1000 ));
super.initState();
}
#override
void dispose() {
controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
controller.forward( from: 0.0 );
final diferenciaAnimar = widget.porcentaje - porcentajeAnterior;
porcentajeAnterior = widget.porcentaje;
return AnimatedBuilder(
animation: controller,
builder: (BuildContext context, Widget child) {
return StreamBuilder<List<Todo>>(
stream: DatabaseService().trueItems(),
builder: (context, snapshot) {
// List<Todo> trueitems = snapshot.data;
if (!snapshot.hasData) return SizedBox();
return Container(
padding: EdgeInsets.all(10),
width: double.infinity,
height: double.infinity,
child: StreamBuilder<List<Todo>>(
stream: DatabaseService().listTodos(),
builder: (context, snapshot) {
// List<Todo> listtodo = snapshot.data;
// final porcentaje = ((trueitems.length/listtodo.length)).toDouble();
return CustomPaint(
painter: _MiRadialProgress(
( ((widget.porcentaje*100) - (diferenciaAnimar))) + (((diferenciaAnimar) * controller.value)),
widget.colorprimario,
widget.colorsecundario,
widget.grosorsecundario,
widget.grosorprimario)
);
}
),
// child: Text('${widget.porcentaje}'),
);
}
);
}
);
}
}
I put the _miradialprogress code if it can helps too. Thanks!
class _MiRadialProgress extends CustomPainter{
final porcentaje;
final colorprimario;
final colorsecundario;
final double grosorsecundario;
final double grosorprimario;
_MiRadialProgress(
this.porcentaje,
this.colorprimario,
this.colorsecundario,
this.grosorsecundario,
this.grosorprimario
);
#override
void paint(Canvas canvas, Size size) {
// Completed circle
final paint = new Paint()
..strokeWidth = grosorsecundario
..color = colorsecundario
..style = PaintingStyle.stroke;
final center = new Offset(size.width * 0.5, size.height * 0.5);
final radio = min(size.width * 0.5, size.height * 0.5);
canvas.drawCircle(center, radio, paint);
//arc
final paintArco = new Paint()
..strokeWidth = grosorprimario
..color = colorprimario
..style = PaintingStyle.stroke;
//part that have to be filled
double arcAngle = 2* pi * (porcentaje / 100);
canvas.drawArc(
Rect.fromCircle(center: center, radius: radio),
-pi / 2 ,
arcAngle,
false,
paintArco);
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate)=> true;
}
You should not put controller.forward( from: 0.0 ); inside the build method, since you will start the animation on every re-render (build method execution). It could be that your animation starts all over again while rendering, so it seems that it's not working. Just put the animation start code inside the initState method:
#override
void initState() {
super.initState();
porcentajeAnterior = widget.porcentaje;
controller = new AnimationController(vsync: this, duration: Duration( milliseconds: 1000 ));
controller.forward();
}
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?
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;
}
Hopefully someone will find this helpfull :) Feel free to improve on this! Happy coding !
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.