Timing Animations with Custom Painter in Flutter - flutter

I created a custom widget in flutter using custom painter. Its a status bar. I wanted to animate it, but now a problem is occurring that I don't know how to fix.
The bar consists of a line and several dots, depending on the status. When animating this bar, the line is correctly animated, but the dot is appearing to early, it just pops in.
example of how my widget is currently animating (can't post inline yet)
How can I choose when the dot animates?
This is the code I use for animating my widget:
class _StatusBarState extends State<StatusBar> with TickerProviderStateMixin {
late AnimationController _animationController;
#override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: Duration(seconds: 1),
lowerBound: 1,
upperBound: widget.states.length.toDouble());
}
#override
void didUpdateWidget(covariant StatusBar oldWidget) {
super.didUpdateWidget(oldWidget);
_animationController.animateTo(widget.currentState.toDouble());
}
#override
Widget build(BuildContext context) {
return Column(
children: [
AnimatedBuilder(
animation: _animationController,
builder: (BuildContext buildContext, Widget? child) => Container(
height: 50,
width: MediaQuery.of(context).size.width * 0.75,
child: CustomPaint(
painter: _RentalStatusBarPainter(
context: context,
currentState: _animationController.value,
states: widget.states,
failed: widget.error),
),
),
),
Container(
width: MediaQuery.of(context).size.width * 0.85,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: widget.states
.map((e) => widget.states.indexOf(e) == widget.currentState - 1
? Text(
e,
style: Theme.of(context).textTheme.subtitle1,
)
: Text(
e,
style: Theme.of(context).textTheme.caption,
))
.toList(),
),
),
SizedBox(height: 10),
],
);
}
}
And this is the custom widget that is being painted:
#override
void paint(Canvas canvas, Size size) {
int currentStateInt = currentState.toInt();
final paint = Paint()
..color = Theme.of(context).cardColor
..style = PaintingStyle.stroke
..strokeWidth = 5
..strokeCap = StrokeCap.round;
final dotspaint = Paint()
..color = Theme.of(context).cardColor
..style = PaintingStyle.fill;
final filledpaint = Paint()
..color = failed
? Theme.of(context).colorScheme.error
: Theme.of(context).colorScheme.secondary
..style = PaintingStyle.stroke
..strokeWidth = 5
..strokeCap = StrokeCap.round;
final filleddotspaint = Paint()
..color = failed
? Theme.of(context).colorScheme.error
: Theme.of(context).colorScheme.secondary
..style = PaintingStyle.fill
..strokeCap = StrokeCap.round;
//draw the background
canvas.drawLine(
Offset(0, size.height / 2),
Offset(size.width, size.height / 2),
paint,
);
for (int i = 0; i < numberOfStates; i++) {
final double x = size.width * i / (numberOfStates - 1);
canvas.drawCircle(Offset(x, size.height / 2), 10, dotspaint);
}
//draw the filled out stuff
canvas.drawLine(
Offset(0, size.height / 2),
Offset(size.width * (currentState - 1) / (numberOfStates - 1),
size.height / 2),
filledpaint,
);
for (int i = 0; i < currentState; i++) {
final double x = size.width * i / (numberOfStates - 1);
canvas.drawCircle(Offset(x, size.height / 2), 10, filleddotspaint);
}
}
Any ideas or insights into how this "automatic" animation actually works is deeply appreciated!
Anton

I actually found a workaround for this, and have an assumption of why my circles were not animated:
for (int i = 0; i < currentState - 1; i++) {
final double x = size.width * i / (numberOfStates - 1);
canvas.drawCircle(Offset(x, size.height / 2), 10, filleddotspaint);
}
final double x = size.width * (currentState - 1) / (numberOfStates - 1);
canvas.drawCircle(Offset(x, size.height / 2), 10, filleddotspaint);
I changed the last circle to be drawn outside of the for loop. This seems to be enough for the framework to detect that this is something that it should animate. Maybe the for loop confused it in some way?

Related

Flutter Segmented Ring Around Number

In Flutter I try to make a segmented ring (or radial gauge) around a number to indicate its value. Like here
The example of in the link is already a solution itself, however, I read syncfusion widgets are not free. (I am using it commercially)
Any alternatives or ideas how to set this into practice?
You can use CustomPainter to achieve the same,
class RingPainter extends CustomPainter {
const RingPainter({
required this.percentage,
this.startColor,
this.endColor,
this.width,
}) : assert(percentage >= 0 && percentage <= 100,
"Percentage must be in the range 0-100!");
final double percentage;
final Color? startColor;
final Color? endColor;
final double? width;
double get progress => percentage / 100;
#override
void paint(Canvas canvas, Size size) {
var angle = math.pi / 180 * 230;
canvas.rotateAroundCenter(size, angle);
canvas.drawRing(
size,
1,
startColor: Colors.black12,
endColor: Colors.black12,
width: width,
);
canvas.drawRing(
size,
progress,
startColor: startColor,
endColor: endColor,
width: width,
);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
extension on Canvas {
rotateAroundCenter(Size size, double angleInRadians) {
final double r =
math.sqrt(size.width * size.width + size.height * size.height) / 2;
final alpha = math.atan(size.height / size.width);
final beta = alpha + angleInRadians;
final shiftY = r * math.sin(beta);
final shiftX = r * math.cos(beta);
final translateX = size.width / 2 - shiftX;
final translateY = size.height / 2 - shiftY;
translate(translateX, translateY);
rotate(angleInRadians);
}
drawRing(
Size size,
double value, {
Color? startColor,
Color? endColor,
double? width,
}) {
final rect = Rect.fromLTWH(-15, 0.0, size.width, size.height);
final gradient = SweepGradient(
startAngle: 3 * math.pi / 2,
endAngle: 7 * math.pi / 2,
tileMode: TileMode.repeated,
colors: [
startColor ?? Colors.pink,
endColor ?? Colors.blueAccent,
],
);
final paint = Paint()
..shader = gradient.createShader(rect)
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke
..strokeWidth = width ?? 24;
final center = Offset(size.width / 2, size.height / 2);
final radius =
math.min(size.width / 2, size.height / 2) - ((width ?? 24) / 2);
const startAngle = -math.pi / 2;
final sweepAngle = 2 * math.pi * value * 0.723;
drawArc(
Rect.fromCircle(center: center, radius: radius),
startAngle,
sweepAngle,
false,
paint,
);
}
}
Use it with CustomPaint widget,
CustomPaint(
painter: RingPainter(
percentage: 70,
width: 20,
),
)
In case, you want text inside ring,
Center(
child: SizedBox(
width: 200,
height: 200,
child: Stack(
fit: StackFit.expand,
children: [
const Center(
child: Text(
"70%",
style: TextStyle(
fontSize: 48,
fontWeight: FontWeight.w900,
color: Colors.purple,
),
),
),
CustomPaint(
painter: RingPainter(
percentage: 70,
width: 20,
),
),
],
),
),
)

How to draw an Arc in the middle of CustomPainter shape

I am trying to draw the following shape using CustomPainter, I am looking for a way
to make this shape as one shape (union), so for example using Stack with Container and half circle shape is not desired.
I have managed to paint the base rectangular shape as the following image shows, is it possible to build on top of this code to draw a half a circle in the middle ?
Container(
height: 85,
color: Colors.blueGrey,
child: Stack(
children: [
CustomPaint(
size: Size(size.width, 85),
painter: MyCustomPainter(),
)
],
),
),
class MyCustomPainter extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
var paint = Paint()
..color = Colors.red
..style = PaintingStyle.fill;
var path = Path()..moveTo(0, 20);
path.quadraticBezierTo(0, 0, 0, size.height * 0.5);
path.quadraticBezierTo(0, 0, size.width * 0.2, 0);
path.quadraticBezierTo(size.width, 0, size.width * 0.8, 0);
path.quadraticBezierTo(size.width, 0, size.width, size.height * 0.5);
path.lineTo(size.width, size.height);
path.lineTo(0, size.height);
canvas.drawPath(path, paint);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}
While the question about using union operation, I am focusing on
canvas.drawPath(
Path.combine(PathOperation.union, path1, circlePath), paint);
The current paint takes full height and doesnt maintain the ration based on your desire result. I am using Rect on Path to demonstrating the union operation. Change the size/value according to your needs.
class MyCustomPainter extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
var paint = Paint()
..color = Colors.red
..style = PaintingStyle.fill;
final double base = size.height * .4;
const corner = Radius.circular(16);
final path1 = Path()
..addRRect(
RRect.fromLTRBAndCorners(0, base, size.width, size.height,
topLeft: corner, topRight: corner),
);
final center = Offset(size.width / 2, size.height / 2);
final radius = size.height / 2;
final circlePath = Path()
..addOval(Rect.fromCircle(center: center, radius: radius));
canvas.drawPath(
Path.combine(PathOperation.union, path1, circlePath), paint);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
More about CustomPaint

Slicing a circular container in which each slice behaves like a tab in Flutter

I am trying to achieve a circular widget in which each slice represents a tab and having a separation line between each slice coming out from the centre of the circle as shown in the image.
I have tried bunch of packages and I found the following package fulfilling the purpose to some extent. https://pub.dev/packages/circular_widgets
I have also tried with Clipping and Custom painting API, but couldn't achieve what i am looking for.
I made custom painter with widgets
I hope this help you
class CircleCustomPainer extends CustomPainter {
var count = 6;
#override
void paint(Canvas canvas, Size size) {
Paint paint = Paint();
paint.color = Colors.red;
Paint paint1 = Paint();
paint1.color = Colors.white;
paint1.strokeWidth = 4;
paint1.style = PaintingStyle.stroke;
canvas.drawCircle(
Offset(size.width / 2, size.height / 2), size.height / 2, paint);
Path path = Path();
path.moveTo(size.width / 2, size.height / 2);
for (var i = 0; i < count; i++) {
var o = (2 * i * math.pi) / count;
var x = 150 * math.cos(o) + (size.width / 2);
var y = 150 * math.sin(o) + (size.height / 2);
path.lineTo(x, y);
path.moveTo(size.width / 2, size.height / 2);
}
canvas.drawPath(path, paint1);
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
this is widget creator for menu
Widget getWidgets(int count, Size size) {
List<Widget> list = [];
for (var i = 0; i < count; i++) {
var o = (2 * i * math.pi) / count;
o = o + ((360 / count) / 57.2958) / 2;
var x = (size.width / 3) * math.cos(o) + (size.width / 2);
var y = (size.width / 3) * math.sin(o) + (size.height / 2);
list.add(Positioned.fromRect(
rect: Rect.fromCenter(center: Offset(x, y), height: 60, width: 60),
child: Column(
children: [
Container(
color: Colors.black,
width: 60,
height: 60,
),
],
),
));
}
return Container(
width: 300,
height: 300,
alignment: Alignment.center,
child: Stack(
children: list,
),
);
}
you can used like this
SizedBox(
width: 300,
height: 300,
child: Stack(
alignment: Alignment.center,
children: [
CustomPaint(
size: Size(300, 300),
painter: CircleCustomPainer(),
),
Center(child: getWidgets(6, Size(300, 300)))
],
),
)
I will probably create a youtube video for that

flutter: custom shape as text background

I need to achieve this in flutter:
I tried making a Row and setting a background to the Text to handle the ractangle, then a ClipPath to make the triangle. The problem with this is that the edge is not precise (you can see a thin line between the rectangle and the triangle in the image below), also the ClipPath has fixed size so if I want to change the text size at some point I'll have to fine-tune the triangle again.
here's my code:
class TriangleClipper extends CustomClipper<Path> {
#override
Path getClip(Size size) {
final path = Path();
path.lineTo(0.0, size.height);
path.lineTo(size.width / 3, size.height);
path.close();
return path;
}
#override
bool shouldReclip(TriangleClipper oldClipper) => false;
}
Row(
children: [
Container(
color: Colors.orange,
child: Padding(
padding: const EdgeInsets.all(4.0),
child: Text(
"a label",
),
),
),
ClipPath(
clipper: TriangleClipper(),
child: Container(
color: Colors.orange,
height: 23,
width: 20,
),
)
],
)
I though using double.infinity instead of fixed size in ClipPath's child would solve it but doing that makes the triangle disappear (probably it becomes too big that I can't see it or goes behind something).
What is the correct way to approache this problem? I guess I could draw the trapezoid using a ClipPath, but how to make it so the width of it adapts to the lenght of the Text?
One way to achieve this is like so.
class TriangleClipper extends CustomClipper<Path> {
#override
Path getClip(Size size) {
final w = size.width;
final h = size.height;
final triangleWidth = 10;
final path = Path();
path.lineTo(0, 0);
path.lineTo(0, h);
path.lineTo(w, h);
path.lineTo(w - triangleWidth, 0);
path.close();
return path;
}
#override
bool shouldReclip(TriangleClipper oldClipper) => false;
}
and use it like so.
ClipPath(
clipper: TriangleClipper(),
child: Container(
padding: EdgeInsets.fromLTRB(4, 4, 14, 4), // 4 + triangleWidth for right padding
color: Colors.orange,
child: Text('A label'),
),
),
You can try the following code:
import 'dart:ui' as ui;
child: CustomPaint(
size: Size(WIDTH,(WIDTH*0.625).toDouble()), //You can Replace [WIDTH] with your desired width for Custom Paint and height will be calculated automatically
painter: RPSCustomPainter(),
),
class RPSCustomPainter extends CustomPainter{
#override
void paint(Canvas canvas, Size size) {
Paint paint_0 = new Paint()
..color = Color.fromARGB(255, 33, 150, 243)
..style = PaintingStyle.fill
..strokeWidth = 1;
paint_0.shader = ui.Gradient.linear(Offset(size.width*0.29,size.height*0.28),Offset(size.width*0.29,size.height*0.28),[Color(0xff7c1010),Color(0xffffffff)],[0.00,1.00]);
Path path_0 = Path();
path_0.moveTo(size.width*0.2937500,size.height*0.2780000);
canvas.drawPath(path_0, paint_0);
Paint paint_1 = new Paint()
..color = Color.fromARGB(255, 33, 150, 243)
..style = PaintingStyle.fill
..strokeWidth = 1;
Path path_1 = Path();
path_1.moveTo(size.width*0.3750000,size.height*0.4000000);
path_1.lineTo(size.width*0.5000000,size.height*0.4000000);
path_1.lineTo(size.width*0.5375000,size.height*0.5000000);
path_1.lineTo(size.width*0.3750000,size.height*0.5000000);
path_1.lineTo(size.width*0.3750000,size.height*0.4000000);
path_1.close();
canvas.drawPath(path_1, paint_1);
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
You can try it by yourself: https://shapemaker.web.app/#/

Flutter draw widget to canvas at certain path position

I have this custom path in my page
class TestPathPainter extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 2.0
..color = Colors.black;
var x = size.width;
var y = size.height;
print(x);
var path = Path()
..moveTo(x, y / 4)
..lineTo(x * 0.95, y / 4)
..lineTo(x * 0.95, y / 3)
..lineTo(x * 0.99, y / 3)
..lineTo(x * 0.99, y / 3.7)
..lineTo(x * 0.955, y / 3.7)
..lineTo(x * 0.955, y / 3.15)
..lineTo(x * 0.98, y / 3.15)
..lineTo(x * 0.98, y / 3.5)
..lineTo(x * 0.94, y / 3.5) // <==== I want to display a Checkbox here
..lineTo(x * 0.94, y / 2)
..lineTo(x * 0.91, y / 2)
..lineTo(x * 0.91, y / 1.65)
..lineTo(x * 0.94, y / 1.65)
..lineTo(x * 0.94, y / 1.4)
..lineTo(x * 0.91, y / 1.4);
canvas.drawPath(path, paint);
}
#override
bool shouldRepaint(TestPathPainter oldDelegate) => false;
}
How can i draw/render a widget ( a checkbox for example ) somewhere on this path?
I tried using a stacked widget and positioning the checkboxes but this wont look the same on every device.
The easiest way to position a widget at a specific location on the screen is to use a Stack and a Positioned widget.
Though, the problem we face is that the positioning (top, right, bottom, left) refers to the side of the child widget and not the center.
So, we need to adjust the positioning.
Solution 1 - Using SizedBox > Center
Positioned(
left: point.dx * width - 24,
top: point.dy * height - 24,
child: SizedBox(
width: 48,
height: 48,
child: Center(
child: Checkbox(
value: true,
onChanged: (_) {},
),
),
),
),
48 has been chosen to be sure we are bigger than the full size (including the padding caused by the materialTapTargetSize and the visualDensity.
This leads us to a second solution.
Solution 2: Computing the full size of the Checkbox
Though the Checkbox has a static const width of 18, it may vary depending on the materialTapTargetSize and the visualDensity.
If we have a look at the source code of the CheckBox on GitHub:
We can define a ComputeCheckBoxSize:
double computeCheckBoxSize(BuildContext context) {
final ThemeData themeData = Theme.of(context);
final MaterialTapTargetSize effectiveMaterialTapTargetSize =
themeData.checkboxTheme.materialTapTargetSize ??
themeData.materialTapTargetSize;
final VisualDensity effectiveVisualDensity =
themeData.checkboxTheme.visualDensity ?? themeData.visualDensity;
Size size;
switch (effectiveMaterialTapTargetSize) {
case MaterialTapTargetSize.padded:
size = const Size(kMinInteractiveDimension, kMinInteractiveDimension);
break;
case MaterialTapTargetSize.shrinkWrap:
size = const Size(
kMinInteractiveDimension - 8.0, kMinInteractiveDimension - 8.0);
break;
}
size += effectiveVisualDensity.baseSizeAdjustment;
return size.longestSide;
}
Our Positioned widget can be further simplified to:
Positioned(
left: point.dx * width - checkBoxSize / 2,
top: point.dy * height - checkBoxSize / 2,
child: Checkbox(
value: true,
onChanged: (_) {},
),
)
Full source code
import 'dart:math' show Random;
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
home: HomePage(),
),
);
}
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
double checkBoxSize = computeCheckBoxSize(context);
return Scaffold(
body: LayoutBuilder(
builder: (context, constraints) {
final height = constraints.biggest.height;
final width = constraints.biggest.width;
return Stack(
children: [
Container(color: Colors.amber.shade100),
Positioned.fill(child: CustomPaint(painter: TestPathPainter())),
...points
.map(
(point) => Positioned(
left: point.dx * width - checkBoxSize / 2,
top: point.dy * height - checkBoxSize / 2,
child: Checkbox(
value: true,
onChanged: (_) {},
),
),
)
.toList(),
],
);
},
),
);
}
}
double computeCheckBoxSize(BuildContext context) {
final ThemeData themeData = Theme.of(context);
final MaterialTapTargetSize effectiveMaterialTapTargetSize =
themeData.checkboxTheme.materialTapTargetSize ??
themeData.materialTapTargetSize;
final VisualDensity effectiveVisualDensity =
themeData.checkboxTheme.visualDensity ?? themeData.visualDensity;
Size size;
switch (effectiveMaterialTapTargetSize) {
case MaterialTapTargetSize.padded:
size = const Size(kMinInteractiveDimension, kMinInteractiveDimension);
break;
case MaterialTapTargetSize.shrinkWrap:
size = const Size(
kMinInteractiveDimension - 8.0, kMinInteractiveDimension - 8.0);
break;
}
size += effectiveVisualDensity.baseSizeAdjustment;
print(size);
return size.longestSide;
}
class TestPathPainter extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 2.0
..color = Colors.black;
final path = Path()
..moveTo(
points[0].dx * size.width,
points[0].dy * size.height,
);
points.sublist(1).forEach(
(point) => path.lineTo(
point.dx * size.width,
point.dy * size.height,
),
);
canvas.drawPath(path, paint);
}
#override
bool shouldRepaint(TestPathPainter oldDelegate) => false;
}
final random = Random();
final List<Offset> points = List.generate(
10,
(index) => Offset(.1 + random.nextDouble() * .8, .1 + index * .8 / 9),
);