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),
);
Related
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,
),
),
],
),
),
)
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
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?
I have TextField i need to change the border Corners Cut, I have try to do with wrap TextField on Container and apply BeveledRectangleBorder but is not proper.
Please suggest me how can do this.
Update
After searching a little i've found the source code of that design you shared with me. They have build a custom input border (CutCornersBorder) for that particular style. You can use that file and add that to your theme to get that look of your TextField without adding any extra code.
CutCornersBorder
return MaterialApp(
home: HomePage(),
theme: ThemeData(
inputDecorationTheme: InputDecorationTheme(border: CutCornersBorder()),
),
);
Full Preview
OLD
You can do it by creating your own custom painter implementation.
FlatCorneredBackgroundPainter
class FlatCorneredBackgroundPainter extends CustomPainter {
double radius, strokeWidth;
Color strokeColor;
FlatCorneredBackgroundPainter(
{this.radius = 10, this.strokeWidth = 4, this.strokeColor = Colors.blue});
#override
void paint(Canvas canvas, Size size) {
double w = size.width;
double h = size.height;
Paint paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..color = strokeColor;
Path path = Path()
..addPolygon([
Offset(radius, 0),
Offset(w - radius, 0),
Offset(w, radius),
Offset(w, h - radius),
Offset(w - radius, h),
Offset(radius, h),
Offset(0, h - radius),
Offset(0, radius),
], true);
canvas.drawPath(path, paint);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}
Usage
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: CustomPaint(
painter: FlatCorneredBackgroundPainter(radius: 10.0, strokeColor: Colors.red, strokeWidth: 2),
child: Container(
width: 300,
height: 70,
padding: const EdgeInsets.all(8.0),
child: TextField(),
),
),
),
);
}
}
I am building a list of boxes layouts in my app using flutter. I want dotted border around the box. I have used card widget to create the boxes. But, how can I get dotted border around the boxes?
EDIT
I have added this as a package in pub.
Now, all you need to do is
DottedBorder(
color: Colors.black,
gap: 3,
strokeWidth: 1,
child: FlutterLogo(size: 148),
)
Working Solution [Outdated]
Like tomerpacific said in this answer, Flutter does not have a default implementation for dashed border at the moment.
I worked for some time yesterday and was able to come up with a solution using CustomPainter. Hope this helps someone.
Add the DashedRect to your container, like so
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Container(
height: 400,
width: 300,
color: Colors.black12,
child: DashedRect(color: Colors.red, strokeWidth: 2.0, gap: 3.0,),
),
),
);
}
}
DashedRect.dart
import 'package:flutter/material.dart';
import 'dart:math' as math;
class DashedRect extends StatelessWidget {
final Color color;
final double strokeWidth;
final double gap;
DashedRect(
{this.color = Colors.black, this.strokeWidth = 1.0, this.gap = 5.0});
#override
Widget build(BuildContext context) {
return Container(
child: Padding(
padding: EdgeInsets.all(strokeWidth / 2),
child: CustomPaint(
painter:
DashRectPainter(color: color, strokeWidth: strokeWidth, gap: gap),
),
),
);
}
}
class DashRectPainter extends CustomPainter {
double strokeWidth;
Color color;
double gap;
DashRectPainter(
{this.strokeWidth = 5.0, this.color = Colors.red, this.gap = 5.0});
#override
void paint(Canvas canvas, Size size) {
Paint dashedPaint = Paint()
..color = color
..strokeWidth = strokeWidth
..style = PaintingStyle.stroke;
double x = size.width;
double y = size.height;
Path _topPath = getDashedPath(
a: math.Point(0, 0),
b: math.Point(x, 0),
gap: gap,
);
Path _rightPath = getDashedPath(
a: math.Point(x, 0),
b: math.Point(x, y),
gap: gap,
);
Path _bottomPath = getDashedPath(
a: math.Point(0, y),
b: math.Point(x, y),
gap: gap,
);
Path _leftPath = getDashedPath(
a: math.Point(0, 0),
b: math.Point(0.001, y),
gap: gap,
);
canvas.drawPath(_topPath, dashedPaint);
canvas.drawPath(_rightPath, dashedPaint);
canvas.drawPath(_bottomPath, dashedPaint);
canvas.drawPath(_leftPath, dashedPaint);
}
Path getDashedPath({
#required math.Point<double> a,
#required math.Point<double> b,
#required gap,
}) {
Size size = Size(b.x - a.x, b.y - a.y);
Path path = Path();
path.moveTo(a.x, a.y);
bool shouldDraw = true;
math.Point currentPoint = math.Point(a.x, a.y);
num radians = math.atan(size.height / size.width);
num dx = math.cos(radians) * gap < 0
? math.cos(radians) * gap * -1
: math.cos(radians) * gap;
num dy = math.sin(radians) * gap < 0
? math.sin(radians) * gap * -1
: math.sin(radians) * gap;
while (currentPoint.x <= b.x && currentPoint.y <= b.y) {
shouldDraw
? path.lineTo(currentPoint.x, currentPoint.y)
: path.moveTo(currentPoint.x, currentPoint.y);
shouldDraw = !shouldDraw;
currentPoint = math.Point(
currentPoint.x + dx,
currentPoint.y + dy,
);
}
return path;
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
I do not expect this to fit in with all use cases and there is a lot of room for customization and improvement. Comment if you find any bugs.
You can use dotted_border Flutter package
return DottedBorder(
borderType: BorderType.RRect,
radius: Radius.circular(20),
dashPattern: [10, 10],
color: Colors.grey,
strokeWidth: 2,
child: Card(
color: Colors.amber,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
child: Center(child: Text("hi")),
)
There is an one plugin for draw dotted border around widgets
https://pub.dev/packages/dotted_border
Using this plugin you can draw dotted or dashed border
//1. Install the plugin by add dependencies in pubspace.yaml
dotted_border: ^1.0.6
Add below code for show border
DottedBorder(
color: Colors.black,
strokeWidth: 1,
child: FlutterLogo(size: 148),
)
BorderStyle.none can be useful if you wanna apply some animation or remove\add border function onTap(like a lighting border) event or similar.