Add A Custom Decoration To Container, Using Painter Class - flutter

I know I can give decoration to my container, and then provide a border to it.
But I am trying to Wrap my Container with some Custom border using Custom Paint class, The Custom Paint Class should surround or provide a border to container like a dashed border or any custom shape border,
Note:- Please Don't suggest me to use any custom package like dashed Border, I want the answer in the form of Custom Painter like BoxPainter class etc.
So, Far I have wrote this code
import 'dart:math';
import 'package:flutter/material.dart';
class CustPaint extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
width: 300,
height: 300,
decoration: CustomDecoration(),
child: new Icon(Icons.wb_sunny),
)),
);
}
}
class CustomDecoration extends Decoration {
#override
BoxPainter createBoxPainter([void Function() onChanged]) {
return BoxDecorationPainter();
}
}
class BoxDecorationPainter extends BoxPainter {
#override
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
final Rect bounds = offset & configuration.size;
_drawDecoration(canvas, bounds);
}
}
void _drawDecoration(Canvas canvas, Rect size) {
var radius = min(size.width / 2, size.height / 2);
var circleRaidus = radius * .012;
var paint = Paint()
..color = Colors.teal
..strokeWidth = circleRaidus * .5
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
for (int i = 0; i < 60; i++) {
Offset topRight = Offset(size.left + i * 4, size.top);
Offset downLeft = Offset(size.left, size.top + i * 4);
Offset bottomRight = Offset(size.left + i * 4, size.bottom);
Offset downRight = Offset(size.right, size.top + i * 4);
canvas.drawCircle(topRight, circleRaidus * 2, paint);
canvas.drawCircle(bottomRight, circleRaidus * 2, paint);
canvas.drawCircle(downRight, circleRaidus * 2, paint);
canvas.drawCircle(downLeft, circleRaidus * 2, paint);
}
}
And What I have Achieved is
But The OUTPUT
I want is that all the lines (with tiny circles) should join and there should be no gap in between them while meeting corner to corner. (means they should join to cover the container like a square or rectangle)
Also I want to give some space between those tiny circles like
some padding.
It would be a plus if those circles can be
replaced by any image.
Thanks in Advance

The number of circles has to be adapted to the size of the box:
final nbShapes = [
((bounds.width + gap) / (shapeSize + gap)).floor(),
((bounds.height + gap) / (shapeSize + gap)).floor(),
];
In my Solution, the CustomDecorationBox can be configured with:
double shapeSize, the size of the shapes,
double shapeGap, the gap between two shapes,
Paint shapePaint, the paint used to draw the shapes (support for both strokes and fill paints)
ShapePainter paintShape, a function painting the shapes
I provided 3 ShapePainter samples:
paintCircle()
paintRectangle()
ShapePainter createNGonPainter(int n), a factory for n-gon painters
Full source code:
import 'dart:math';
import 'package:flutter/material.dart';
typedef ShapePainter = void Function(Canvas, Rect, Paint);
Random random = Random();
void main() {
runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
title: 'CustomDecoration Demo',
home: Scaffold(
body: MyWidget(),
),
),
);
}
class MyWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: LayoutBuilder(
builder: (context, constraints) {
final size = Size(1, .6) * constraints.biggest.width;
print(size);
return GridView.count(
crossAxisCount: 3,
children: List.generate(
120,
(index) => Padding(
padding: EdgeInsets.all(size.width * .01),
child: Container(
decoration: CustomDecoration(
shapeSize: 5.0 + random.nextInt(10),
shapeGap: 2.0 + random.nextInt(3),
shapePaint: Paint()
..color = Color(0x99000000 + random.nextInt(0xffffff))
..strokeWidth = random.nextInt(3).toDouble()
..style = random.nextInt(3) == 2
? PaintingStyle.fill
: PaintingStyle.stroke,
paintShape: random.nextInt(4) == 0
? paintCircle
: createNGonPainter(3 + random.nextInt(5)),
),
child: Center(
child: Text(
index.toString(),
style: TextStyle(fontSize: 24.0),
),
),
),
),
),
);
},
),
);
}
}
class CustomDecoration extends Decoration {
final double shapeSize;
final double shapeGap;
final Paint shapePaint;
final ShapePainter paintShape;
CustomDecoration({
this.shapeSize,
this.shapeGap,
this.shapePaint,
this.paintShape,
}) : super();
#override
BoxPainter createBoxPainter([void Function() onChanged]) {
return BoxDecorationPainter(
shapeSize: shapeSize ?? 10,
shapeGap: shapeGap ?? 4,
shapePaint: shapePaint ?? Paint(),
paintShape: paintShape ?? paintCircle);
}
}
class BoxDecorationPainter extends BoxPainter {
final double shapeSize;
final double shapeGap;
final Paint shapePaint;
final ShapePainter paintShape;
BoxDecorationPainter({
this.shapeSize,
this.shapeGap,
this.shapePaint,
this.paintShape,
}) : super();
#override
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
final Rect bounds = offset & configuration.size;
_drawDecoration(canvas, bounds, shapeSize, shapeGap);
}
void _drawDecoration(
Canvas canvas, Rect bounds, double shapeSize, double gap) {
final nbShapes = [
((bounds.width + gap) / (shapeSize + gap)).floor(),
((bounds.height + gap) / (shapeSize + gap)).floor(),
];
final correctedGaps = [
(bounds.width - nbShapes[0] * shapeSize) / (nbShapes[0] - 1),
(bounds.height - nbShapes[1] * shapeSize) / (nbShapes[1] - 1),
];
final steps = [
correctedGaps[0] + shapeSize,
correctedGaps[1] + shapeSize,
];
for (int i = 0; i < nbShapes[0]; i++) {
paintShape(
canvas,
Rect.fromLTWH(
bounds.left + steps[0] * i,
bounds.top,
shapeSize,
shapeSize,
),
shapePaint,
);
paintShape(
canvas,
Rect.fromLTWH(
bounds.left + steps[0] * i,
bounds.bottom - shapeSize,
shapeSize,
shapeSize,
),
shapePaint,
);
}
for (int i = 1; i < nbShapes[1] - 1; i++) {
paintShape(
canvas,
Rect.fromLTWH(
bounds.left,
bounds.top + steps[1] * i,
shapeSize,
shapeSize,
),
shapePaint,
);
paintShape(
canvas,
Rect.fromLTWH(
bounds.right - shapeSize,
bounds.top + steps[1] * i,
shapeSize,
shapeSize,
),
shapePaint,
);
}
}
}
void paintCircle(Canvas canvas, Rect bounds, Paint paint) {
canvas.drawCircle(
Offset(
bounds.left + bounds.width / 2,
bounds.top + bounds.height / 2,
),
bounds.shortestSide / 2,
paint,
);
}
void paintRectangle(Canvas canvas, Rect bounds, Paint paint) {
canvas.drawRect(bounds, paint);
}
ShapePainter createNGonPainter(int n) => (canvas, bounds, paint) {
Path path = Path();
path.moveTo(
bounds.left + (bounds.width + cos(2 * pi / n) * bounds.width) / 2,
bounds.top + (bounds.height + sin(2 * pi / n) * bounds.height) / 2,
);
for (var k = 2; k <= n; k++) {
path.lineTo(
bounds.left + (bounds.width + cos(2 * k * pi / n) * bounds.width) / 2,
bounds.top +
(bounds.height + sin(2 * k * pi / n) * bounds.height) / 2,
);
}
path.close();
canvas.drawPath(path, paint);
};

Related

How to create quarter ring shape in Flutter?

I'm trying to create the quarter ring shape in the top left corner without success, any help?
I tried creating a container with border radius and then give it a border for the color, but it didn't go as I expected
You can create custom ring circle by this way
Firstly you need to Create custom class same as below
import 'dart:math' as math;
class MyCustomPainter extends CustomPainter {
final learned;
final notLearned;
final range;
final totalQuestions;
double pi = math.pi;
MyCustomPainter({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;
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, paint1);
}
double getRadians(double value) {
return (360 * value) * pi / 180;
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
Then call that class in following way
Stack(
children: [
//Your body widget will be here
CustomPaint(
painter: MyCustomPainter(
totalQuestions: 300,
learned: 75,
notLearned: 75,
range: 10),
)
],
),
It will look like this

Why Flutter AnimationController becomes very laggy after repeating itself for 3 times

I have an AnimationController, and I want to repeat it every time the animation completes.
Let's say I want it to repeat for 4 times as is shown below:
// ......
// _sets is initialised as 4
_controller = AnimationController(
duration: Duration(seconds: _roundDuration), vsync: this);
_controller.addListener(() {
if (_controller.isCompleted) {
_sets--;
if (_sets > 0) {
_controller.reset();
_controller.forward();
} else {
_controller.stop();
}
}
});
super.initState();
}
The problem is that after repeating this process for 3 times, it becomes very laggy.
The value of the controller is passed to an AnimatedBuilder for driving my CustomPainter-based animation:
child: AnimatedBuilder(
builder: (_, __) {
return CustomPaint(
painter: MyPainter(
percentValue: _controller.value,
moveDuration: _moveDuration,
holdDuration: _holdDuration));
},
animation: _controller,
),
And my CustomPainter looks like this:
class MyPainter extends CustomPainter {
final double percentValue;
final int moveDuration;
final int holdDuration;
MyPainter(
{required this.percentValue,
required this.moveDuration,
required this.holdDuration});
#override
void paint(Canvas canvas, Size size) {
// print("paint percent value: $percentValue");
final holdingLinePaint = Paint()
..strokeWidth = 40
..color = Colors.amber
..strokeCap = StrokeCap.round;
final linePaint = Paint()
..strokeWidth = 40
..color = Colors.red
..strokeCap = StrokeCap.round;
const lineLength = 380;
const leftPadding = 10.0;
// animation duration
// moveDuration represents the two red ends and holdDuration represents the yellow part at the middle of the line
final totalDuration = 2 * moveDuration + holdDuration;
final dy = size.height / 2;
final lineStart = Offset(leftPadding, dy);
// lineEnd is animating according to the percentValue passed in
final lineEnd = Offset(percentValue * 380 + leftPadding, dy);
// line one
var firstEnd =
Offset((lineLength / totalDuration) * moveDuration + leftPadding, dy);
canvas.drawCircle(firstEnd, 10, Paint());
// line two
var secondStart = firstEnd;
var secondEnd = Offset(
(moveDuration + holdDuration) / totalDuration * lineLength +
leftPadding,
dy);
canvas.drawCircle(secondEnd, 10, Paint());
// line three
var thirdStart = secondEnd;
// divided into 3 phrases
if (percentValue < (moveDuration / totalDuration)) {
canvas.drawLine(lineStart, lineEnd, linePaint);
} else if (percentValue >= (moveDuration / totalDuration) &&
percentValue < (moveDuration + holdDuration) / totalDuration) {
canvas.drawLine(secondStart, lineEnd, holdingLinePaint);
canvas.drawLine(lineStart, firstEnd, linePaint);
} else {
canvas.drawLine(thirdStart, lineEnd, linePaint);
canvas.drawLine(secondStart, secondEnd, holdingLinePaint);
canvas.drawLine(lineStart, firstEnd, linePaint);
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
Basically, I am just animating a line but have to draw it with different colors according to different phrases.
Any advice?

Creating a Proper Semi Circular Slider Widget in Flutter

how do we create a proper Semi Circular Slider that has "steps" line division.
I have checked out many packages on pub.dev but they doesn't seem to provide a proper Semi Circular Slider. They look more like progress bar rather than a slider
Any thoughts please?
This can be done with a CustomPainter, GestureDetector, and a bunch of math.
Full example: https://gist.github.com/PixelToast/7dfbc4d743b108755b6521d0b8f24fd9
DartPad: https://dartpad.dartlang.org/?id=7dfbc4d743b108755b6521d0b8f24fd9
class SemiCircleSlider extends StatefulWidget {
const SemiCircleSlider({
Key? key,
required this.initialValue,
required this.divisions,
required this.onChanged,
required this.image,
}) : super(key: key);
final int initialValue;
final int divisions;
final ValueChanged<int> onChanged;
final ImageProvider image;
#override
State<SemiCircleSlider> createState() => _SemiCircleSliderState();
}
class _SemiCircleSliderState extends State<SemiCircleSlider> {
late var value = widget.initialValue;
#override
Widget build(BuildContext context) {
return SizedBox(
width: 350,
child: LayoutBuilder(
builder: (context, constraints) {
// Apply some padding to the outside so the nub doesn't go past the
// edge of the painter.
const inset = 32.0;
final arcWidth = constraints.maxWidth - inset * 2;
final height = (arcWidth / 2) + inset * 2;
final arcHeight = (height - inset * 2) * 2;
final arcRect = Rect.fromLTRB(
inset,
height - (inset + arcHeight),
arcWidth + inset,
height - inset,
);
Widget child = TweenAnimationBuilder<double>(
tween: Tween(begin: value.toDouble(), end: value.toDouble()),
duration: const Duration(milliseconds: 50),
curve: Curves.ease,
builder: (context, value, child) {
return CustomPaint(
painter: SemiCircleSliderPainter(
divisions: widget.divisions,
arcRect: arcRect,
// Map the value to the angle at which to display the nub
nubAngle: (1 - (value / (widget.divisions - 1))) * pi,
),
child: SizedBox(
height: height,
),
);
},
);
child = GestureDetector(
// Use TweenAnimationBuilder to smoothly animate between divisions
child: child,
onPanUpdate: (e) {
// Calculate the angle of the tap relative to the center of the
// arc, then map that angle to a value
final position = e.localPosition - arcRect.center;
final angle = atan2(position.dy, position.dx);
final newValue =
((1 - (angle / pi)) * (widget.divisions - 1)).round();
if (value != newValue &&
newValue >= 0 &&
newValue < widget.divisions) {
widget.onChanged(newValue);
setState(() {
value = newValue;
});
}
},
);
// Subtract by one to prevent the background from bleeding through
// and creating a seam
const imageInset = inset + SemiCircleSliderPainter.lineWidth - 1;
const imageTopInset = inset - SemiCircleSliderPainter.lineWidth / 2;
child = Stack(
fit: StackFit.passthrough,
children: [
// Position the image so that it fits neatly inside the semicircle
Positioned(
left: imageInset,
top: imageTopInset,
right: imageInset,
bottom: imageInset,
child: ClipRRect(
// A clever trick to round it into a semi-circle: round the
// bottom left and bottom right a large amount
borderRadius: const BorderRadius.vertical(
bottom: Radius.circular(1000.0),
),
child: Image(
image: widget.image,
fit: BoxFit.cover,
),
),
),
child,
],
);
return child;
},
),
);
}
}
class SemiCircleSliderPainter extends CustomPainter {
SemiCircleSliderPainter({
required this.divisions,
required this.arcRect,
required this.nubAngle,
});
final int divisions;
final Rect arcRect;
final double nubAngle;
static const nubRadius = 16.0;
static const lineWidth = 16.0;
static const stepThickness = 3.0;
static const stepLength = 2.0;
late final lineArcRect = arcRect.deflate(lineWidth / 2);
late final xradius = lineArcRect.width / 2;
late final yradius = lineArcRect.height / 2;
late final center = arcRect.center;
late final nubPath = Path()
..addPath(
Path()
..moveTo(0, 0)
..arcTo(
const Offset(nubRadius / 2, -nubRadius) &
const Size.fromRadius(nubRadius),
5 * pi / 4,
3 * pi / 2,
false,
),
Offset(
center.dx + cos(nubAngle) * xradius,
center.dy + sin(nubAngle) * yradius,
),
matrix4: Matrix4.rotationZ(nubAngle).storage,
);
#override
void paint(Canvas canvas, Size size) {
// Paint large arc
canvas.drawPath(
Path()
// Extend a line on the left and right so the markers aren't sitting
// right on the border
..moveTo(lineArcRect.right, lineArcRect.center.dy - lineWidth / 2)
..arcTo(
lineArcRect,
0,
pi,
false,
)
..lineTo(lineArcRect.left, lineArcRect.center.dy - lineWidth / 2),
Paint()
..style = PaintingStyle.stroke
..color = Colors.black
..strokeWidth = lineWidth,
);
// Paint division markers
for (var i = 0; i < divisions; i++) {
final angle = pi * i / (divisions - 1);
final xnorm = cos(angle);
final ynorm = sin(angle);
canvas.drawLine(
center +
Offset(
xnorm * (xradius - stepLength),
ynorm * (yradius - stepLength),
),
center +
Offset(
xnorm * (xradius + stepLength),
ynorm * (yradius + stepLength),
),
Paint()
..style = PaintingStyle.stroke
..color = Colors.white
..strokeWidth = stepThickness
..strokeCap = StrokeCap.round,
);
}
// Paint nub
canvas.drawPath(
nubPath,
Paint()..color = Colors.pink.shade200,
);
}
#override
bool? hitTest(Offset position) {
// Only respond to hit tests when tapping the nub
return nubPath.contains(position);
}
#override
bool shouldRepaint(SemiCircleSliderPainter oldDelegate) =>
divisions != oldDelegate.divisions ||
arcRect != oldDelegate.arcRect ||
nubAngle != oldDelegate.nubAngle;
}
For semi-circle, I would recommend sleek_circular_slider which doesn't require any kind of license. You can see this YouTube video for visual learning.
If you are ok with circular sliders, you've just got more options. You can use flutter_circular_slider package and the author has the behind the scene explanation here.
Here is another video tutorial if you are more into built on my own attitude. Here is the complete source code for it.
https://github.com/JideGuru/youtube_videos/tree/master/rainbow_circular_slider
This is based on the source code of sleek_circular_slider customized to be almost exactly the screenshot provided. I've included some new properties like touchWidth, innerTrackWidth, outerTrackWidth, handlerSize and handlerWidth.
Check out the live demo on DartPad
Source code:
/*
Copyright (c) 2019 Mat Nuckowski
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import 'dart:math' as math;
import 'dart:ui' as ui;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(),
debugShowCheckedModeBanner: false,
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
String _modifier(double value) => '${value.ceil().toInt()}';
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xff827c7e),
body: Center(
child: CircularSlider(
appearance: CircularSliderAppearance(
startAngle: 180,
angleRange: 180,
size: 300,
counterClockwise: true,
infoProperties: InfoProperties(modifier: _modifier),
customColors: CustomSliderColors(),
customWidths: CustomSliderWidths(
touchWidth: 60,
innerTrackWidth: 10,
outerTrackWidth: 4,
handlerSize: 20,
handlerWidth: 20,
),
),
min: 0,
max: 10,
initialValue: 6,
onChange: (value) {},
),
),
);
}
}
typedef OnChange = void Function(double value);
typedef InnerWidget = Widget Function(double percentage);
class CircularSlider extends StatefulWidget {
final double initialValue;
final double min;
final double max;
final CircularSliderAppearance appearance;
final OnChange? onChange;
final OnChange? onChangeStart;
final OnChange? onChangeEnd;
final InnerWidget? innerWidget;
static const defaultAppearance = CircularSliderAppearance();
double get angle =>
valueToAngle(initialValue, min, max, appearance.angleRange);
const CircularSlider({
Key? key,
this.initialValue = 50,
this.min = 0,
this.max = 100,
this.appearance = defaultAppearance,
this.onChange,
this.onChangeStart,
this.onChangeEnd,
this.innerWidget,
}) : assert(min <= max),
assert(initialValue >= min && initialValue <= max),
super(key: key);
#override
State<CircularSlider> createState() => _CircularSliderState();
}
class _CircularSliderState extends State<CircularSlider>
with SingleTickerProviderStateMixin {
bool _isHandlerSelected = false;
bool _animationInProgress = false;
_CurvePainter? _painter;
double? _oldWidgetAngle;
double? _oldWidgetValue;
double? _curAngle;
late double _startAngle;
late double _angleRange;
double? _selectedAngle;
double? _rotation;
SpinAnimationManager? _spinManager;
ValueChangedAnimationManager? _animationManager;
late int _appearanceHashCode;
bool get _interactionEnabled => (widget.onChangeEnd != null ||
widget.onChange != null && !widget.appearance.spinnerMode);
#override
void initState() {
super.initState();
_startAngle = widget.appearance.startAngle;
_angleRange = widget.appearance.angleRange;
_appearanceHashCode = widget.appearance.hashCode;
if (!widget.appearance.animationEnabled) return;
widget.appearance.spinnerMode ? _spin() : _animate();
}
#override
void didUpdateWidget(CircularSlider oldWidget) {
if (oldWidget.angle != widget.angle &&
_curAngle?.toStringAsFixed(4) != widget.angle.toStringAsFixed(4)) {
_animate();
}
super.didUpdateWidget(oldWidget);
}
void _animate() {
if (!widget.appearance.animationEnabled || widget.appearance.spinnerMode) {
_setupPainter();
_updateOnChange();
return;
}
_animationManager ??= ValueChangedAnimationManager(
tickerProvider: this,
minValue: widget.min,
maxValue: widget.max,
durationMultiplier: widget.appearance.animDurationMultiplier,
);
_animationManager!.animate(
initialValue: widget.initialValue,
angle: widget.angle,
oldAngle: _oldWidgetAngle,
oldValue: _oldWidgetValue,
valueChangedAnimation: ((double anim, bool animationCompleted) {
_animationInProgress = !animationCompleted;
setState(() {
if (!animationCompleted) {
_curAngle = anim;
// update painter and the on change closure
_setupPainter();
_updateOnChange();
}
});
}));
}
void _spin() {
_spinManager = SpinAnimationManager(
tickerProvider: this,
duration: Duration(milliseconds: widget.appearance.spinnerDuration),
spinAnimation: ((double anim1, anim2, anim3) {
setState(() {
_rotation = anim1;
_startAngle = math.pi * anim2;
_curAngle = anim3;
_setupPainter();
_updateOnChange();
});
}));
_spinManager!.spin();
}
#override
Widget build(BuildContext context) {
/// _setupPainter excution when _painter is null or appearance has changed.
if (_painter == null || _appearanceHashCode != widget.appearance.hashCode) {
_appearanceHashCode = widget.appearance.hashCode;
_setupPainter();
}
return RawGestureDetector(
gestures: <Type, GestureRecognizerFactory>{
_CustomPanGestureRecognizer:
GestureRecognizerFactoryWithHandlers<_CustomPanGestureRecognizer>(
() => _CustomPanGestureRecognizer(
onPanDown: _onPanDown,
onPanUpdate: _onPanUpdate,
onPanEnd: _onPanEnd,
),
(_CustomPanGestureRecognizer instance) {},
),
},
child: _buildRotatingPainter(
rotation: _rotation,
size: Size(widget.appearance.size, widget.appearance.size)));
}
#override
void dispose() {
_spinManager?.dispose();
_animationManager?.dispose();
super.dispose();
}
void _setupPainter({bool counterClockwise = false}) {
var defaultAngle = _curAngle ?? widget.angle;
if (_oldWidgetAngle != null) {
if (_oldWidgetAngle != widget.angle) {
_selectedAngle = null;
defaultAngle = widget.angle;
}
}
_curAngle = calculateAngle(
startAngle: _startAngle,
angleRange: _angleRange,
selectedAngle: _selectedAngle,
defaultAngle: defaultAngle,
counterClockwise: counterClockwise);
_painter = _CurvePainter(
startAngle: _startAngle,
angleRange: _angleRange,
angle: _curAngle! < 0.5 ? 0.5 : _curAngle!,
appearance: widget.appearance);
_oldWidgetAngle = widget.angle;
_oldWidgetValue = widget.initialValue;
}
void _updateOnChange() {
if (widget.onChange != null && !_animationInProgress) {
final value =
angleToValue(_curAngle!, widget.min, widget.max, _angleRange);
widget.onChange!(value);
}
}
Widget _buildRotatingPainter({double? rotation, required Size size}) {
if (rotation != null) {
return Transform(
transform: Matrix4.identity()..rotateZ((rotation) * 5 * math.pi / 6),
alignment: FractionalOffset.center,
child: _buildPainter(size: size));
}
return _buildPainter(size: size);
}
Widget _buildPainter({required Size size}) {
return CustomPaint(
painter: _painter,
child: SizedBox(
width: size.width,
height: size.height,
child: _buildChildWidget()));
}
Widget? _buildChildWidget() {
if (widget.appearance.spinnerMode) return null;
final value = angleToValue(_curAngle!, widget.min, widget.max, _angleRange);
final childWidget = widget.innerWidget != null
? widget.innerWidget!(value)
: SliderLabel(
value: value,
appearance: widget.appearance,
);
return childWidget;
}
void _onPanUpdate(Offset details) {
if (!_isHandlerSelected) return;
if (_painter?.center == null) return;
_handlePan(details, false);
}
void _onPanEnd(Offset details) {
_handlePan(details, true);
if (widget.onChangeEnd != null) {
widget.onChangeEnd!(
angleToValue(_curAngle!, widget.min, widget.max, _angleRange));
}
_isHandlerSelected = false;
}
void _handlePan(Offset details, bool isPanEnd) {
if (_painter?.center == null) return;
RenderBox renderBox = context.findRenderObject() as RenderBox;
var position = renderBox.globalToLocal(details);
final double touchWidth = widget.appearance.touchWidth >= 25.0
? widget.appearance.touchWidth
: 25.0;
if (isPointAlongCircle(
position, _painter!.center!, _painter!.radius, touchWidth)) {
_selectedAngle = coordinatesToRadians(_painter!.center!, position);
// setup painter with new angle values and update onChange
_setupPainter(counterClockwise: widget.appearance.counterClockwise);
_updateOnChange();
setState(() {});
}
}
bool _onPanDown(Offset details) {
if (_painter == null || _interactionEnabled == false) {
return false;
}
RenderBox renderBox = context.findRenderObject() as RenderBox;
var position = renderBox.globalToLocal(details);
final angleWithinRange = isAngleWithinRange(
startAngle: _startAngle,
angleRange: _angleRange,
touchAngle: coordinatesToRadians(_painter!.center!, position),
previousAngle: _curAngle,
counterClockwise: widget.appearance.counterClockwise);
if (!angleWithinRange) {
return false;
}
final double touchWidth = widget.appearance.touchWidth >= 25.0
? widget.appearance.touchWidth
: 25.0;
if (isPointAlongCircle(
position, _painter!.center!, _painter!.radius, touchWidth)) {
_isHandlerSelected = true;
if (widget.onChangeStart != null) {
widget.onChangeStart!(
angleToValue(_curAngle!, widget.min, widget.max, _angleRange));
}
_onPanUpdate(details);
} else {
_isHandlerSelected = false;
}
return _isHandlerSelected;
}
}
typedef SpinAnimation = void Function(
double animation1, double animation2, double animation3);
class SpinAnimationManager {
final TickerProvider tickerProvider;
final Duration duration;
final SpinAnimation spinAnimation;
SpinAnimationManager({
required this.spinAnimation,
required this.duration,
required this.tickerProvider,
});
late Animation<double> _animation1;
late Animation<double> _animation2;
late Animation<double> _animation3;
late AnimationController _animController;
void spin() {
_animController = AnimationController(
vsync: tickerProvider, duration: duration)
..addListener(() {
spinAnimation(_animation1.value, _animation2.value, _animation3.value);
})
..repeat();
_animation1 = Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation(
parent: _animController,
curve: const Interval(0.5, 1.0, curve: Curves.linear)));
_animation2 = Tween<double>(begin: -80.0, end: 100.0).animate(
CurvedAnimation(
parent: _animController,
curve: const Interval(0, 1.0, curve: Curves.linear)));
_animation3 = Tween(begin: 0.0, end: 360.0).animate(CurvedAnimation(
parent: _animController,
curve: const Interval(0.0, 1.0, curve: SpinnerCurve())));
}
void dispose() {
_animController.dispose();
}
}
class SpinnerCurve extends Curve {
const SpinnerCurve();
#override
double transform(double t) => (t <= 0.5) ? 1.9 * t : 1.85 * (1 - t);
}
typedef ValueChangeAnimation = void Function(
double animation, bool animationFinished);
class ValueChangedAnimationManager {
final TickerProvider tickerProvider;
final double durationMultiplier;
final double minValue;
final double maxValue;
ValueChangedAnimationManager({
required this.tickerProvider,
required this.minValue,
required this.maxValue,
this.durationMultiplier = 1.0,
});
late Animation<double> _animation;
late final AnimationController _animController =
AnimationController(vsync: tickerProvider);
bool _animationCompleted = false;
void animate(
{required double initialValue,
double? oldValue,
required double angle,
double? oldAngle,
required ValueChangeAnimation valueChangedAnimation}) {
_animationCompleted = false;
final duration = (durationMultiplier *
valueToDuration(
initialValue, oldValue ?? minValue, minValue, maxValue))
.toInt();
_animController.duration = Duration(milliseconds: duration);
final curvedAnimation = CurvedAnimation(
parent: _animController,
curve: Curves.easeOut,
);
_animation =
Tween<double>(begin: oldAngle ?? 0, end: angle).animate(curvedAnimation)
..addListener(() {
valueChangedAnimation(_animation.value, _animationCompleted);
})
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
_animationCompleted = true;
_animController.reset();
}
});
_animController.forward();
}
void dispose() {
_animController.dispose();
}
}
class _CurvePainter extends CustomPainter {
final double angle;
final CircularSliderAppearance appearance;
final double startAngle;
final double angleRange;
Offset? handler;
Offset? center;
late double radius;
_CurvePainter(
{required this.appearance,
this.angle = 30,
required this.startAngle,
required this.angleRange});
#override
void paint(Canvas canvas, Size size) {
radius =
math.min(size.width / 2, size.height / 2) - appearance.touchWidth * 0.5;
center = Offset(size.width / 2, size.height / 2);
final trackPaint = Paint()
..style = PaintingStyle.stroke
..color = appearance.outerTrackColor
..strokeWidth = appearance.outerTrackWidth;
drawCircularArc(
canvas: canvas,
size: Size(size.width, size.height),
paint: trackPaint,
radius: radius +
appearance.innerTrackWidth / 2 +
appearance.outerTrackWidth / 2,
ignoreAngle: true,
spinnerMode: appearance.spinnerMode);
trackPaint.color = appearance.innerTrackColor;
trackPaint.strokeWidth = appearance.innerTrackWidth;
drawCircularArc(
canvas: canvas,
size: size,
paint: trackPaint,
radius: radius,
ignoreAngle: true,
spinnerMode: appearance.spinnerMode);
final currentAngle = appearance.counterClockwise ? -angle : angle;
var dividersPaint = Paint()
..color = Colors.white
..strokeWidth = 1.5;
for (final angle in [0, 18, 36, 54, 72, 108, 126, 144, 162, 180]) {
Offset handler = degreesToCoordinates(center!, -math.pi / 2 + angle + 1.5,
radius - appearance.innerTrackWidth / 2 + 2);
Offset handler2 = degreesToCoordinates(
center!,
-math.pi / 2 + angle + 1.5,
radius + appearance.innerTrackWidth / 2 - 2);
canvas.drawLine(handler, handler2, dividersPaint);
}
var dotPaint = Paint();
Offset dotHandler =
degreesToCoordinates(center!, -math.pi / 2 + 90 + 1.5, radius);
dotPaint.shader = ui.Gradient.radial(
dotHandler,
appearance.innerTrackWidth / 3,
[const Color(0xfff963b5), const Color(0xfffe94cd)]);
canvas.drawCircle(dotHandler, appearance.innerTrackWidth / 3, dotPaint);
Offset handler = degreesToCoordinates(
center!, -math.pi / 2 + startAngle + currentAngle + 1.5, radius);
Offset handler2 = degreesToCoordinates(
center!,
-math.pi / 2 + startAngle + currentAngle + 1.5,
radius + appearance.handlerWidth);
var handlerPaint = Paint()
..shader = ui.Gradient.linear(handler, handler2,
[const Color(0xfffbf9fa), const Color(0xffff7ba3)]);
final path = Path()
..moveTo(handler2.dx, handler2.dy)
..arcTo(
Rect.fromCenter(
center: handler2,
width: appearance.handlerSize,
height: appearance.handlerSize),
degreeToRadians(currentAngle) + math.pi / 2 - math.pi / 4,
math.pi + math.pi / 2,
true,
)
..lineTo(handler.dx, handler.dy)
..close();
canvas.drawPath(path, handlerPaint);
}
drawCircularArc({
required Canvas canvas,
required Size size,
required Paint paint,
required double radius,
bool ignoreAngle = false,
bool spinnerMode = false,
}) {
final double angleValue = ignoreAngle ? 0 : (angleRange - angle);
final range = appearance.counterClockwise ? -angleRange : angleRange;
final currentAngle = appearance.counterClockwise ? angleValue : -angleValue;
canvas.drawArc(
Rect.fromCircle(center: center!, radius: radius),
degreeToRadians(spinnerMode ? 0 : startAngle),
degreeToRadians(spinnerMode ? 360 : range + currentAngle),
false,
paint,
);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
double degreeToRadians(double degree) => (math.pi / 180) * degree;
double radiansToDegrees(double radians) => radians * (180 / math.pi);
Offset degreesToCoordinates(Offset center, double degrees, double radius) =>
radiansToCoordinates(center, degreeToRadians(degrees), radius);
Offset radiansToCoordinates(Offset center, double radians, double radius) {
var dx = center.dx + radius * math.cos(radians);
var dy = center.dy + radius * math.sin(radians);
return Offset(dx, dy);
}
double coordinatesToRadians(Offset center, Offset coords) {
var a = coords.dx - center.dx;
var b = center.dy - coords.dy;
return radiansNormalized(math.atan2(b, a));
}
double radiansNormalized(double radians) {
var normalized = radians < 0 ? -radians : 2 * math.pi - radians;
return normalized;
}
bool isPointInsideCircle(Offset point, Offset center, double rradius) {
var radius = rradius * 1.2;
return point.dx < (center.dx + radius) &&
point.dx > (center.dx - radius) &&
point.dy < (center.dy + radius) &&
point.dy > (center.dy - radius);
}
bool isPointAlongCircle(
Offset point, Offset center, double radius, double width) {
var dx = math.pow(point.dx - center.dx, 2);
var dy = math.pow(point.dy - center.dy, 2);
var distance = math.sqrt(dx + dy);
return (distance - radius).abs() < width;
}
double calculateRawAngle({
required double startAngle,
required double angleRange,
required double selectedAngle,
bool counterClockwise = false,
}) {
double angle = radiansToDegrees(selectedAngle);
if (!counterClockwise) {
if (angle >= startAngle && angle <= 360.0) return angle - startAngle;
return 360.0 - startAngle + angle;
}
if (angle <= startAngle) return startAngle - angle;
return 360.0 - angle + startAngle;
}
double calculateAngle({
required double startAngle,
required double angleRange,
required selectedAngle,
required defaultAngle,
bool counterClockwise = false,
}) {
if (selectedAngle == null) return defaultAngle;
double calcAngle = calculateRawAngle(
startAngle: startAngle,
angleRange: angleRange,
selectedAngle: selectedAngle,
counterClockwise: counterClockwise,
);
if (calcAngle - angleRange > (360.0 - angleRange) * 0.5) return 0.0;
if (calcAngle > angleRange) return angleRange;
return calcAngle;
}
bool isAngleWithinRange({
required double startAngle,
required double angleRange,
required touchAngle,
required previousAngle,
bool counterClockwise = false,
}) {
double calcAngle = calculateRawAngle(
startAngle: startAngle,
angleRange: angleRange,
selectedAngle: touchAngle,
counterClockwise: counterClockwise);
return !(calcAngle > angleRange);
}
int valueToDuration(double value, double previous, double min, double max) {
final divider = (max - min) / 100;
return divider != 0 ? (value - previous).abs() ~/ divider * 15 : 0;
}
double valueToPercentage(double value, double min, double max) =>
value / ((max - min) / 100);
double valueToAngle(double value, double min, double max, double angleRange) =>
percentageToAngle(valueToPercentage(value - min, min, max), angleRange);
double percentageToValue(double percentage, double min, double max) =>
((max - min) / 100) * percentage + min;
double percentageToAngle(double percentage, double angleRange) {
final step = angleRange / 100;
if (percentage > 100) return angleRange;
if (percentage < 0) return 0.5;
return percentage * step;
}
double angleToValue(double angle, double min, double max, double angleRange) {
return percentageToValue(angleToPercentage(angle, angleRange), min, max);
}
double angleToPercentage(double angle, double angleRange) {
final step = angleRange / 100;
if (angle > angleRange) return 100;
if (angle < 0.5) return 0;
return angle / step;
}
class _CustomPanGestureRecognizer extends OneSequenceGestureRecognizer {
final Function onPanDown;
final Function onPanUpdate;
final Function onPanEnd;
_CustomPanGestureRecognizer({
required this.onPanDown,
required this.onPanUpdate,
required this.onPanEnd,
});
#override
void addPointer(PointerEvent event) {
if (onPanDown(event.position)) {
startTrackingPointer(event.pointer);
resolve(GestureDisposition.accepted);
} else {
stopTrackingPointer(event.pointer);
}
}
#override
void handleEvent(PointerEvent event) {
if (event is PointerMoveEvent) {
onPanUpdate(event.position);
}
if (event is PointerUpEvent) {
onPanEnd(event.position);
stopTrackingPointer(event.pointer);
}
}
#override
String get debugDescription => 'customPan';
#override
void didStopTrackingLastPointer(int pointer) {}
}
class SliderLabel extends StatelessWidget {
final double value;
final CircularSliderAppearance appearance;
const SliderLabel({Key? key, required this.value, required this.appearance})
: super(key: key);
#override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: builtInfo(appearance),
);
}
List<Widget> builtInfo(CircularSliderAppearance appearance) {
var widgets = <Widget>[];
if (appearance.infoTopLabelText != null) {
widgets.add(Text(
appearance.infoTopLabelText!,
style: appearance.infoTopLabelStyle,
));
}
final modifier = appearance.infoModifier(value);
widgets.add(
Text(modifier, style: appearance.infoMainLabelStyle),
);
if (appearance.infoBottomLabelText != null) {
widgets.add(Text(
appearance.infoBottomLabelText!,
style: appearance.infoBottomLabelStyle,
));
}
return widgets;
}
}
typedef PercentageModifier = String Function(double percentage);
class CircularSliderAppearance {
static const double _defaultSize = 150.0;
static const double _defaultStartAngle = 150.0;
static const double _defaultAngleRange = 240.0;
static const Color _defaultInnerTrackColor = Colors.black;
static const Color _defaultOuterTrackColor = Colors.white;
String _defaultPercentageModifier(double value) =>
'${value.ceil().toInt()} %';
final double size;
final double startAngle;
final double angleRange;
final bool animationEnabled;
final bool spinnerMode;
final bool counterClockwise;
final double animDurationMultiplier;
final int spinnerDuration;
final CustomSliderWidths? customWidths;
final CustomSliderColors? customColors;
final InfoProperties? infoProperties;
double? get _customInnerTrackWidth => customWidths?.innerTrackWidth;
double? get _customOuterTrackWidth => customWidths?.outerTrackWidth;
double? get _customTouchWidth => customWidths?.touchWidth;
double? get _customHandlerSize => customWidths?.handlerSize;
double? get _customHandlerWidth => customWidths?.handlerWidth;
double get innerTrackWidth => _customInnerTrackWidth ?? touchWidth / 4.0;
double get outerTrackWidth => _customOuterTrackWidth ?? touchWidth / 4.0;
double get touchWidth => _customTouchWidth ?? size / 10.0;
double get handlerSize => _customHandlerSize ?? touchWidth / 5.0;
double get handlerWidth => _customHandlerWidth ?? touchWidth / 5.0;
Color? get _customInnerTrackColor => customColors?.innerTrackColor;
Color? get _customOuterTrackColor => customColors?.outerTrackColor;
Color get innerTrackColor =>
_customInnerTrackColor ?? _defaultInnerTrackColor;
Color get outerTrackColor =>
_customOuterTrackColor ?? _defaultOuterTrackColor;
String? get _topLabelText => infoProperties?.topLabelText;
String? get _bottomLabelText => infoProperties?.bottomLabelText;
TextStyle? get _mainLabelStyle => infoProperties?.mainLabelStyle;
TextStyle? get _topLabelStyle => infoProperties?.topLabelStyle;
TextStyle? get _bottomLabelStyle => infoProperties?.bottomLabelStyle;
PercentageModifier? get _modifier => infoProperties?.modifier;
PercentageModifier get infoModifier =>
_modifier ?? _defaultPercentageModifier;
String? get infoTopLabelText => _topLabelText;
String? get infoBottomLabelText => _bottomLabelText;
TextStyle get infoMainLabelStyle =>
_mainLabelStyle ??
TextStyle(
fontWeight: FontWeight.w100,
fontSize: size / 5.0,
color: const Color.fromRGBO(30, 0, 59, 1.0));
TextStyle get infoTopLabelStyle =>
_topLabelStyle ??
TextStyle(
fontWeight: FontWeight.w600,
fontSize: size / 10.0,
color: const Color.fromRGBO(147, 81, 120, 1.0));
TextStyle get infoBottomLabelStyle =>
_bottomLabelStyle ??
TextStyle(
fontWeight: FontWeight.w600,
fontSize: size / 10.0,
color: const Color.fromRGBO(147, 81, 120, 1.0));
const CircularSliderAppearance({
this.customWidths,
this.customColors,
this.size = _defaultSize,
this.startAngle = _defaultStartAngle,
this.angleRange = _defaultAngleRange,
this.infoProperties,
this.animationEnabled = true,
this.counterClockwise = false,
this.spinnerMode = false,
this.spinnerDuration = 1500,
this.animDurationMultiplier = 1.0,
});
}
class CustomSliderWidths {
final double? innerTrackWidth;
final double? outerTrackWidth;
final double? touchWidth;
final double? handlerSize;
final double? handlerWidth;
CustomSliderWidths({
this.innerTrackWidth,
this.outerTrackWidth,
this.touchWidth,
this.handlerSize,
this.handlerWidth,
});
}
class CustomSliderColors {
final Color? innerTrackColor;
final Color? outerTrackColor;
CustomSliderColors({
this.innerTrackColor,
this.outerTrackColor,
});
}
class InfoProperties {
final PercentageModifier? modifier;
final TextStyle? mainLabelStyle;
final TextStyle? topLabelStyle;
final TextStyle? bottomLabelStyle;
final String? topLabelText;
final String? bottomLabelText;
InfoProperties({
this.topLabelText,
this.bottomLabelText,
this.mainLabelStyle,
this.topLabelStyle,
this.bottomLabelStyle,
this.modifier,
});
}
You can use CustomPaint to paint various arcs and slider in your widget,
and then you can use GestureDetector to detect the pan action and slide the slider in the widget according to the pan update (onPanUpdate).
check out the working example of code here.
here is the screenshot of widget I made,
,
here is the code,
class CircularSlider extends StatefulWidget {
final double rotateAngle;
const CircularSlider({super.key, this.rotateAngle = 0});
#override
State<CircularSlider> createState() => _CircularSliderState();
}
class _CircularSliderState extends State<CircularSlider> {
double arcAngle = 0.0;
#override
void initState() {
arcAngle = widget.rotateAngle;
super.initState();
}
inRadiusArea(Offset position) {
final pos = position.translate(200.0, 200.0);
return 100 < pos.distance && 120 > pos.distance;
}
double getRadians(Offset center, Offset coords, double radius) {
var a = coords.dx - center.dx;
if (a > radius) {
a = radius;
} else if (a < -radius) {
a = -radius;
}
return math.acos(a / radius);
}
void _onPanUpdate(DragUpdateDetails details) {
final pos = details.localPosition;
final rad = getRadians(const Offset(200, 200), pos, 200);
if (rad >= 0 && rad <= math.pi) {
setState(() {
arcAngle = rad;
});
}
return;
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onPanUpdate: _onPanUpdate,
behavior: HitTestBehavior.translucent,
child: SizedBox(
height: 400,
width: 400,
child: Column(
children: [
CustomPaint(
painter: MyPainter(
strokeWidth: 20,
color: Colors.white,
radius: 102,
),
),
CustomPaint(
painter: MyPainter(
strokeWidth: 15,
color: Colors.black,
radius: 100,
),
),
CustomPaint(
painter: MyStepsPainter(
strokeWidth: 13,
color: Colors.white,
radius: 102,
),
),
_MySlider(
radius: 118,
arcAngle: arcAngle,
color: Colors.pink,
),
],
),
),
);
}
}
class MyPainter extends CustomPainter {
double startAngle;
double endAngle;
double sweepAngle;
double strokeWidth;
double radius;
Color color;
MyPainter({
this.startAngle = 0.0,
this.endAngle = 0.0,
this.sweepAngle = math.pi,
this.radius = 200,
this.color = Colors.black,
this.strokeWidth = 4.0,
});
#override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final rect = Rect.fromCircle(center: center, radius: radius);
const useCenter = false;
final paint = Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth;
canvas.drawArc(rect, startAngle, sweepAngle, useCenter, paint);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}
class MyStepsPainter extends CustomPainter {
double startAngle;
double sweepAngle;
double stepSweepAngle;
double strokeWidth;
double radius;
int stepNumber;
Color color;
MyStepsPainter({
this.startAngle = 0.0,
this.sweepAngle = math.pi,
this.stepSweepAngle = math.pi / 100,
this.radius = 200,
this.stepNumber = 10,
this.color = Colors.black,
this.strokeWidth = 0.5,
});
#override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final rect = Rect.fromCircle(center: center, radius: radius);
const useCenter = false;
final paint = Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth;
final steps = 2 * stepNumber + 2;
final sweepInterval = 2 * math.pi / steps;
final halfStepSweepAngle = stepSweepAngle / 2;
var angle = 0.0;
var firstHalf = angle - halfStepSweepAngle;
var secondHalf = angle + halfStepSweepAngle;
while (angle < math.pi) {
final startA = firstHalf > 0 ? firstHalf : angle;
final endA = secondHalf <= math.pi ? secondHalf : angle;
canvas.drawArc(rect, startA, endA - startA, useCenter, paint);
angle += sweepInterval;
firstHalf = angle - halfStepSweepAngle;
secondHalf = angle + halfStepSweepAngle;
}
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}
class _MySlider extends StatelessWidget {
final Color color;
final double radius;
final double arcAngle;
const _MySlider({
this.color = Colors.pink,
this.radius = 210.0,
this.arcAngle = 0.0,
});
#override
Widget build(BuildContext context) {
var sinT = math.sin(arcAngle);
var cosT = math.cos(arcAngle);
final center = Offset(cosT * radius, sinT * radius);
print(sinT);
print(cosT);
print(arcAngle);
return Transform.rotate(
origin: center,
angle: arcAngle,
child: CustomPaint(
painter: MySlider(
radius: radius,
arcAngle: arcAngle,
color: color,
),
),
);
}
}
class MySlider extends CustomPainter {
final Color color;
final double radius;
final double arcAngle;
MySlider({
this.color = Colors.pink,
this.radius = 210.0,
this.arcAngle = 0.0,
});
#override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..style = PaintingStyle.fill
..strokeWidth = 1;
var sinT = math.sin(arcAngle);
var cosT = math.cos(arcAngle);
var path = Path();
path.moveTo(cosT * radius - 20, sinT * radius);
path.lineTo(cosT * radius, sinT * radius - 20);
path.lineTo(cosT * radius, sinT * radius + 20);
path.lineTo(cosT * radius - 20, sinT * radius);
path.moveTo(cosT * radius, sinT * radius);
final center = Offset(cosT * radius, sinT * radius);
final rect = Rect.fromCircle(center: center, radius: 20.0);
path.addArc(rect, -math.pi / 2, math.pi);
canvas.drawPath(path, paint);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}

Flutter circular progress indicator - repeatable-Custom painter

I want to create something like that:
Want to achieve
I have achieved this:
Done up until now
I am struggling to add just vertical line at state of this circular progress bar just like the line at trailing.
import 'dart:math';
import 'package:flutter/material.dart';
class LoaderPaint extends CustomPainter {
final double percentage;
LoaderPaint({
required this.percentage,
});
deg2Rand(double deg) => deg * pi / 180;
#override
void paint(Canvas canvas, Size size) {
final midOffset = Offset(size.width / 2, size.height / 2);
final paint = Paint()
..strokeCap = StrokeCap.round
..color = Colors.white
..style = PaintingStyle.stroke
..strokeWidth = 2;
canvas.drawLine(
Offset(midOffset.dy, 10),
Offset(midOffset.dy,-10),
paint,
);
canvas.drawArc(
Rect.fromCenter(center: midOffset, width: size.width, height: size.height),
deg2Rand(-90),
deg2Rand(360 * percentage),
false,
paint,
);
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
Calculate the end position of the line by using the formula found in this stackoverflow answer. Since we start the angle at -90 degrees we must take that away from the sweepAngle.
Calculate the points 10 units before and after the end position of the line using the formula in this answer, plugging in the center of the circle and the end position of the line.
Draw the line with canvas.drawLine
Here is what your updated LoaderPaint class looks like with these changes:
class LoaderPaint extends CustomPainter {
final double percentage;
const LoaderPaint({
required this.percentage,
});
deg2Rand(double deg) => deg * pi / 180;
#override
void paint(Canvas canvas, Size size) {
final radius = size.width / 2;
final sweepAngle = deg2Rand(360 * percentage);
final theta = deg2Rand(-90) + sweepAngle;
final midOffset = Offset(radius, radius);
final endOffset = Offset(radius + radius * cos(theta), radius + radius * sin(theta));
final midEndDiff = sqrt(pow(endOffset.dx - midOffset.dx, 2) + pow(endOffset.dy - midOffset.dy, 2));
final paint = Paint()
..strokeCap = StrokeCap.round
..color = Colors.white
..style = PaintingStyle.stroke
..strokeWidth = 2;
canvas.drawLine(
Offset(midOffset.dy, 10),
Offset(midOffset.dy,-10),
paint,
);
canvas.drawArc(
Rect.fromCenter(center: midOffset, width: size.width, height: size.height),
deg2Rand(-90),
sweepAngle,
false,
paint,
);
canvas.drawLine(
Offset(endOffset.dx + (10/midEndDiff) * (endOffset.dx - midOffset.dx), endOffset.dy + (10/midEndDiff) * (endOffset.dy - midOffset.dy)),
Offset(endOffset.dx - (10/midEndDiff) * (endOffset.dx - midOffset.dx), endOffset.dy - (10/midEndDiff) * (endOffset.dy - midOffset.dy)),
paint,
);
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
This dartpad shows it working. You can easily change the percentage to see it work at all angles.
You must bear in mind this will only work if the width and height of the CustomPaint widget are exactly the same, however your example without the end cap would also break if the width and height were different.

Make a non-rectangular area draggable

To make a widget draggable, it has to be passed to a Draggable like so:
Draggable<String>(
data: "SomeDateToBeDragged",
child: Container(
width: 300,
height: 200,
alignment: Alignment.center,
color: Colors.purple,
child: Image.network(
'https://someServer.com/dog.jpg',
fit: BoxFit.cover,
),
),
...
As far as I know, a Widget is something rectangular. At least, I found nothing else than rectangular widgets.
I'd like to build a sketching app. Thus, I'd like to make a 'two dimensional' connector [a Line between two points] draggable.
How to I make a line draggable?
In other words: I'd like to make a click initiate a drag only if the drag e.g. is on a painted area and not on transparent background of the area. If I would draw a circle, it would drag if the circle would be clicked. If clicked some pixel outside the circle, it should not start a drag.
Flutter is incredible in making things that appear complex really easy.
Here is a solution for a draggable segment of line with two handles for its extremities.
As you see, you can grab either the line itself or its extremities.
A quick overview of the solution:
MyApp is the MaterialApp with the Scaffold
CustomPainterDraggable is the main Widget with the State Management
I used Hooks Riverpod
Have a look how to use freezed
LinePainter a basic Painter to paint the line and the two handles
Utils, a Utility class to calculate the distance between the cursor and the line or handles. The distance defines which part of the drawing should be dragged around.
Part, a Union class defining the different elements of the drawing (line and handles) to better structure the code.
1. Material App
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/all.dart';
part '66070975.sketch_app.freezed.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ProviderScope(
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Draggable Custom Painter',
home: Scaffold(
body: CustomPainterDraggable(),
),
),
);
}
}
2. Part Union
#freezed
abstract class Part with _$Part {
const factory Part.line() = _Line;
const factory Part.a() = _A;
const factory Part.b() = _B;
const factory Part.noPart() = _NoPart;
}
3. CustomPainterDraggable
class CustomPainterDraggable extends HookWidget {
#override
Widget build(BuildContext context) {
final a = useState(Offset(50, 50));
final b = useState(Offset(150, 200));
final dragging = useState(Part.noPart());
return GestureDetector(
onPanStart: (details) => dragging.value =
Utils.shouldGrab(a.value, b.value, details.globalPosition),
onPanEnd: (details) {
dragging.value = Part.noPart();
},
onPanUpdate: (details) {
dragging.value.when(
line: () {
a.value += details.delta;
b.value += details.delta;
},
a: () => a.value += details.delta,
b: () => b.value += details.delta,
noPart: () {},
);
},
child: Container(
color: Colors.white,
child: CustomPaint(
painter: LinePainter(a: a.value, b: b.value),
child: Container(),
),
),
);
}
}
4. LinePainter
class LinePainter extends CustomPainter {
final Offset a;
final Offset b;
final double lineWidth;
final Color lineColor;
final double pointWidth;
final double pointSize;
final Color pointColor;
Paint get linePaint => Paint()
..color = lineColor
..strokeWidth = lineWidth
..style = PaintingStyle.stroke;
Paint get pointPaint => Paint()
..color = pointColor
..strokeWidth = pointWidth
..style = PaintingStyle.stroke;
LinePainter({
this.a,
this.b,
this.lineWidth = 5,
this.lineColor = Colors.black54,
this.pointWidth = 3,
this.pointSize = 12,
this.pointColor = Colors.red,
});
#override
void paint(Canvas canvas, Size size) {
canvas.drawLine(a, b, linePaint);
canvas.drawRect(
Rect.fromCenter(center: a, width: pointSize, height: pointSize),
pointPaint,
);
canvas.drawRect(
Rect.fromCenter(center: b, width: pointSize, height: pointSize),
pointPaint,
);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
5. Utils
class Utils {
static double maxDistance = 20;
static Part shouldGrab(Offset a, Offset b, Offset target) {
if ((a - target).distance < maxDistance) {
return Part.a();
}
if ((b - target).distance < maxDistance) {
return Part.b();
}
if (shortestDistance(a, b, target) < maxDistance) {
return Part.line();
}
return Part.noPart();
}
static double shortestDistance(Offset a, Offset b, Offset target) {
double px = b.dx - a.dx;
double py = b.dy - a.dy;
double temp = (px * px) + (py * py);
double u = ((target.dx - a.dx) * px + (target.dy - a.dy) * py) / temp;
if (u > 1) {
u = 1;
} else if (u < 0) {
u = 0;
}
double x = a.dx + u * px;
double y = a.dy + u * py;
double dx = x - target.dx;
double dy = y - target.dy;
double dist = math.sqrt(dx * dx + dy * dy);
return dist;
}
}
Full Source Code (Easy to copy paste)
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/all.dart';
part '66070975.sketch_app.freezed.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ProviderScope(
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Draggable Custom Painter',
home: Scaffold(
body: CustomPainterDraggable(),
),
),
);
}
}
#freezed
abstract class Part with _$Part {
const factory Part.line() = _Line;
const factory Part.a() = _A;
const factory Part.b() = _B;
const factory Part.noPart() = _NoPart;
}
class CustomPainterDraggable extends HookWidget {
#override
Widget build(BuildContext context) {
final a = useState(Offset(50, 50));
final b = useState(Offset(150, 200));
final dragging = useState(Part.noPart());
return GestureDetector(
onPanStart: (details) => dragging.value =
Utils.shouldGrab(a.value, b.value, details.globalPosition),
onPanEnd: (details) {
dragging.value = Part.noPart();
},
onPanUpdate: (details) {
dragging.value.when(
line: () {
a.value += details.delta;
b.value += details.delta;
},
a: () => a.value += details.delta,
b: () => b.value += details.delta,
noPart: () {},
);
},
child: Container(
color: Colors.white,
child: CustomPaint(
painter: LinePainter(a: a.value, b: b.value),
child: Container(),
),
),
);
}
}
class LinePainter extends CustomPainter {
final Offset a;
final Offset b;
final double lineWidth;
final Color lineColor;
final double pointWidth;
final double pointSize;
final Color pointColor;
Paint get linePaint => Paint()
..color = lineColor
..strokeWidth = lineWidth
..style = PaintingStyle.stroke;
Paint get pointPaint => Paint()
..color = pointColor
..strokeWidth = pointWidth
..style = PaintingStyle.stroke;
LinePainter({
this.a,
this.b,
this.lineWidth = 5,
this.lineColor = Colors.black54,
this.pointWidth = 3,
this.pointSize = 12,
this.pointColor = Colors.red,
});
#override
void paint(Canvas canvas, Size size) {
canvas.drawLine(a, b, linePaint);
canvas.drawRect(
Rect.fromCenter(center: a, width: pointSize, height: pointSize),
pointPaint,
);
canvas.drawRect(
Rect.fromCenter(center: b, width: pointSize, height: pointSize),
pointPaint,
);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
class Utils {
static double maxDistance = 20;
static Part shouldGrab(Offset a, Offset b, Offset target) {
if ((a - target).distance < maxDistance) {
return Part.a();
}
if ((b - target).distance < maxDistance) {
return Part.b();
}
if (shortestDistance(a, b, target) < maxDistance) {
return Part.line();
}
return Part.noPart();
}
static double shortestDistance(Offset a, Offset b, Offset target) {
double px = b.dx - a.dx;
double py = b.dy - a.dy;
double temp = (px * px) + (py * py);
double u = ((target.dx - a.dx) * px + (target.dy - a.dy) * py) / temp;
if (u > 1) {
u = 1;
} else if (u < 0) {
u = 0;
}
double x = a.dx + u * px;
double y = a.dy + u * py;
double dx = x - target.dx;
double dy = y - target.dy;
double dist = math.sqrt(dx * dx + dy * dy);
return dist;
}
}
As a Container creates a Renderbox, the resulting draggable area will always be a rectangle.
You can create a line using a CustomPainter. However, the line created by this will by itself not be draggable. If you wrap it with a Container, the line will be draggable. But the area that is draggable is then again determined by the size of the container.
I'd suggest you use a Canvas and keep track of the state of your lines by yourself, like in this thread: How to draw custom shape in flutter and drag that shape around?