Related
I have to make a QR code screen, I need to work on this screen with the focus of Qr reading in the middle and its surroundings blurred.
I want to make a focus point in the middle of the background blur on the QR code reading screen, I wrote paint for this, but I could not reach a healthy result. The only thing missing from ShapeBorder is that the background is blurred. How can I add this?
class QrScannerOverlayShape extends ShapeBorder {
QrScannerOverlayShape({
this.borderColor = Colors.red,
this.borderWidth = 3.0,
this.overlayColor = const Color.fromRGBO(0, 0, 0, 80),
this.borderRadius = 0,
this.borderLength = 40,
double? cutOutSize,
double? cutOutWidth,
double? cutOutHeight,
this.cutOutBottomOffset = 0,
}) : cutOutWidth = cutOutWidth ?? cutOutSize ?? 250,
cutOutHeight = cutOutHeight ?? cutOutSize ?? 250 {
assert(
borderLength <=
min(this.cutOutWidth, this.cutOutHeight) / 2 + borderWidth * 2,
"Border can't be larger than ${min(this.cutOutWidth, this.cutOutHeight) / 2 + borderWidth * 2}",
);
assert(
(cutOutWidth == null && cutOutHeight == null) ||
(cutOutSize == null && cutOutWidth != null && cutOutHeight != null),
'Use only cutOutWidth and cutOutHeight or only cutOutSize');
}
final Color borderColor;
final double borderWidth;
final Color overlayColor;
final double borderRadius;
final double borderLength;
final double cutOutWidth;
final double cutOutHeight;
final double cutOutBottomOffset;
#override
EdgeInsetsGeometry get dimensions => const EdgeInsets.all(10);
#override
Path getInnerPath(Rect rect, {TextDirection? textDirection}) {
return Path()
..fillType = PathFillType.evenOdd
..addPath(getOuterPath(rect), Offset.zero);
}
#override
Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
Path getLeftTopPath(Rect rect) {
return Path()
..moveTo(rect.left, rect.bottom)
..lineTo(rect.left, rect.top)
..lineTo(rect.right, rect.top);
}
return getLeftTopPath(rect)
..lineTo(
rect.right,
rect.bottom,
)
..lineTo(
rect.left,
rect.bottom,
)
..lineTo(
rect.left,
rect.top,
);
}
#override
void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {
final width = rect.width;
final borderWidthSize = width / 2;
final height = rect.height;
final borderOffset = borderWidth / 2;
final mBorderLength =
borderLength > min(cutOutHeight, cutOutHeight) / 2 + borderWidth * 2
? borderWidthSize / 2
: borderLength;
final mCutOutWidth =
cutOutWidth < width ? cutOutWidth : width - borderOffset;
final mCutOutHeight =
cutOutHeight < height ? cutOutHeight : height - borderOffset;
final backgroundPaint = Paint()
..color = overlayColor
..style = PaintingStyle.fill;
final borderPaint = Paint()
..color = borderColor
..style = PaintingStyle.stroke
..strokeWidth = borderWidth;
final boxPaint = Paint()
..color = borderColor
..style = PaintingStyle.fill
..blendMode = BlendMode.dstOut;
final cutOutRect = Rect.fromLTWH(
rect.left + width / 2 - mCutOutWidth / 2 + borderOffset,
-cutOutBottomOffset +
rect.top +
height / 2 -
mCutOutHeight / 2 +
borderOffset,
mCutOutWidth - borderOffset * 2,
mCutOutHeight - borderOffset * 2,
);
canvas
..saveLayer(rect, backgroundPaint)
..drawRect(rect, backgroundPaint)
// Draw top right corner
..drawRRect(
RRect.fromLTRBAndCorners(
cutOutRect.right - mBorderLength,
cutOutRect.top,
cutOutRect.right,
cutOutRect.top + mBorderLength,
topRight: Radius.circular(borderRadius),
),
borderPaint,
)
// Draw top left corner
..drawRRect(
RRect.fromLTRBAndCorners(
cutOutRect.left,
cutOutRect.top,
cutOutRect.left + mBorderLength,
cutOutRect.top + mBorderLength,
topLeft: Radius.circular(borderRadius),
),
borderPaint,
)
// Draw bottom right corner
..drawRRect(
RRect.fromLTRBAndCorners(
cutOutRect.right - mBorderLength,
cutOutRect.bottom - mBorderLength,
cutOutRect.right,
cutOutRect.bottom,
bottomRight: Radius.circular(borderRadius),
),
borderPaint,
)
// Draw bottom left corner
..drawRRect(
RRect.fromLTRBAndCorners(
cutOutRect.left,
cutOutRect.bottom - mBorderLength,
cutOutRect.left + mBorderLength,
cutOutRect.bottom,
bottomLeft: Radius.circular(borderRadius),
),
borderPaint,
)
..drawRRect(
RRect.fromRectAndRadius(
cutOutRect,
Radius.circular(borderRadius),
),
boxPaint,
)
..restore();
}
#override
ShapeBorder scale(double t) {
return QrScannerOverlayShape(
borderColor: borderColor,
borderWidth: borderWidth,
overlayColor: overlayColor,
);
}
}
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.
I want to make a data wheel.
This is what it looks like :
My problem:
I want all the text that is between 181 degrees and 360 degrees, i.e. the left part of the circle, to be flipped 180 degrees so that the reading direction is easier.
My code:
class CanvasPainter extends CustomPainter
{
List<AreaEntity> areas;
CanvasPainter(this.areas)
: dotsPerRing = areas.length;
final int dotsPerRing;
final double dotRadius = 6;
#override
void paint(Canvas canvas, Size size) {
// General variable
final Offset ringCenter = size.center(Offset.zero);
final double centerCircleRadius = size.width / 6;
final double deltaAngle = 2 * pi / dotsPerRing;
// Main Circle
_drawBigCircle(
canvas: canvas,
offset: ringCenter,
radius: centerCircleRadius,
);
_drawDots(
canvas: canvas,
offset: ringCenter,
radius: centerCircleRadius,
deltaAngle: deltaAngle
);
_drawTextDot(
canvas: canvas,
size: size,
offset: ringCenter,
radius: centerCircleRadius,
deltaAngle: deltaAngle,
areas: areas
);
}
void _drawCenterCircle({
required Canvas canvas,
required Offset offset,
required double radius
}){
Paint outCirclePaint = Paint()
..color = AppColors.kcolor_bleu
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke
..strokeWidth = 2;
canvas.drawCircle(offset, 20, outCirclePaint);
Paint inCirclePaint = Paint()
..color = AppColors.kcolor_bleu
..strokeCap = StrokeCap.round;
canvas.drawCircle(offset, 5, inCirclePaint);
}
void _drawBigCircle({
required Canvas canvas,
required Offset offset,
required double radius
}){
Paint defaultCirclePaint = Paint()
..color = AppColors.kBg_light
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke
..strokeWidth = 1;
canvas.drawCircle(offset, radius, defaultCirclePaint);
}
void _drawDots({
required Canvas canvas,
required Offset offset,
required double radius,
required double deltaAngle
}){
// Dot
final Paint dotBackground = Paint()
..color = AppColors.kBg_normal
..strokeCap = StrokeCap.round;
final Paint dotPaint = Paint()
..color = AppColors.kFont_grey
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke
..strokeWidth = 3;
// Draw a single ring
Path dotCirclePath = Path();
for(int i = 0; i < dotsPerRing; i++){
dotCirclePath.addOval(
Rect.fromCircle(
center: offset + _rotateVector(
Offset(0, radius),
deltaAngle * i
),
radius: dotRadius
)
);
}
canvas.drawPath(dotCirclePath, dotBackground);
canvas.drawPath(dotCirclePath, dotPaint);
}
void _drawTextDot({
required Canvas canvas,
required Size size,
required Offset offset,
required double radius,
required double deltaAngle,
required List<AreaEntity> areas
}){
areas.forEach((m) {
double w3 = deltaAngle * m.id;
drawTextSlant(canvas, offset, radius, w3, m.text, TextStyle(
color: Colors.white,
fontSize: 8.0,
fontWeight: FontWeight.bold,
));
});
}
void drawTextSlant(
Canvas canvas,
Offset arcCenter,
double radius,
double w,
String text,
TextStyle style
){
final pos = Offset(radius, 0);
canvas.save();
canvas.translate(arcCenter.dx, arcCenter.dy);
canvas.rotate(w - degreeToRadian(90));
if(radianToDegree(w) > 180){
// => how to reverse text ??? -------------------------------------------------
}
final tp = measureText(canvas, text, style);
final ww = tp.height;
tp.paint(canvas, pos + Offset(15 , -ww / 2.0));
canvas.restore();
}
TextPainter measureText(Canvas canvas, String text, TextStyle style)
{
final textSpan = TextSpan(text: text, style: style);
final textPainter = TextPainter(text: textSpan, textDirection: TextDirection.ltr);
textPainter.layout(minWidth: 0, maxWidth: double.maxFinite);
return textPainter;
}
Offset _rotateVector(Offset vector, double angleInRadians)
{
return Offset(
(cos(angleInRadians) * vector.dx) - (sin(angleInRadians) * vector.dy),
(sin(angleInRadians) * vector.dx) - (cos(angleInRadians) * vector.dy)
);
}
double degreeToRadian(double degree) {
return degree * pi / 180;
}
double radianToDegree(double radian) {
return radian * 180 / pi;
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
I'm new to canvas so if you have any suggestions to improve my code I'll gladly accept the advice
Any guidance on the best way to accomplish this would be appreciated.
EDIT : I succeeded I edit the code
void _drawTextDot({
required Canvas canvas,
required Size size,
required Offset offset,
required double radius,
required double deltaAngle,
required List<PaxsAreaEntity> areas
}){
areas.forEach((m) {
double w3 = deltaAngle * m.id;
drawTextSlant(canvas, offset, radius, w3, m.text, TextStyle(
color: AppColors.kFont_grey,
fontSize: 6.0,
fontWeight: FontWeight.bold,
), size);
});
}
void drawTextSlant(
Canvas canvas,
Offset arcCenter,
double radius,
double w,
String text,
TextStyle style,
Size size
){
double distanceFromDot = 20;
final pos = Offset(0, 0);
canvas.save();
canvas.translate(size.width / 2, size.height / 2);
canvas.rotate(degreeToRadian(-90));
canvas.translate(radius * cos(w), radius * sin(w));
canvas.rotate(w);
final tp = measureText(canvas, text, style);
final ww = tp.height;
if(radianToDegree(w) > 180){
canvas.rotate(degreeToRadian(180));
canvas.translate(-tp.width, 0);
distanceFromDot = distanceFromDot * -1;
}
tp.paint(canvas, pos + Offset(distanceFromDot , -ww / 2.0));
canvas.restore();
}
Im trying to build kind of circular squared progress bar in dart/flutter.
maybe someone knows how to make it possible? iv'e tried all, paint, border and more, but without success.
example image - circular squared progress bar
You can try square_percent_indicater package here.
If you want a more customized widget, try CustomPainter:
class _MyBorderPainter extends CustomPainter {
_MyBorderPainter(
{required this.progress, required this.borderColor, this.strokeWdth});
double progress; // desirable value for corners side
double? strokeWdth;
Color borderColor;
#override
void paint(Canvas canvas, Size size) {
double x = min(size.height, size.width);
double x2 = x / 2;
double x4 = x / 4;
Paint paintt = new Paint()
..color = progress == 0 ? Colors.transparent : borderColor
..style = PaintingStyle.stroke
..strokeWidth = strokeWdth ?? 4.0;
// ..strokeCap = StrokeCap.round;
Path path = Path()
..moveTo(x2, x)
..lineTo(x4, x)
..quadraticBezierTo(0, x, 0, x - x4)
..lineTo(0, x4)
..quadraticBezierTo(0, 0, x4, 0)
..lineTo(x - x4, 0)
..quadraticBezierTo(x, 0, x, x4)
..lineTo(x, x - x4)
..quadraticBezierTo(x, x, x - x4, x)
..lineTo(x2, x);
PathMetric pathMetric = path.computeMetrics().first;
Path extractPath = pathMetric.extractPath(
pathMetric.length * (0.5 * (1 - progress)),
pathMetric.length * (0.5 + 0.5 * progress));
canvas.drawPath(extractPath, paintt);
}
#override
bool shouldRepaint(covariant _MyBorderPainter oldDelegate) {
return oldDelegate.progress != progress;
}
}
class SquareProgressBar extends StatelessWidget {
const SquareProgressBar(
{required this.strokeWdth,
required this.borderColor,
this.borderBgColor,
required this.progress,
this.child,
Key? key})
: super(key: key);
final double strokeWdth;
final Color borderColor;
final Color? borderBgColor;
final double progress;
final Widget? child;
#override
Widget build(BuildContext context) {
return CustomPaint(
painter: _MyBorderPainter(
borderColor: borderBgColor ?? Colors.grey,
strokeWdth: strokeWdth,
progress: 1.0),
child: CustomPaint(
painter: _MyBorderPainter(
progress: this.progress,
borderColor: borderColor,
strokeWdth: strokeWdth,
),
child: this.child ?? null,
));
}
}
which results in:
I'm using the basic slider, and I found how to update just the parts of the slider theme data that I want to change like trackHeight, but unfortunately I'm not sure how to update the field for "trackShape". Here is what I do in the main app to update track height for example:
final SliderThemeData tweakedSliderTheme = Theme.of(context).sliderTheme.copyWith(
trackHeight: 22.0,
//trackShape: RectangularSliderTrackShape(), // How do I update this??
);
I did try using ClipRRect() around the slider widget and that had no effect.
Here is a simple page for one slider:
import 'package:flutter/material.dart';
class RoomControl extends StatefulWidget {
#override
_RoomControlState createState() => _RoomControlState();
}
class _RoomControlState extends State<RoomControl> {
double _value = 0.0;
void _setvalue(double value) => setState(() => _value = value);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Name here'),
),
//hit Ctrl+space in intellij to know what are the options you can use in flutter widgets
body: Container(
padding: EdgeInsets.all(32.0),
child: Center(
child: Column(
children: <Widget>[
Text('Value: ${(_value * 100).round()}'),
ClipRRect(
borderRadius: BorderRadius.circular(5.0),
child:Slider(
value: _value,
onChanged: _setvalue,
divisions: 10,
)
)
],
),
),
),
);
}
}
Here is what that slider looks like:
UPDATE:
After getting the answer, I was able to create something like this easily by updating the tick mark shape, and the thumb shape:
final SliderThemeData tweakedSliderTheme = Theme.of(context).sliderTheme.copyWith(
trackHeight: 20.0,
thumbShape: MyRoundSliderThumbShape(enabledThumbRadius: 13.0, disabledThumbRadius: 13.0),
trackShape: MyRoundSliderTrackShape(), // TODO: this is hard coded right now for 20 track height
inactiveTrackColor: lightGreySliderColor,
activeTickMarkColor: Color(blackPrimaryValue),
inactiveTickMarkColor: colorizedMenuColor,
tickMarkShape: MyRectSliderTickMarkShape(tickMarkRadius: 4.0),
);
There was a bit of a trick to the tick mark shape. If you make it too big it just skips painting it! Probably makes sense but I didn't know much about paint/rendering so it took me a while to learn how to get the tick marks (Rect) to show up properly
I've copied the base code from RectangularSliderTrackShape into a new class called RoundSliderTrackShape.
round_slider_track_shape.dart
import 'dart:math';
import 'package:flutter/material.dart';
class RoundSliderTrackShape extends SliderTrackShape {
/// Create a slider track that draws 2 rectangles.
const RoundSliderTrackShape({ this.disabledThumbGapWidth = 2.0 });
/// Horizontal spacing, or gap, between the disabled thumb and the track.
///
/// This is only used when the slider is disabled. There is no gap around
/// the thumb and any part of the track when the slider is enabled. The
/// Material spec defaults this gap width 2, which is half of the disabled
/// thumb radius.
final double disabledThumbGapWidth;
#override
Rect getPreferredRect({
RenderBox parentBox,
Offset offset = Offset.zero,
SliderThemeData sliderTheme,
bool isEnabled,
bool isDiscrete,
}) {
final double overlayWidth = sliderTheme.overlayShape.getPreferredSize(isEnabled, isDiscrete).width;
final double trackHeight = sliderTheme.trackHeight;
assert(overlayWidth >= 0);
assert(trackHeight >= 0);
assert(parentBox.size.width >= overlayWidth);
assert(parentBox.size.height >= trackHeight);
final double trackLeft = offset.dx + overlayWidth / 2;
final double trackTop = offset.dy + (parentBox.size.height - trackHeight) / 2;
// TODO(clocksmith): Although this works for a material, perhaps the default
// rectangular track should be padded not just by the overlay, but by the
// max of the thumb and the overlay, in case there is no overlay.
final double trackWidth = parentBox.size.width - overlayWidth;
return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
}
#override
void paint(
PaintingContext context,
Offset offset, {
RenderBox parentBox,
SliderThemeData sliderTheme,
Animation<double> enableAnimation,
TextDirection textDirection,
Offset thumbCenter,
bool isDiscrete,
bool isEnabled,
}) {
// If the slider track height is 0, then it makes no difference whether the
// track is painted or not, therefore the painting can be a no-op.
if (sliderTheme.trackHeight == 0) {
return;
}
// Assign the track segment paints, which are left: active, right: inactive,
// but reversed for right to left text.
final ColorTween activeTrackColorTween = ColorTween(begin: sliderTheme.disabledActiveTrackColor , end: sliderTheme.activeTrackColor);
final ColorTween inactiveTrackColorTween = ColorTween(begin: sliderTheme.disabledInactiveTrackColor , end: sliderTheme.inactiveTrackColor);
final Paint activePaint = Paint()..color = activeTrackColorTween.evaluate(enableAnimation);
final Paint inactivePaint = Paint()..color = inactiveTrackColorTween.evaluate(enableAnimation);
Paint leftTrackPaint;
Paint rightTrackPaint;
switch (textDirection) {
case TextDirection.ltr:
leftTrackPaint = activePaint;
rightTrackPaint = inactivePaint;
break;
case TextDirection.rtl:
leftTrackPaint = inactivePaint;
rightTrackPaint = activePaint;
break;
}
// Used to create a gap around the thumb iff the slider is disabled.
// If the slider is enabled, the track can be drawn beneath the thumb
// without a gap. But when the slider is disabled, the track is shortened
// and this gap helps determine how much shorter it should be.
// TODO(clocksmith): The new Material spec has a gray circle in place of this gap.
double horizontalAdjustment = 0.0;
if (!isEnabled) {
final double disabledThumbRadius = sliderTheme.thumbShape.getPreferredSize(false, isDiscrete).width / 2.0;
final double gap = disabledThumbGapWidth * (1.0 - enableAnimation.value);
horizontalAdjustment = disabledThumbRadius + gap;
}
final Rect trackRect = getPreferredRect(
parentBox: parentBox,
offset: offset,
sliderTheme: sliderTheme,
isEnabled: isEnabled,
isDiscrete: isDiscrete,
);
final Rect leftTrackSegment = Rect.fromLTRB(trackRect.left, trackRect.top, thumbCenter.dx - horizontalAdjustment, trackRect.bottom);
// Left Arc
context.canvas.drawArc(
Rect.fromCircle(center: Offset(trackRect.left, trackRect.top + 11.0), radius: 11.0),
-pi * 3 / 2, // -270 degrees
pi, // 180 degrees
false,
trackRect.left - thumbCenter.dx == 0.0 ? rightTrackPaint : leftTrackPaint
);
// Right Arc
context.canvas.drawArc(
Rect.fromCircle(center: Offset(trackRect.right, trackRect.top + 11.0), radius: 11.0),
-pi / 2, // -90 degrees
pi, // 180 degrees
false,
trackRect.right - thumbCenter.dx == 0.0 ? leftTrackPaint : rightTrackPaint
);
context.canvas.drawRect(leftTrackSegment, leftTrackPaint);
final Rect rightTrackSegment = Rect.fromLTRB(thumbCenter.dx + horizontalAdjustment, trackRect.top, trackRect.right, trackRect.bottom);
context.canvas.drawRect(rightTrackSegment, rightTrackPaint);
}
}
The following is how the SliderTheme is setup.
import 'package:flutter_stackoverflow/round_slider_track_shape.dart';
...
sliderTheme: Theme.of(context).sliderTheme.copyWith(
trackHeight: 22.0,
trackShape: RoundSliderTrackShape(),
activeTrackColor: Colors.green,
// trackShape: RectangularSliderTrackShape(),
),
What was added was two circle arcs to the side of the SliderTrack
// Left Arc
context.canvas.drawArc(
Rect.fromCircle(center: Offset(trackRect.left, trackRect.top + 11.0), radius: 11.0),
-pi * 3 / 2, // -270 degrees
pi, // 180 degrees
false,
trackRect.left - thumbCenter.dx == 0.0 ? rightTrackPaint : leftTrackPaint
);
// Right Arc
context.canvas.drawArc(
Rect.fromCircle(center: Offset(trackRect.right, trackRect.top + 11.0), radius: 11.0),
-pi / 2, // -90 degrees
pi, // 180 degrees
false,
trackRect.right - thumbCenter.dx == 0.0 ? leftTrackPaint : rightTrackPaint
);
Answer by #Jun Xiang seems to work but I did a minor change in the following code:
// Left Arc
context.canvas.drawArc(
Rect.fromCircle(center: Offset(trackRect.left, trackRect.top + 11.0), radius: 11.0),
-pi * 3 / 2, // -270 degrees
pi, // 180 degrees
false,
trackRect.left - thumbCenter.dx == 0.0 ? rightTrackPaint : leftTrackPaint
);
// Right Arc
context.canvas.drawArc(
Rect.fromCircle(center: Offset(trackRect.right, trackRect.top + 11.0), radius: 11.0),
-pi / 2, // -90 degrees
pi, // 180 degrees
false,
trackRect.right - thumbCenter.dx == 0.0 ? leftTrackPaint : rightTrackPaint
);
Instead of using 11.0 I used sliderTheme.trackHeight * 1/2 and seems to work for all track heights entered (not only 22).
PS: I couldn't comment so I posted an answer.
I have a better solution
import 'package:flutter/material.dart';
class RoundSliderTrackShape extends SliderTrackShape {
const RoundSliderTrackShape({this.disabledThumbGapWidth = 2.0, this.radius = 0});
final double disabledThumbGapWidth;
final double radius;
#override
Rect getPreferredRect({
RenderBox parentBox,
Offset offset = Offset.zero,
SliderThemeData sliderTheme,
bool isEnabled,
bool isDiscrete,
}) {
final double overlayWidth = sliderTheme.overlayShape.getPreferredSize(isEnabled, isDiscrete).width;
final double trackHeight = sliderTheme.trackHeight;
assert(overlayWidth >= 0);
assert(trackHeight >= 0);
assert(parentBox.size.width >= overlayWidth);
assert(parentBox.size.height >= trackHeight);
final double trackLeft = offset.dx + overlayWidth / 2;
final double trackTop = offset.dy + (parentBox.size.height - trackHeight) / 2;
final double trackWidth = parentBox.size.width - overlayWidth;
return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
}
#override
void paint(
PaintingContext context,
Offset offset, {
RenderBox parentBox,
SliderThemeData sliderTheme,
Animation<double> enableAnimation,
TextDirection textDirection,
Offset thumbCenter,
bool isDiscrete,
bool isEnabled,
}) {
if (sliderTheme.trackHeight == 0) {
return;
}
final ColorTween activeTrackColorTween =
ColorTween(begin: sliderTheme.disabledActiveTrackColor, end: sliderTheme.activeTrackColor);
final ColorTween inactiveTrackColorTween =
ColorTween(begin: sliderTheme.disabledInactiveTrackColor, end: sliderTheme.inactiveTrackColor);
final Paint activePaint = Paint()..color = activeTrackColorTween.evaluate(enableAnimation);
final Paint inactivePaint = Paint()..color = inactiveTrackColorTween.evaluate(enableAnimation);
Paint leftTrackPaint;
Paint rightTrackPaint;
switch (textDirection) {
case TextDirection.ltr:
leftTrackPaint = activePaint;
rightTrackPaint = inactivePaint;
break;
case TextDirection.rtl:
leftTrackPaint = inactivePaint;
rightTrackPaint = activePaint;
break;
}
double horizontalAdjustment = 0.0;
if (!isEnabled) {
final double disabledThumbRadius =
sliderTheme.thumbShape.getPreferredSize(false, isDiscrete).width / 2.0;
final double gap = disabledThumbGapWidth * (1.0 - enableAnimation.value);
horizontalAdjustment = disabledThumbRadius + gap;
}
final Rect trackRect = getPreferredRect(
parentBox: parentBox,
offset: offset,
sliderTheme: sliderTheme,
isEnabled: isEnabled,
isDiscrete: isDiscrete,
);
//Modify this side
final RRect leftTrackSegment = RRect.fromLTRBR(trackRect.left, trackRect.top,
thumbCenter.dx - horizontalAdjustment, trackRect.bottom, Radius.circular(radius));
context.canvas.drawRRect(leftTrackSegment, leftTrackPaint);
final RRect rightTrackSegment = RRect.fromLTRBR(thumbCenter.dx + horizontalAdjustment, trackRect.top,
trackRect.right, trackRect.bottom, Radius.circular(radius));
context.canvas.drawRRect(rightTrackSegment, rightTrackPaint);
}
}
use:
trackShape: RoundSliderTrackShape(radius: 8)
The default trackShape for slider is now a RoundedRectSliderTrackShape in master branch:
https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/material/slider_theme.dart#L462
Does that work for you?
RectangularSliderTrackShape can be preserved by overriding the trackShape in the SliderThemeData.
Create custom shape like this, In this i am drawing 2 circles on thumb
class SliderThumbShape extends SliderComponentShape {
/// Create a slider thumb that draws a circle.
const SliderThumbShape({
this.enabledThumbRadius = 10.0,
this.disabledThumbRadius,
this.elevation = 1.0,
this.pressedElevation = 6.0,
});
/// The preferred radius of the round thumb shape when the slider is enabled.
///
/// If it is not provided, then the material default of 10 is used.
final double enabledThumbRadius;
/// The preferred radius of the round thumb shape when the slider is disabled.
///
/// If no disabledRadius is provided, then it is equal to the
/// [enabledThumbRadius]
final double disabledThumbRadius;
double get _disabledThumbRadius => disabledThumbRadius ?? enabledThumbRadius;
/// The resting elevation adds shadow to the unpressed thumb.
///
/// The default is 1.
///
/// Use 0 for no shadow. The higher the value, the larger the shadow. For
/// example, a value of 12 will create a very large shadow.
///
final double elevation;
/// The pressed elevation adds shadow to the pressed thumb.
///
/// The default is 6.
///
/// Use 0 for no shadow. The higher the value, the larger the shadow. For
/// example, a value of 12 will create a very large shadow.
final double pressedElevation;
#override
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
return Size.fromRadius(isEnabled == true ? enabledThumbRadius : _disabledThumbRadius);
}
#override
void paint(
PaintingContext context,
Offset center, {
Animation<double> activationAnimation,
#required Animation<double> enableAnimation,
bool isDiscrete,
TextPainter labelPainter,
RenderBox parentBox,
#required SliderThemeData sliderTheme,
TextDirection textDirection,
double value,
double textScaleFactor,
Size sizeWithOverflow,
}) {
assert(context != null);
assert(center != null);
assert(enableAnimation != null);
assert(sliderTheme != null);
assert(sliderTheme.disabledThumbColor != null);
assert(sliderTheme.thumbColor != null);
assert(!sizeWithOverflow.isEmpty);
final Canvas canvas = context.canvas;
final Tween<double> radiusTween = Tween<double>(
begin: _disabledThumbRadius,
end: enabledThumbRadius,
);
final double radius = radiusTween.evaluate(enableAnimation);
final Tween<double> elevationTween = Tween<double>(
begin: elevation,
end: pressedElevation,
);
final double evaluatedElevation = elevationTween.evaluate(activationAnimation);
{
final Path path = Path()
..addArc(Rect.fromCenter(center: center, width: 1 * radius, height: 1 * radius), 0, math.pi * 2);
Paint paint = Paint()..color = Colors.black;
paint.strokeWidth = 15;
paint.style = PaintingStyle.stroke;
canvas.drawCircle(
center,
radius,
paint,
);
{
Paint paint = Paint()..color = Colors.white;
paint.style = PaintingStyle.fill;
canvas.drawCircle(
center,
radius,
paint,
);
}
}
}
}
Then use it in your widget tree like this
SliderTheme(
data: SliderThemeData(
activeTrackColor: Colors.blue,
inactiveTrackColor: Color(0xffd0d2d3),
trackHeight: 2,
thumbShape: SliderThumbShape(),
),
child: Slider(
onChanged: (value) {},
value: 40.5,
max: 100,
min: 0,
),
),
The other option is to draw a thick line with a round StrokeCap. The cap radius at the two ends is in addition to the line width.
#override
void paint(PaintingContext context, Offset offset,
{required RenderBox parentBox,
required SliderThemeData sliderTheme,
required Animation<double> enableAnimation,
required Offset thumbCenter,
bool isEnabled = true,
bool isDiscrete = true,
required TextDirection textDirection}) {
final canvas = context.canvas;
final width = 200;
final height = 16;
canvas.drawLine(
Offset(0, height / 2),
Offset(width, height / 2),
Paint()
..strokeCap = StrokeCap.round
..strokeWidth = height);
}