How to add line dash in Flutter - flutter

How to make a line dash in Flutter like this?

As a workaround, in your case, you can do something like this
class MySeparator extends StatelessWidget {
const MySeparator({Key? key, this.height = 1, this.color = Colors.black})
: super(key: key);
final double height;
final Color color;
#override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final boxWidth = constraints.constrainWidth();
const dashWidth = 10.0;
final dashHeight = height;
final dashCount = (boxWidth / (2 * dashWidth)).floor();
return Flex(
children: List.generate(dashCount, (_) {
return SizedBox(
width: dashWidth,
height: dashHeight,
child: DecoratedBox(
decoration: BoxDecoration(color: color),
),
);
}),
mainAxisAlignment: MainAxisAlignment.spaceBetween,
direction: Axis.horizontal,
);
},
);
}
}
and use it const MySeparator()
class App extends StatelessWidget {
const App({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Material(
child: Container(
color: Colors.blue,
child: Center(
child: Container(
height: 600,
width: 350,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(16.0)),
),
child: Flex(
direction: Axis.vertical,
children: [
Expanded(child: Container()),
const MySeparator(color: Colors.grey),
Container(height: 200),
],
),
),
),
),
),
);
}
}

class DashedLinePainter extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
double dashWidth = 9, dashSpace = 5, startX = 0;
final paint = Paint()
..color = Colors.grey
..strokeWidth = 1;
while (startX < size.width) {
canvas.drawLine(Offset(startX, 0), Offset(startX + dashWidth, 0), paint);
startX += dashWidth + dashSpace;
}
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}

// garis putus putus
Row(
children: List.generate(150~/10, (index) => Expanded(
child: Container(
color: index%2==0?Colors.transparent
:Colors.grey,
height: 2,
),
)),
),

CustomPainter can help here as well. In this example is a vertical dash line but could be changed easily.
class LineDashedPainter extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
var paint = Paint()..strokeWidth = 2;
var max = 35;
var dashWidth = 5;
var dashSpace = 5;
double startY = 0;
while (max >= 0) {
canvas.drawLine(Offset(0, startY), Offset(0, startY + dashWidth), paint);
final space = (dashSpace + dashWidth);
startY += space;
max -= space;
}
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
and that use CustomPaint Widget:
CustomPaint(painter: LineDashedPainter())

I have written flutter_dash library for drawing that dash. Just one line and you should have a dash :D
Dash(length: 200, dashColor: Colors.red)
Give it a try!

The following code creates a dashed path not only for lines, but for any path you want dashed.
Demo:
The idea is to take the originalPath and move along it, alternately adding dashes and gaps until the entire path has been extracted:
Path _getDashedPath(
Path originalPath,
double dashLength,
double dashGapLength,
) {
final metricsIterator = originalPath.computeMetrics().iterator;
while (metricsIterator.moveNext()) {
final metric = metricsIterator.current;
_dashedPathProperties.extractedPathLength = 0.0;
while (_dashedPathProperties.extractedPathLength < metric.length) {
if (_dashedPathProperties.addDashNext) {
_dashedPathProperties.addDash(metric, dashLength);
} else {
_dashedPathProperties.addDashGap(metric, dashGapLength);
}
}
}
return _dashedPathProperties.path;
}
I created a class DashedPathProperties to track things like the currently extractedPathLength or the _remainingDashLength, which becomes relevant if the originalPath consists of several sub-paths and a dash (or dash gap) must be continued on the next sub-path:
class DashedPathProperties {
double extractedPathLength;
Path path;
final double _dashLength;
double _remainingDashLength;
double _remainingDashGapLength;
bool _previousWasDash;
DashedPathProperties({
required this.path,
required double dashLength,
required double dashGapLength,
}) : assert(dashLength > 0.0, 'dashLength must be > 0.0'),
assert(dashGapLength > 0.0, 'dashGapLength must be > 0.0'),
_dashLength = dashLength,
_remainingDashLength = dashLength,
_remainingDashGapLength = dashGapLength,
_previousWasDash = false,
extractedPathLength = 0.0;
//...
}
You can use like this (you could wrap your CustomPaint in a ClipRect if you want to make sure that the painter cannot paint outside of the bounds):
CustomPaint(
painter: DashedPathPainter(
originalPath: Path()
..addOval(
const Rect.fromLTWH(0, 0, 100, 100),
),
pathColor: Colors.white,
),
size: const Size(100.0, 100.0),
)
Full example code you can run in DartPad:
import 'dart:ui' as ui;
import 'dart:math' as math;
import 'package:flutter/material.dart';
const Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(
scaffoldBackgroundColor: darkBlue,
),
debugShowCheckedModeBanner: false,
home: const Scaffold(
body: Center(
child: ExampleDashedPath(),
),
),
);
}
}
class ExampleDashedPath extends StatelessWidget {
const ExampleDashedPath({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Column(
children: [
const SizedBox(height: 50),
CustomPaint(
painter: DashedPathPainter(
originalPath: Path()..lineTo(100, 0),
pathColor: Colors.red,
strokeWidth: 5.0,
dashGapLength: 10.0,
dashLength: 10.0,
),
size: const Size(100.0, 2.0),
),
const SizedBox(height: 50),
CustomPaint(
painter: DashedPathPainter(
originalPath: Path()
..addOval(
const Rect.fromLTWH(0, 0, 100, 100),
),
pathColor: Colors.white,
),
size: const Size(100.0, 100.0),
),
const SizedBox(height: 50),
CustomPaint(
painter: DashedPathPainter(
originalPath: Path()
..addRect(
const Rect.fromLTWH(0, 0, 100, 100),
)
..lineTo(100, 100),
pathColor: Colors.grey,
strokeWidth: 2.0,
dashLength: 25.0,
),
size: const Size(100.0, 100.0),
),
],
);
}
}
class DashedPathPainter extends CustomPainter {
final Path originalPath;
final Color pathColor;
final double strokeWidth;
final double dashGapLength;
final double dashLength;
late DashedPathProperties _dashedPathProperties;
DashedPathPainter({
required this.originalPath,
required this.pathColor,
this.strokeWidth = 3.0,
this.dashGapLength = 5.0,
this.dashLength = 10.0,
});
#override
void paint(Canvas canvas, Size size) {
_dashedPathProperties = DashedPathProperties(
path: Path(),
dashLength: dashLength,
dashGapLength: dashGapLength,
);
final dashedPath = _getDashedPath(originalPath, dashLength, dashGapLength);
canvas.drawPath(
dashedPath,
Paint()
..style = PaintingStyle.stroke
..color = pathColor
..strokeWidth = strokeWidth,
);
}
#override
bool shouldRepaint(DashedPathPainter oldDelegate) =>
oldDelegate.originalPath != originalPath ||
oldDelegate.pathColor != pathColor ||
oldDelegate.strokeWidth != strokeWidth ||
oldDelegate.dashGapLength != dashGapLength ||
oldDelegate.dashLength != dashLength;
Path _getDashedPath(
Path originalPath,
double dashLength,
double dashGapLength,
) {
final metricsIterator = originalPath.computeMetrics().iterator;
while (metricsIterator.moveNext()) {
final metric = metricsIterator.current;
_dashedPathProperties.extractedPathLength = 0.0;
while (_dashedPathProperties.extractedPathLength < metric.length) {
if (_dashedPathProperties.addDashNext) {
_dashedPathProperties.addDash(metric, dashLength);
} else {
_dashedPathProperties.addDashGap(metric, dashGapLength);
}
}
}
return _dashedPathProperties.path;
}
}
class DashedPathProperties {
double extractedPathLength;
Path path;
final double _dashLength;
double _remainingDashLength;
double _remainingDashGapLength;
bool _previousWasDash;
DashedPathProperties({
required this.path,
required double dashLength,
required double dashGapLength,
}) : assert(dashLength > 0.0, 'dashLength must be > 0.0'),
assert(dashGapLength > 0.0, 'dashGapLength must be > 0.0'),
_dashLength = dashLength,
_remainingDashLength = dashLength,
_remainingDashGapLength = dashGapLength,
_previousWasDash = false,
extractedPathLength = 0.0;
bool get addDashNext {
if (!_previousWasDash || _remainingDashLength != _dashLength) {
return true;
}
return false;
}
void addDash(ui.PathMetric metric, double dashLength) {
// Calculate lengths (actual + available)
final end = _calculateLength(metric, _remainingDashLength);
final availableEnd = _calculateLength(metric, dashLength);
// Add path
final pathSegment = metric.extractPath(extractedPathLength, end);
path.addPath(pathSegment, Offset.zero);
// Update
final delta = _remainingDashLength - (end - extractedPathLength);
_remainingDashLength = _updateRemainingLength(
delta: delta,
end: end,
availableEnd: availableEnd,
initialLength: dashLength,
);
extractedPathLength = end;
_previousWasDash = true;
}
void addDashGap(ui.PathMetric metric, double dashGapLength) {
// Calculate lengths (actual + available)
final end = _calculateLength(metric, _remainingDashGapLength);
final availableEnd = _calculateLength(metric, dashGapLength);
// Move path's end point
ui.Tangent tangent = metric.getTangentForOffset(end)!;
path.moveTo(tangent.position.dx, tangent.position.dy);
// Update
final delta = end - extractedPathLength;
_remainingDashGapLength = _updateRemainingLength(
delta: delta,
end: end,
availableEnd: availableEnd,
initialLength: dashGapLength,
);
extractedPathLength = end;
_previousWasDash = false;
}
double _calculateLength(ui.PathMetric metric, double addedLength) {
return math.min(extractedPathLength + addedLength, metric.length);
}
double _updateRemainingLength({
required double delta,
required double end,
required double availableEnd,
required double initialLength,
}) {
return (delta > 0 && availableEnd == end) ? delta : initialLength;
}
}

Vertical dashed line:
I modifed maksimr's example:
class DashedLine extends StatelessWidget {
final double height;
final double heightContainer;
final Color color;
const DashedLine({this.height = 3, this.color = Colors.black, this.heightContainer = 70});
#override
Widget build(BuildContext context) {
return Container(
height: heightContainer,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final boxHeight = constraints.constrainHeight();
final dashWidth = 10.0;
final dashHeight = height;
final dashCount = (boxHeight / (2 * dashHeight)).floor();
return Flex(
children: List.generate(dashCount, (_) {
return SizedBox(
width: dashWidth,
height: dashHeight,
child: DecoratedBox(
decoration: BoxDecoration(color: color),
),
);
}),
mainAxisAlignment: MainAxisAlignment.spaceBetween,
direction: Axis.vertical,
);
},
),
);
}
}

You can use CustomPainter with a linear gradient dashed shader for your lines.
// GradientRotation(3.14 / 2) — for vertical lines with dashes
// GradientRotation(0) — for horizontal lines with dashes
// .createShader(Rect.fromLTWH(0, 0, 10, 10) — 10 is the size of repeated shaders part
// This method can be tricky if you need a line oriented by some angle.
Paint()..shader = LinearGradient(
colors: [Colors.blue, Colors.transparent],
stops: [0.5, 0.5],
tileMode: TileMode.repeated,
transform: GradientRotation(3.14 / 2))
.createShader(Rect.fromLTWH(0, 0, 10, 10))
..style = PaintingStyle.stroke
..strokeWidth = 6

Create this class:
class DotWidget extends StatelessWidget {
final double totalWidth, dashWidth, emptyWidth, dashHeight;
final Color dashColor;
const DotWidget({
this.totalWidth = 300,
this.dashWidth = 10,
this.emptyWidth = 5,
this.dashHeight = 2,
this.dashColor = Colors.black,
Key key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(
totalWidth ~/ (dashWidth + emptyWidth),
(_) => Container(
width: dashWidth,
height: dashHeight,
color: dashColor,
margin: EdgeInsets.only(left: emptyWidth / 2, right: emptyWidth / 2),
),
),
);
}
}
Usage:
Use it like any other widget
child: DotWidget(
dashColor: Colors.black,
dashHeight: 2,
dashWidth: 100,
)

I created a CustomPainter by integrating the solution here and the math from here. This CustomPainter allows to draw a solid line or a dashed line by specifying the length of the dash and the length of the space between dashes. But the best thing is you can even draw the solid or dashed line in all directions. I mean horizontal, vertical, and even diagonal!
This is the code for the CustomPainter:
import 'dart:math';
import 'package:flutter/material.dart';
class LinePainter extends CustomPainter {
final Offset firstOffset;
final Offset secondOffset;
final Color color;
final double strokeWidth;
final double dashLength;
final double dashSpace;
const LinePainter({
required this.firstOffset,
required this.secondOffset,
this.color = Colors.black,
this.strokeWidth = 2.0,
this.dashLength = 4.0,
this.dashSpace = 4.0,
});
#override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..strokeWidth = strokeWidth;
_drawDashedLine(
dashLength, dashSpace, firstOffset, secondOffset, canvas, size, paint);
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
void _drawDashedLine(double dashLength, double dashSpace, Offset firstOffset,
Offset secondOffset, Canvas canvas, Size size, Paint paint) {
var startOffset = firstOffset;
var intervals = _getDirectionVector(firstOffset, secondOffset).length /
(dashLength + dashSpace);
for (var i = 0; i < intervals; i++) {
var endOffset = _getNextOffset(startOffset, secondOffset, dashLength);
/// Draw a small line.
canvas.drawLine(startOffset, endOffset, paint);
/// Update the starting offset.
startOffset = _getNextOffset(endOffset, secondOffset, dashSpace);
}
}
Offset _getNextOffset(
Offset firstOffset,
Offset secondOffset,
double smallVectorLength,
) {
var directionVector = _getDirectionVector(firstOffset, secondOffset);
var rescaleFactor = smallVectorLength / directionVector.length;
if (rescaleFactor.isNaN || rescaleFactor.isInfinite) {
rescaleFactor = 1;
}
var rescaledVector = Offset(directionVector.vector.dx * rescaleFactor,
directionVector.vector.dy * rescaleFactor);
var newOffset = Offset(
firstOffset.dx + rescaledVector.dx, firstOffset.dy + rescaledVector.dy);
return newOffset;
}
DirectionVector _getDirectionVector(Offset firstVector, Offset secondVector) {
var directionVector = Offset(
secondVector.dx - firstVector.dx, secondVector.dy - firstVector.dy);
var directionVectorLength =
sqrt(pow(directionVector.dx, 2) + pow(directionVector.dy, 2));
return DirectionVector(
vector: directionVector,
length: directionVectorLength,
);
}
}
class DirectionVector {
final Offset vector;
final double length;
const DirectionVector({
required this.vector,
required this.length,
});
}
You can use this CustomPainter by setting up the painter parameter of a CustomPaint widget, like this:
CustomPaint(
painter: LinePainter(
firstOffset: Offset(0, 0),
secondOffset: Offset(10, 10),
),
),
The result is shown in the following image:

Thank to marksimr answer, here is the code for both vertical and horizontal dash line.
Horizontal usage:
DashLineView(
fillRate: 0.7,
),
Vertical usage:
DashLineView(
fillRate: 0.7,
direction: Axis.vertical,
),
Full code:
class DashLineView extends StatelessWidget {
final double dashHeight;
final double dashWith;
final Color dashColor;
final double fillRate; // [0, 1] totalDashSpace/totalSpace
final Axis direction;
DashLineView(
{this.dashHeight = 1,
this.dashWith = 8,
this.dashColor = Colors.black,
this.fillRate = 0.5,
this.direction = Axis.horizontal});
#override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final boxSize = direction == Axis.horizontal
? constraints.constrainWidth()
: constraints.constrainHeight();
final dCount = (boxSize * fillRate / dashWith).floor();
return Flex(
children: List.generate(dCount, (_) {
return SizedBox(
width: direction == Axis.horizontal ? dashWith : dashHeight,
height: direction == Axis.horizontal ? dashHeight : dashWith,
child: DecoratedBox(
decoration: BoxDecoration(color: dashColor),
),
);
}),
mainAxisAlignment: MainAxisAlignment.spaceBetween,
direction: direction,
);
},
);
}
}

Row(
children: List.generate(20, (index) {
return Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Container(
height: 5,
width: 10,
color: Color(0XFFf2f2f2),
),
),
);
}),
)

Here is the code for horizontal dashed line, like your image. CustomPaint is highly recommended by flutter team for stuff like this. It is fast and efficient for rendering also. You can play with Offset to change the direction.
class MyClass extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Center(
child: CustomPaint(
painter: MyLinePainter(),
),
);
}
}
class MyLinePainter extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
var max = 100;
var dashWidth, dashSpace = 5;
double startX = 0;
final paint = Paint()..color = Colors.grey;
while (max >= 0) {
canvas.drawLine(Offset(startX, 0), Offset(startX + dashWidth, 0), paint..strokeWidth = 1);
final space = (dashSpace + dashWidth);
startX += space;
max -= space;
}
}

You can use this:
Widget dashedHorizontalLine(){
return Row(
children: [
for (int i = 0; i < 20; i++)
Expanded(
child: Row(
children: [
Expanded(
child: Divider(
color: AppColors.darkGreen,
thickness: 2,
),
),
Expanded(
child: Container(),
),
],
),
),
],
);
}

I came up with this solution.
Row( // Dashed line
children: [
for (int i = 0; i < 25; i++)
Container(
width: 5,
height: 1,
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
width: 1,
color: i % 2 == 0
? const Color.fromRGBO(214, 211, 211, 1)
: Colors.transparent,
),
),
),
),
],
),
Output:

Try this,
class DotDivider extends StatelessWidget {
final double width;
final double height;
final double gap;
final Color color;
final double lineHeight;
const DotDivider(
{this.height = 1.0,
this.color = Colors.black,
this.width = 2.0,
this.gap = 2.0,
this.lineHeight = 10.0});
#override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final boxWidth = constraints.constrainWidth();
final dashWidth = width;
final dashHeight = height;
final dashCount = (boxWidth / dashWidth).floor();
return Container(
height: (lineHeight * 2) + height,
child: ListView.builder(
physics: NeverScrollableScrollPhysics(),
scrollDirection: Axis.horizontal,
itemCount: dashCount,
itemBuilder: (BuildContext context, int index) => Center(
child: Container(
width: dashWidth,
height: dashHeight,
margin:
EdgeInsets.symmetric(vertical: lineHeight, horizontal: gap),
decoration: BoxDecoration(color: color),
),
),
),
);
},
);
}
}

You should prefer using CustomPainter because it's more performance and suitable for such issues.
class DashLine extends StatelessWidget {
const DashLine({
Key key,
this.color,
this.dashWidth,
this.dashSpace,
this.strokeWidth,
}) : super(key: key);
final Color color;
final double dashWidth;
final double dashSpace;
final double strokeWidth;
#override
Widget build(BuildContext context) {
return CustomPaint(
painter: _DashLinePainter(
color: color,
dashWidth: dashWidth,
dashSpace: dashSpace,
strokeWidth: strokeWidth,
),
);
}
}
class _DashLinePainter extends CustomPainter {
_DashLinePainter({
Color color,
double dashWidth,
double dashSpace,
double strokeWidth,
}) : _color = color ?? Colors.red,
_dashWidth = dashWidth ?? 5.0,
_dashSpace = dashSpace ?? 5.0,
_strokeWidth = strokeWidth ?? 1.0;
final Color _color;
final double _dashWidth;
final double _dashSpace;
final double _strokeWidth;
#override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = _color
..strokeWidth = _strokeWidth;
var max = size.width;
var startX = 0.0;
while (max >= 0) {
canvas.drawLine(Offset(startX, 0), Offset(startX + _dashWidth, 0), paint);
final space = (_dashSpace + _dashWidth);
startX += space;
max -= space;
}
}
#override
bool shouldRepaint(_DashLinePainter oldDelegate) {
return _color != oldDelegate._color ||
_dashWidth != oldDelegate._dashWidth ||
_dashSpace != oldDelegate._dashSpace ||
_strokeWidth != oldDelegate._strokeWidth;
}
}

Use dotted_line: ^3.0.0 lib which provides dashed lines and many more link
import 'package:dotted_line/dotted_line.dart';
DottedLine(
direction: Axis.horizontal,
lineLength: double.infinity,
lineThickness: 1.0,
dashLength: 4.0,
dashColor: Colors.grey,
dashRadius: 0.0,
dashGapLength: 4.0,
dashGapColor: Colors.transparent,
dashGapRadius: 0.0,
)
Output:

When going for the CustomPainter approach, painting a dashed line in any direction can be achieved by a snippet like this:
void _drawDashedLine(Canvas canvas, Offset start, Offset end, Paint paint) {
const dashLength = 10.0;
const stride = 2 * dashLength;
var distance = (end - start).distance;
while (distance > 0) {
final remaining = end - start;
final direction = remaining / remaining.distance;
final next = start + (direction * dashLength);
canvas.drawLine(start, next, paint);
start = start + (direction * stride);
distance -= stride;
}
}
This method should be embedded in your CustomPainter implementation of a CustomPaint widget, like mentioned in the other answers.

Container(
color: Colors.white,
height: 40.0,
child: Center(
child: Text(
"---------------------------------------------------------------------------",
maxLines: 1,
style: typoNormalTextRegular.copyWith(
color: colorABGray),
),
),
),
Only use Text Widget, easy solution

To have horizontal dashed line I have made following custom class:
Custom Painter Class:
class DrawDottedhorizontalline extends CustomPainter {
Paint _paint;
DrawDottedhorizontalline() {
_paint = Paint();
_paint.color = Colors.black; //dots color
_paint.strokeWidth = 2; //dots thickness
_paint.strokeCap = StrokeCap.square; //dots corner edges
}
#override
void paint(Canvas canvas, Size size) {
for (double i = -300; i < 300; i = i + 15) {
// 15 is space between dots
if (i % 3 == 0)
canvas.drawLine(Offset(i, 0.0), Offset(i + 10, 0.0), _paint);
}
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}
Use:
Container(
color: Colors.white,
height: 150.0, //height of container
child: Center(
child:CustomPaint(painter: DrawDottedhorizontalline()),
//drawing horizontal dotted/dash line
),
),

Related

CompositedTransformFollower how to transform container, but not decoration border thickness?

I am using an OverlayEntry to provide a "selection marquee" around a set of geometry.
The marquee follows the geometry widget position via a CompositedTransformFollower.
The problem is, when the user "zooms" in on a widget (the widget is scaled using a transform) - my marquee is scaled (good), and hence the border thickness is also scaled (bad).
Is there some way I can ensure the border decoration is always rendered as "X" (physical) pixels thick, even when scaled with a transform?
Thanks
check the following FooWidget widget, the main idea is to get RenderFollowerLayer object used by CompositedTransformFollower and then to call its renderFollowerLayer?.layer?.getLastTransform() method which gives you the current Matrix4 transform
when you have the matrix you can call _getScaleFromMatrix method and get the current scale factor
also notice that CurstomPaint allows to repaint its content without rebuilding the whole ui tree (you have to do that if you want to use "normal" BoxDecoration when you need to use different border width for different scale factors)
class FooWidget extends StatefulWidget {
#override
State<FooWidget> createState() => _FooWidgetState();
}
class _FooWidgetState extends State<FooWidget> with TickerProviderStateMixin {
final link = LayerLink();
final followerKey = GlobalKey();
late final transformController = AnimationController.unbounded(
vsync: this,
duration: const Duration(milliseconds: 1234),
);
late final borderController = AnimationController.unbounded(
vsync: this,
duration: const Duration(milliseconds: 2000),
);
OverlayEntry? overlayEntry;
final repaint = ValueNotifier(0);
#override
void initState() {
super.initState();
SchedulerBinding.instance.addPersistentFrameCallback(_forceRepaintCallback);
SchedulerBinding.instance.addPostFrameCallback((timeStamp) => _toggleOverlay());
_animateBorder();
_animateTransform();
}
void _forceRepaintCallback(Duration timeStamp) {
// debugPrint('timeStamp: $timeStamp');
repaint.value++;
}
_animateTransform() async {
while (true) {
await transformController.animateTo(transformController.value + 8, curve: Curves.easeInOut);
await Future.delayed(const Duration(milliseconds: 1300));
}
}
_animateBorder() async {
while (true) {
await borderController.animateTo(borderController.value + 4.567 * pi, curve: Curves.ease);
await Future.delayed(const Duration(milliseconds: 800));
}
}
static const kSize = Size(100, 100);
static final kSizeCenter = kSize.center(Offset.zero);
#override
Widget build(BuildContext context) {
// timeDilation = 10;
return Stack(
children: [
Align(
alignment: const Alignment(0, -0.5),
child: AnimatedBuilder(
animation: transformController,
builder: (context, child) {
return Transform(
transform: composeMatrixFromOffsets(
anchor: kSizeCenter,
translate: kSizeCenter,
rotation: -transformController.value / 5,
scale: ui.lerpDouble(0.5, 4, pow(sin(transformController.value / 10), 2).toDouble())!,
),
child: child,
);
},
child: CompositedTransformTarget(
link: link,
child: SizedBox.fromSize(
size: kSize,
child: const FlutterLogo(),
),
),
),
),
Align(
alignment: const Alignment(0, -0.75),
child: ElevatedButton(
style: const ButtonStyle(
backgroundColor: MaterialStatePropertyAll(Colors.orange),
),
onPressed: _toggleOverlay,
child: Text(overlayEntry == null? 'add overlay' : 'remove overlay'),
),
),
],
);
}
_toggleOverlay() {
if (overlayEntry == null) {
overlayEntry = OverlayEntry(
builder: (ctx) => CompositedTransformFollower(
link: link,
key: followerKey,
child: DefaultTextStyle(
style: const TextStyle(
color: Colors.black,
fontSize: 18,
),
child: UnconstrainedBox(
alignment: Alignment.topLeft,
child: IgnorePointer(
child: CustomPaint(
painter: FooWidgetPainter(
followerKey: followerKey,
borderController: borderController,
// repaint: Listenable.merge([transformController, borderController]),
repaint: repaint,
),
child: SizedBox.fromSize(
size: link.leaderSize,
child: const Center(
child: Text('text and border come from the overlay', textAlign: TextAlign.center),
),
),
),
),
),
),
),
);
Overlay.of(context).insert(overlayEntry!);
SchedulerBinding.instance.addPostFrameCallback((timeStamp) => overlayEntry!.markNeedsBuild());
} else {
overlayEntry!.remove();
overlayEntry = null;
}
setState(() {});
}
#override
void dispose() {
super.dispose();
transformController.dispose();
borderController.dispose();
}
}
class FooWidgetPainter extends CustomPainter {
FooWidgetPainter({
required this.followerKey,
required this.borderController,
required Listenable repaint,
}) : super(repaint: repaint);
final AnimationController borderController;
final GlobalKey followerKey;
RenderFollowerLayer? renderFollowerLayer;
#override
void paint(ui.Canvas canvas, Size size) {
final rect = Offset.zero & size;
renderFollowerLayer ??= followerKey.currentContext?.findRenderObject() as RenderFollowerLayer;
final matrix = renderFollowerLayer?.layer?.getLastTransform();
if (matrix == null) {
debugPrint('cannot get last transform');
return;
}
double scale = _getScaleFromMatrix(matrix);
//
// debugPrint('matrix: $matrix, rect: $rect, scale: $scale');
//
final shader = SweepGradient(
colors: [Colors.black12, Colors.red.shade900, Colors.blue.shade900, Colors.black12],
stops: const [0.375, 0.4, 0.6, 0.625],
transform: GradientRotation(borderController.value),
).createShader(rect);
// draws 6 px border no matter what [scale] is used
final paint = Paint()
..shader = shader
..maskFilter = MaskFilter.blur(BlurStyle.outer, 6 / scale);
final rrect = RRect.fromRectAndRadius(rect, const Radius.circular(12));
for (int i = 0; i < 3; i++ ) {
canvas.drawRRect(rrect, paint);
}
}
/// find the scaleX factor of the [matrix]
/// see Matrix4.getMaxScaleOnAxis() implementation
double _getScaleFromMatrix(Matrix4 matrix) {
final s = matrix.storage;
return sqrt(s[0] * s[0] + s[1] * s[1] + s[2] * s[2]);
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
Matrix4 composeMatrixFromOffsets({
double scale = 1,
double rotation = 0,
Offset translate = Offset.zero,
Offset anchor = Offset.zero,
}) {
final double c = cos(rotation) * scale;
final double s = sin(rotation) * scale;
final double dx = translate.dx - c * anchor.dx + s * anchor.dy;
final double dy = translate.dy - s * anchor.dx - c * anchor.dy;
return Matrix4(c, s, 0, 0, -s, c, 0, 0, 0, 0, 1, 0, dx, dy, 0, 1);
}

Moving Circle in customer painter with gesture detector not working

I am trying to use gesture detector to move the red point that i draw in custom painter around the xy plot that i draw in a seperate custom painter but i cant seem to get it to work. I would like to thank any help give in advance.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
bool isDown = false;
var xPos = 5.0;
var yPos = 5.0;
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Builder(builder: (BuildContext context) {
return Scaffold(
body: Center(
child: Container(
alignment: Alignment.center,
height: MediaQuery.of(context).size.height * 0.3,
width: MediaQuery.of(context).size.width,
color: Colors.grey,
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.3,
),
padding:
const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
child: LayoutBuilder(
builder: (_, constraints) => Container(
width: constraints.widthConstraints().maxWidth,
height: constraints.widthConstraints().maxHeight,
color: Colors.white,
child: Stack(children: [
// plotting X Y axis
SizedBox(
width: constraints.widthConstraints().maxWidth,
height:
constraints.widthConstraints().maxHeight,
child: CustomPaint(painter: PlotPainter())),
GestureDetector(
onPanUpdate: (details) {
final tapPos = details.localPosition;
if (tapPos.dx > 5 &&
tapPos.dx <
constraints
.widthConstraints()
.maxWidth) {
setState(() {
xPos = tapPos.dx - 5;
yPos = tapPos.dy - 5;
});
}
if (tapPos.dy > 5 &&
tapPos.dy <
constraints
.widthConstraints()
.maxHeight) {
setState(() {
yPos = tapPos.dy - 5;
});
}
},
child: SizedBox(
width:
constraints.widthConstraints().maxWidth,
height:
constraints.widthConstraints().maxHeight,
child: CustomPaint(
painter: PointsPainter(xPos, yPos))),
),
// plotting points
]),
)),
),
),
);
}));
}
}
class PointsPainter extends CustomPainter {
PointsPainter(this.xPos, this.yPos);
double xPos;
double yPos;
#override
void paint(Canvas canvas, Size size) {
// TODO: implement points
final midY = size.height / 2;
final midX = size.width / 2;
final Circlepaint = Paint()
..style = PaintingStyle.fill
..color = Colors.red;
canvas.drawCircle(Offset(xPos, yPos), 5, Circlepaint);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
// TODO: implement shouldRepaint
return true;
}
}
// drawing the x y axis
class PlotPainter extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
final midY = size.height / 2;
final midX = size.width / 2;
final paint = Paint()
..style = PaintingStyle.fill
..color = Colors.black
..strokeWidth = 1.0;
final textPainterx = TextPainter(
text: const TextSpan(
text: 'x',
style: TextStyle(
color: Colors.black,
fontSize: 15,
),
),
textDirection: TextDirection.ltr,
textAlign: TextAlign.center);
final textPaintery = TextPainter(
text: const TextSpan(
text: 'y',
style: TextStyle(
color: Colors.black,
fontSize: 13,
),
),
textDirection: TextDirection.ltr,
textAlign: TextAlign.center);
// X axis
canvas.drawLine(Offset(0, midY), Offset(size.width, midY), paint);
//y Axis
canvas.drawLine(Offset(midX, 0), Offset(midX, size.height), paint);
//arrow head of the X-Y Axis
canvas.drawLine(
Offset(size.width, midY), Offset(size.width - 5, midY - 3), paint);
canvas.drawLine(
Offset(size.width, midY), Offset(size.width - 5, midY + 3), paint);
canvas.drawLine(Offset(midX, 0), Offset(midX - 3, 5), paint);
canvas.drawLine(Offset(midX, 0), Offset(midX + 3, 5), paint);
textPainterx.layout();
textPaintery.layout();
// Draw the text X at the X axis
textPainterx.paint(canvas, Offset(size.width - 7, midY + 1));
// Draw the text y at the Y axis
textPaintery.paint(canvas, Offset(midX + 5, 0));
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
The Problem was that you did not wrap the custom painter inside gesture detector now that is working fine
now you only have to add condition that it will not go more up and down on given height

Flutter custom shape using CustomPaint

I just want my created shape so that i can stack a widget to achieve the Image below. i am trying to get the transparent shape at the back ground of the X and Love. I Try using the shape maker but my mouse designing is not perfect. here is the code generated from the shape maker
child: CustomPaint(
size: Size(400,(400*0.2857142857142857).toDouble()),
painter: RPSCustomPainter(),
),
class RPSCustomPainter extends CustomPainter{
#override
void paint(Canvas canvas, Size size) {
Paint paint_0 = new Paint()
..color = Color.fromARGB(255, 33, 150, 243)
..style = PaintingStyle.stroke
..strokeWidth = 1;
Path path_0 = Path();
path_0.moveTo(size.width*0.2137714,size.height*0.2524000);
path_0.cubicTo(size.width*0.1736143,size.height*0.4775500,size.width*0.1973000,size.height*0.6711500,size.width*0.2153286,size.height*0.7510000);
path_0.cubicTo(size.width*0.2270429,size.height*0.7777500,size.width*0.2705286,size.height*0.9439500,size.width*0.3556000,size.height*0.7521500);
path_0.cubicTo(size.width*0.3856000,size.height*0.6504000,size.width*0.3970143,size.height*0.6162000,size.width*0.4283571,size.height*0.7526000);
path_0.cubicTo(size.width*0.4669286,size.height*0.8264000,size.width*0.5172429,size.height*0.9022500,size.width*0.5719714,size.height*0.7500000);
path_0.cubicTo(size.width*0.6146429,size.height*0.5440500,size.width*0.5914429,size.height*0.3101000,size.width*0.5713714,size.height*0.2514000);
path_0.cubicTo(size.width*0.5520714,size.height*0.1778000,size.width*0.4875429,size.height*0.0767500,size.width*0.4296571,size.height*0.2527000);
path_0.cubicTo(size.width*0.4023714,size.height*0.3646000,size.width*0.3816857,size.height*0.3850000,size.width*0.3557143,size.height*0.2523000);
path_0.cubicTo(size.width*0.3438571,size.height*0.2086000,size.width*0.2652143,size.height*0.0579000,size.width*0.2137714,size.height*0.2524000);
path_0.close();
canvas.drawPath(path_0, paint_0);
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
what i am trying to achieve
my result. the shape is not perfect.
thanks
At first I wanted to describe you the ways you can achieve the shape you want and so on...
But got carried away with this fun programming challenge and ended up creating an actual widget :)
It depends on the font_awesome_flutter package, so don't forget to install it (for the heart icon). font_awesome_flutter
So the widget's source code is:
import 'package:flutter/material.dart';
import 'dart:math' as math;
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
extension ToRadians on int {
double get toRadians => this * (math.pi / 180.0);
}
enum _ButtonType { like, dislike }
class LikeOrNot extends StatelessWidget {
final VoidCallback onLike;
final VoidCallback onDislike;
// Percents from total widget width, default - 2%
final _gapSizeRatio = 0.02;
final _likeIconColor = const Color(0xffb85076);
final _dislikeIconColor = Colors.white;
const LikeOrNot({
Key? key,
required this.onLike,
required this.onDislike,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: 2,
child: LayoutBuilder(
builder: (context, constraints) {
final buttonPaddings = constraints.maxHeight * 0.1;
final halfWidth = constraints.maxWidth / 2;
return Stack(
children: [
Positioned.fill(
child: CustomPaint(
painter: RPSCustomPainter(
gapSizeRatio: _gapSizeRatio,
),
),
),
Positioned(
left: 0,
bottom: 0,
top: 0,
right: halfWidth + constraints.maxWidth * _gapSizeRatio,
child: SizedBox.expand(
child: Padding(
padding: EdgeInsets.all(buttonPaddings),
child: _buildButton(_ButtonType.dislike),
),
),
),
Positioned(
right: 0,
bottom: 0,
top: 0,
left: halfWidth + constraints.maxWidth * _gapSizeRatio,
child: SizedBox.expand(
child: Padding(
padding: EdgeInsets.all(buttonPaddings),
child: _buildButton(_ButtonType.like),
),
),
),
],
);
},
),
);
}
Widget _buildButton(_ButtonType buttonType) {
final isPositiveAction = buttonType == _ButtonType.like;
return ElevatedButton(
style: ElevatedButton.styleFrom(
shape: const CircleBorder(),
primary: isPositiveAction ? _dislikeIconColor : _likeIconColor,
onPrimary: isPositiveAction ? _likeIconColor : _dislikeIconColor,
padding: EdgeInsets.zero,
elevation: 10,
shadowColor: Colors.black54,
),
onPressed: onDislike,
child: FractionallySizedBox(
widthFactor: 0.35,
heightFactor: 0.35,
child: FittedBox(
child: isPositiveAction
? const FaIcon(FontAwesomeIcons.heart)
: const Icon(Icons.close),
),
),
);
}
}
class RPSCustomPainter extends CustomPainter {
final double gapSizeRatio;
RPSCustomPainter({
required this.gapSizeRatio,
});
#override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.black.withOpacity(0.08)
..style = PaintingStyle.fill
..strokeWidth = 1;
final path = Path();
final gapSize = size.width * gapSizeRatio;
final arcRadius = size.height / 2 - gapSize / 2;
final leftCircleCenter = Offset(
size.width * 0.25 - gapSize / 2,
size.height / 2,
);
final rightCircleCenter = Offset(
size.width * 0.75 + gapSize / 2,
size.height / 2,
);
path.arcTo(
Rect.fromCircle(
center: leftCircleCenter,
radius: arcRadius,
),
45.toRadians,
270.toRadians,
false,
);
final bezierOffset = arcRadius * (105 / 360);
path.quadraticBezierTo(
size.width / 2,
size.height * 0.30,
rightCircleCenter.dx - arcRadius + bezierOffset,
rightCircleCenter.dy - arcRadius + bezierOffset,
);
path.arcTo(
Rect.fromCircle(
center: rightCircleCenter,
radius: arcRadius,
),
225.toRadians,
270.toRadians,
false,
);
path.quadraticBezierTo(
size.width / 2,
size.height * 0.70,
leftCircleCenter.dx + arcRadius - bezierOffset,
leftCircleCenter.dy + arcRadius - bezierOffset,
);
canvas.drawPath(path, paint);
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}
It's dynamic, the buttons are built-in. You get 2 options to work with - onDislike() and onLike() callbacks.
An example using different sizes.
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xffc0496f), Color(0xffdb6b59)],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
),
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
title: const Text('Test'),
backgroundColor: Colors.transparent,
elevation: 0,
),
body: Container(
width: double.infinity,
padding: const EdgeInsets.all(20.0),
child: Column(
children: [
for (final size in List.generate(5, (index) => index++))
FractionallySizedBox(
widthFactor: 1.0 - size * 0.2,
child: LikeOrNot(
onLike: () {},
onDislike: () {},
),
),
],
),
),
),
);
}
}
There's a _gapSize parameter which is the gap between two circles. The one you need is already inside (2% default) but you can get some cool other variations by changing it. For example, here's a gap of 20% total width:

how to make Percent Indicator change color programmatically in flutter?

I'm using a package called percent indicator https://pub.dev/packages/percent_indicator
and I'm currently using its CircularPercentIndicator()
I'm just wondering how to change the progress color when a certain percentage met?
For example: I have a starting progress color of green at 0% when reaching 60% progress color should change to orange and when reaching 80% color should be red.
here's what I got at the moment:
import 'package:flutter/material.dart';
import 'package:percent_indicator/circular_percent_indicator.dart';
class RoutinePage extends StatefulWidget {
#override
_RoutinePageState createState() => _RoutinePageState();
}
class _RoutinePageState extends State<RoutinePage> {
double progress = 0;
#override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Container(
color: Colors.white,
alignment: Alignment(0, 0),
child: CircularPercentIndicator(
animationDuration: 100,
animateFromLastPercent: true,
arcType: ArcType.FULL,
arcBackgroundColor: Colors.black12,
backgroundColor: Colors.white,
progressColor: Colors.green,
percent: progress,
animation: true,
radius: 250.0,
lineWidth: 12.0,
circularStrokeCap: CircularStrokeCap.round,
),
),
Container(
alignment: Alignment(0, 0),
child: Text("${this.progress * 100}%",
style: TextStyle(
fontSize: 30,
fontWeight: FontWeight.bold,
),
),
),
Container(
alignment: Alignment(0.3, 0.5),
child: RaisedButton(
color: Colors.green,
onPressed: () {
final updated = ((this.progress + 0.1).clamp(0.0, 1.0) * 100);
setState(() {
this.progress = updated.round() / 100;
});
print(progress);
},
child: Text('+10%',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
),)),
),
Container(
alignment: Alignment(-0.3, 0.5),
child: RaisedButton(
color: Colors.red,
onPressed: () {
final updated = ((this.progress - 0.1).clamp(0.0, 1.0) * 100);
setState(() {
this.progress = updated.round() / 100;
});
print(progress);
},
child: Text('-10%',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
),)),
),
],
);
}
}
and I don't know if this will help but this is the code of CircularPercentIndicator()
//import 'dart:math';
import 'package:flutter/material.dart';
import 'package:vector_math/vector_math_64.dart' as math;
enum CircularStrokeCap { butt, round, square }
enum ArcType {
HALF,
FULL,
}
class CircularPercentIndicator extends StatefulWidget {
///Percent value between 0.0 and 1.0
final double percent;
final double radius;
///Width of the line of the Circle
final double lineWidth;
///Color of the background of the circle , default = transparent
final Color fillColor;
///First color applied to the complete circle
final Color backgroundColor;
Color get progressColor => _progressColor;
Color _progressColor;
///true if you want the circle to have animation
final bool animation;
///duration of the animation in milliseconds, It only applies if animation attribute is true
final int animationDuration;
///widget at the top of the circle
final Widget header;
///widget at the bottom of the circle
final Widget footer;
///widget inside the circle
final Widget center;
final LinearGradient linearGradient;
///The kind of finish to place on the end of lines drawn, values supported: butt, round, square
final CircularStrokeCap circularStrokeCap;
///the angle which the circle will start the progress (in degrees, eg: 0.0, 45.0, 90.0)
final double startAngle;
/// set true if you want to animate the linear from the last percent value you set
final bool animateFromLastPercent;
/// set false if you don't want to preserve the state of the widget
final bool addAutomaticKeepAlive;
/// set the arc type
final ArcType arcType;
/// set a circular background color when use the arcType property
final Color arcBackgroundColor;
/// set true when you want to display the progress in reverse mode
final bool reverse;
/// Creates a mask filter that takes the progress shape being drawn and blurs it.
final MaskFilter maskFilter;
CircularPercentIndicator(
{Key key,
this.percent = 0.0,
this.lineWidth = 5.0,
this.startAngle = 0.0,
#required this.radius,
this.fillColor = Colors.transparent,
this.backgroundColor = const Color(0xFFB8C7CB),
Color progressColor,
this.linearGradient,
this.animation = false,
this.animationDuration = 500,
this.header,
this.footer,
this.center,
this.addAutomaticKeepAlive = true,
this.circularStrokeCap,
this.arcBackgroundColor,
this.arcType,
this.animateFromLastPercent = false,
this.reverse = false,
this.maskFilter})
: super(key: key) {
if (linearGradient != null && progressColor != null) {
throw ArgumentError(
'Cannot provide both linearGradient and progressColor');
}
_progressColor = progressColor ?? Colors.red;
assert(startAngle >= 0.0);
if (percent < 0.0 || percent > 1.0) {
throw Exception("Percent value must be a double between 0.0 and 1.0");
}
if (arcType == null && arcBackgroundColor != null) {
throw ArgumentError('arcType is required when you arcBackgroundColor');
}
}
#override
_CircularPercentIndicatorState createState() =>
_CircularPercentIndicatorState();
}
class _CircularPercentIndicatorState extends State<CircularPercentIndicator>
with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin {
AnimationController _animationController;
Animation _animation;
double _percent = 0.0;
#override
void dispose() {
if (_animationController != null) {
_animationController.dispose();
}
super.dispose();
}
#override
void initState() {
if (widget.animation) {
_animationController = AnimationController(
vsync: this,
duration: Duration(milliseconds: widget.animationDuration));
_animation =
Tween(begin: 0.0, end: widget.percent).animate(_animationController)
..addListener(() {
setState(() {
_percent = _animation.value;
});
});
_animationController.forward();
} else {
_updateProgress();
}
super.initState();
}
#override
void didUpdateWidget(CircularPercentIndicator oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.percent != widget.percent ||
oldWidget.startAngle != widget.startAngle) {
if (_animationController != null) {
_animationController.duration =
Duration(milliseconds: widget.animationDuration);
_animation = Tween(
begin: widget.animateFromLastPercent ? oldWidget.percent : 0.0,
end: widget.percent)
.animate(_animationController);
_animationController.forward(from: 0.0);
} else {
_updateProgress();
}
}
}
_updateProgress() {
setState(() {
_percent = widget.percent;
});
}
#override
Widget build(BuildContext context) {
super.build(context);
var items = List<Widget>();
if (widget.header != null) {
items.add(widget.header);
}
items.add(Container(
height: widget.radius + widget.lineWidth,
width: widget.radius,
child: CustomPaint(
painter: CirclePainter(
progress: _percent * 360,
progressColor: widget.progressColor,
backgroundColor: widget.backgroundColor,
startAngle: widget.startAngle,
circularStrokeCap: widget.circularStrokeCap,
radius: (widget.radius / 2) - widget.lineWidth / 2,
lineWidth: widget.lineWidth,
arcBackgroundColor: widget.arcBackgroundColor,
arcType: widget.arcType,
reverse: widget.reverse,
linearGradient: widget.linearGradient,
maskFilter: widget.maskFilter),
child: (widget.center != null)
? Center(child: widget.center)
: Container(),
)));
if (widget.footer != null) {
items.add(widget.footer);
}
return Material(
color: widget.fillColor,
child: Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: items,
)),
);
}
#override
bool get wantKeepAlive => widget.addAutomaticKeepAlive;
}
class CirclePainter extends CustomPainter {
final Paint _paintBackground = Paint();
final Paint _paintLine = Paint();
final Paint _paintBackgroundStartAngle = Paint();
final double lineWidth;
final double progress;
final double radius;
final Color progressColor;
final Color backgroundColor;
final CircularStrokeCap circularStrokeCap;
final double startAngle;
final LinearGradient linearGradient;
final Color arcBackgroundColor;
final ArcType arcType;
final bool reverse;
final MaskFilter maskFilter;
CirclePainter(
{this.lineWidth,
this.progress,
#required this.radius,
this.progressColor,
this.backgroundColor,
this.startAngle = 0.0,
this.circularStrokeCap = CircularStrokeCap.round,
this.linearGradient,
this.reverse,
this.arcBackgroundColor,
this.arcType,
this.maskFilter}) {
_paintBackground.color = backgroundColor;
_paintBackground.style = PaintingStyle.stroke;
_paintBackground.strokeWidth = lineWidth;
if (arcBackgroundColor != null) {
_paintBackgroundStartAngle.color = arcBackgroundColor;
_paintBackgroundStartAngle.style = PaintingStyle.stroke;
_paintBackgroundStartAngle.strokeWidth = lineWidth;
}
_paintLine.color = progressColor;
_paintLine.style = PaintingStyle.stroke;
_paintLine.strokeWidth = lineWidth;
if (circularStrokeCap == CircularStrokeCap.round) {
_paintLine.strokeCap = StrokeCap.round;
} else if (circularStrokeCap == CircularStrokeCap.butt) {
_paintLine.strokeCap = StrokeCap.butt;
} else {
_paintLine.strokeCap = StrokeCap.square;
}
}
#override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
canvas.drawCircle(center, radius, _paintBackground);
if (maskFilter != null) {
_paintLine.maskFilter = maskFilter;
}
if (linearGradient != null) {
/*
_paintLine.shader = SweepGradient(
center: FractionalOffset.center,
startAngle: math.radians(-90.0 + startAngle),
endAngle: math.radians(progress),
//tileMode: TileMode.mirror,
colors: linearGradient.colors)
.createShader(
Rect.fromCircle(
center: center,
radius: radius,
),
);*/
_paintLine.shader = linearGradient.createShader(
Rect.fromCircle(
center: center,
radius: radius,
),
);
}
double fixedStartAngle = startAngle;
double startAngleFixedMargin = 1.0;
if (arcType != null) {
if (arcType == ArcType.FULL) {
fixedStartAngle = 220;
startAngleFixedMargin = 172 / fixedStartAngle;
} else {
fixedStartAngle = 270;
startAngleFixedMargin = 135 / fixedStartAngle;
}
}
if (arcBackgroundColor != null) {
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
math.radians(-90.0 + fixedStartAngle),
math.radians(360 * startAngleFixedMargin),
false,
_paintBackgroundStartAngle,
);
}
if (reverse) {
final start =
math.radians(360 * startAngleFixedMargin - 90.0 + fixedStartAngle);
final end = math.radians(-progress * startAngleFixedMargin);
canvas.drawArc(
Rect.fromCircle(
center: center,
radius: radius,
),
start,
end,
false,
_paintLine,
);
} else {
final start = math.radians(-90.0 + fixedStartAngle);
final end = math.radians(progress * startAngleFixedMargin);
canvas.drawArc(
Rect.fromCircle(
center: center,
radius: radius,
),
start,
end,
false,
_paintLine,
);
}
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
One of possible solutions is AnimatedBuilder.
I will show you how can we change color of button and you can easily apply approach to progress indicator.
The example below just shows when tap on button start changing animation. Same for you when you need to start progress bat, just run animationController and check result.
If you have further questions, do not hesitate to ask in comments
#override
void initState() {
_animationController =
AnimationController(vsync: this, duration: Duration(milliseconds: 300));
_colorTween = ColorTween(begin: Colors.red, end: Colors.green)
.animate(_animationController);
super.initState();
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _colorTween,
builder: (context, child) => RaisedButton(
child: Text("Change my color"),
color: _colorTween.value,
onPressed: () {
if (_animationController.status == AnimationStatus.completed) {
_animationController.reverse();
} else {
_animationController.forward();
}
},
),
);
}
thanks for the response. I also came up with other solution and I think I'm good on what I've ended up with. btw here's what I came up with:
import 'package:flutter/material.dart';
import 'package:percent_indicator/circular_percent_indicator.dart';
class RoutinePage extends StatefulWidget {
#override
_RoutinePageState createState() => _RoutinePageState();
}
class _RoutinePageState extends State<RoutinePage> {
double progress = 0;
currentProgressColor() {
if (progress >= 0.6 && progress < 0.8) {
return Colors.orange;
}
if(progress >= 0.8){
return Colors.red;
}
else{
return Colors.green;
}
}
#override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Container(
color: Colors.white,
alignment: Alignment(0, 0),
child: CircularPercentIndicator(
animationDuration: 200,
animateFromLastPercent: true,
arcType: ArcType.FULL,
arcBackgroundColor: Colors.black12,
backgroundColor: Colors.white,
progressColor: currentProgressColor(),
percent: progress,
animation: true,
radius: 250.0,
lineWidth: 12.0,
circularStrokeCap: CircularStrokeCap.butt,
),
),
Container(
alignment: Alignment(0, 0),
child: Text(
"${this.progress * 100}%",
style: TextStyle(
fontSize: 30,
fontWeight: FontWeight.bold,
),
),
),
Container(
alignment: Alignment(0.3, 0.5),
child: RaisedButton(
color: Colors.green,
onPressed: () {
final updated = ((this.progress + 0.1).clamp(0.0, 1.0) * 100);
setState(() {
this.progress = updated.round() / 100;
});
print(progress);
},
child: Text(
'+10%',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
),
)),
),
Container(
alignment: Alignment(-0.3, 0.5),
child: RaisedButton(
color: Colors.red,
onPressed: () {
final updated = ((this.progress - 0.1).clamp(0.0, 1.0) * 100);
setState(() {
this.progress = updated.round() / 100;
});
print(progress);
},
child: Text(
'-10%',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
),
)),
),
],
);
}
}
class ChangeBackgroundColor StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: _changeColor(),
initialData: 0xff0DE95B,
builder: (context, snapshot) {
return LinearPercentIndicator(
percent: 1,
animation: true,
animationDuration: 30000,
progressColor: Colors.grey[200],
backgroundColor: Color(
int.parse(snapshot.data.toString()),
)),
);
},
);
}
}
Stream<int> _changeColor() async* {
yield* Stream.periodic(Duration(seconds: 1), (int a) {
a++;
if (a > 25) {
return 0xffF33709;
} else {
return 0xff0DE95B;
}
});
}

How to create a range slider with thumb as png image in flutter

How to create smiley range slider in flutter. Like below GIF image.
Discrete with Custom Theme I try to change thumb shape. But I want to change thumb as image.
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'package:flutter/material.dart';
class SliderDemo extends StatefulWidget {
static const String routeName = '/material/slider';
#override
_SliderDemoState createState() => new _SliderDemoState();
}
Path _triangle(double size, Offset thumbCenter, {bool invert = false}) {
final Path thumbPath = new Path();
final double height = math.sqrt(3.0) / 2.0;
final double halfSide = size / 2.0;
final double centerHeight = size * height / 3.0;
final double sign = invert ? -1.0 : 1.0;
thumbPath.moveTo(thumbCenter.dx - halfSide, thumbCenter.dy + sign * centerHeight);
thumbPath.lineTo(thumbCenter.dx, thumbCenter.dy - 2.0 * sign * centerHeight);
thumbPath.lineTo(thumbCenter.dx + halfSide, thumbCenter.dy + sign * centerHeight);
thumbPath.close();
return thumbPath;
}
class _CustomThumbShape extends SliderComponentShape {
static const double _thumbSize = 4.0;
static const double _disabledThumbSize = 5.0;
#override
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
return isEnabled ? const Size.fromRadius(_thumbSize) : const Size.fromRadius(_disabledThumbSize);
}
static final Tween<double> sizeTween = new Tween<double>(
begin: _disabledThumbSize,
end: _thumbSize,
);
#override
void paint(
PaintingContext context,
Offset thumbCenter, {
Animation<double> activationAnimation,
Animation<double> enableAnimation,
bool isDiscrete,
TextPainter labelPainter,
RenderBox parentBox,
SliderThemeData sliderTheme,
TextDirection textDirection,
double value,
}) {
final Canvas canvas = context.canvas;
final ColorTween colorTween = new ColorTween(
begin: sliderTheme.disabledThumbColor,
end: sliderTheme.thumbColor,
);
final double size = _thumbSize * sizeTween.evaluate(enableAnimation);
final Path thumbPath = _triangle(size, thumbCenter);
canvas.drawPath(thumbPath, new Paint()..color = colorTween.evaluate(enableAnimation));
}
}
class _CustomValueIndicatorShape extends SliderComponentShape {
static const double _indicatorSize = 4.0;
static const double _disabledIndicatorSize = 3.0;
static const double _slideUpHeight = 40.0;
#override
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
return new Size.fromRadius(isEnabled ? _indicatorSize : _disabledIndicatorSize);
}
static final Tween<double> sizeTween = new Tween<double>(
begin: _disabledIndicatorSize,
end: _indicatorSize,
);
#override
void paint(
PaintingContext context,
Offset thumbCenter, {
Animation<double> activationAnimation,
Animation<double> enableAnimation,
bool isDiscrete,
TextPainter labelPainter,
RenderBox parentBox,
SliderThemeData sliderTheme,
TextDirection textDirection,
double value,
}) {
final Canvas canvas = context.canvas;
final ColorTween enableColor = new ColorTween(
begin: sliderTheme.disabledThumbColor,
end: sliderTheme.valueIndicatorColor,
);
final Tween<double> slideUpTween = new Tween<double>(
begin: 0.0,
end: _slideUpHeight,
);
final double size = _indicatorSize * sizeTween.evaluate(enableAnimation);
final Offset slideUpOffset = new Offset(0.0, -slideUpTween.evaluate(activationAnimation));
final Path thumbPath = _triangle(
size,
thumbCenter + slideUpOffset,
invert: true,
);
final Color paintColor = enableColor.evaluate(enableAnimation).withAlpha((255.0 * activationAnimation.value).round());
canvas.drawPath(
thumbPath,
new Paint()..color = paintColor,
);
canvas.drawLine(
thumbCenter,
thumbCenter + slideUpOffset,
new Paint()
..color = paintColor
..style = PaintingStyle.stroke
..strokeWidth = 2.0);
labelPainter.paint(canvas, thumbCenter + slideUpOffset + new Offset(-labelPainter.width / 2.0, -labelPainter.height - 4.0));
}
}
class _SliderDemoState extends State<SliderDemo> {
double _value = 25.0;
double _discreteValue = 20.0;
#override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
return new Scaffold(
appBar: new AppBar(title: const Text('Sliders')),
body: new Padding(
padding: const EdgeInsets.symmetric(horizontal: 40.0),
child: new Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
new Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
new Slider(
value: _value,
min: 0.0,
max: 100.0,
onChanged: (double value) {
setState(() {
_value = value;
});
},
),
const Text('Continuous'),
],
),
new Column(
mainAxisSize: MainAxisSize.min,
children: const <Widget>[
const Slider(value: 0.25, onChanged: null),
const Text('Disabled'),
],
),
new Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
new Slider(
value: _discreteValue,
min: 0.0,
max: 200.0,
divisions: 5,
label: '${_discreteValue.round()}',
onChanged: (double value) {
setState(() {
_discreteValue = value;
});
},
),
const Text('Discrete'),
],
),
new Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
new SliderTheme(
data: theme.sliderTheme.copyWith(
activeTrackColor: Colors.deepPurple,
inactiveTrackColor: Colors.black26,
activeTickMarkColor: Colors.white70,
inactiveTickMarkColor: Colors.black,
overlayColor: Colors.black12,
thumbColor: Colors.red,
valueIndicatorColor: Colors.deepPurpleAccent,
thumbShape: new _CustomThumbShape(),
valueIndicatorShape: new _CustomValueIndicatorShape(),
valueIndicatorTextStyle: theme.accentTextTheme.body2.copyWith(color: Colors.black87),
),
child: new Slider(
value: _discreteValue,
min: 0.0,
max: 10.0,
divisions: 5,
//semanticFormatterCallback: (double value) => value.round().toString(),
label: '${_discreteValue.round()}',
onChanged: (double value) {
setState(() {
_discreteValue = value;
});
},
),
),
const Text('Discrete with Custom Theme'),
],
),
],
),
),
);
}
}
You need to create custom slider.
Let’s split tasks:
Gestures:
Tap, when user taps on the one of the labels.
Drag, when user drags the indicator.
Animations:
Background animations
moving text
scaling head
Switcher animations:
color
face
Extra:
- Need to think what to do when user stop drugging between two labels.
After 15 hours of thinking and coding, I have:
import 'package:flutter/material.dart';
import 'package:vector_math/vector_math.dart' as v_math;
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
body: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('How was the help you recived?', style: TextStyle(color: Color(0xFF6f7478), fontSize: 18),),
SizedBox(height: 20),
ReviewSlider()
],
),
),
),
);
}
}
class ReviewSlider extends StatefulWidget {
#override
_ReviewSliderState createState() => _ReviewSliderState();
}
class _ReviewSliderState extends State<ReviewSlider> with SingleTickerProviderStateMixin {
double intitalReviewValue = 2;
final List<String> reviews = ['Terrible', 'Bad', 'Okay', 'Good', 'Great'];
Animation<double> _animation;
AnimationController _controller;
Tween<double> _tween;
double _innerWidth;
double _animationValue;
#override
void initState() {
super.initState();
_controller = AnimationController(
value: intitalReviewValue,
vsync: this,
duration: Duration(milliseconds: 400),
);
_tween = Tween(end: intitalReviewValue);
_animation = _tween.animate(
CurvedAnimation(
curve: Curves.easeIn,
parent: _controller,
),
)..addListener(() {
setState(() {
_animationValue = _animation.value;
});
});
_animationValue = intitalReviewValue;
WidgetsBinding.instance.addPostFrameCallback(_afterLayout);
}
_afterLayout(_) {
setState(() {
_innerWidth = MediaQuery.of(context).size.width - 2 * paddingSize;
});
}
void handleTap(int state) {
_controller.duration = Duration(milliseconds: 400);
_tween.begin = _tween.end;
_tween.end = state.toDouble();
_controller.reset();
_controller.forward();
}
_onDrag(details) {
var newAnimatedValue = _calcAnimatedValueFormDragX(
details.globalPosition.dx,
);
if (newAnimatedValue > 0 && newAnimatedValue < reviews.length - 1) {
setState(
() {
_animationValue = newAnimatedValue;
},
);
}
}
_calcAnimatedValueFormDragX(x) {
return (x - circleDiameter / 2 - paddingSize * 2) / _innerWidth * reviews.length;
}
_onDragEnd(_) {
_controller.duration = Duration(milliseconds: 100);
_tween.begin = _animationValue;
_tween.end = _animationValue.round().toDouble();
_controller.reset();
_controller.forward();
}
#override
void dispose() {
super.dispose();
_controller.dispose();
}
#override
Widget build(BuildContext context) {
return Center(
child: _innerWidth == null
? Container()
: Container(
padding: EdgeInsets.symmetric(horizontal: paddingSize),
height: 200,
child: Stack(children: <Widget>[
MeasureLine(
states: reviews,
handleTap: handleTap,
animationValue: _animationValue,
width: _innerWidth,
),
MyIndicator(
animationValue: _animationValue,
width: _innerWidth,
onDrag: _onDrag,
onDragEnd: _onDragEnd,
),
Text(_animationValue.round().toString()),
]),
),
);
}
}
const double circleDiameter = 60;
const double paddingSize = 10;
class MeasureLine extends StatelessWidget {
MeasureLine({this.handleTap, this.animationValue, this.states, this.width});
final double animationValue;
final Function handleTap;
final List<String> states;
final double width;
List<Widget> _buildUnits() {
var res = <Widget>[];
var animatingUnitIndex = animationValue.round();
var unitAnimatingValue = (animationValue * 10 % 10 / 10 - 0.5).abs() * 2;
states.asMap().forEach((index, text) {
var paddingTop = 0.0;
var scale = 0.7;
var opacity = .3;
if (animatingUnitIndex == index) {
paddingTop = unitAnimatingValue * 5;
scale = (1 - unitAnimatingValue) * 0.7;
opacity = 0.3 + unitAnimatingValue * 0.7;
}
res.add(LimitedBox(
key: ValueKey(text),
maxWidth: circleDiameter,
child: GestureDetector(
onTap: () {
handleTap(index);
},
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Transform.scale(
scale: scale,
child: Stack(
children: [
Head(),
Face(
color: Colors.white,
animationValue: index.toDouble(),
)
],
)),
Padding(
padding: EdgeInsets.only(top: paddingTop),
child: Opacity(
opacity: opacity,
child: Text(
text,
style: TextStyle(color: Colors.black),
),
),
)
],
),
),
));
});
return res;
}
#override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Positioned(
top: circleDiameter / 2,
left: 20,
width: width - 40,
child: Container(
width: width,
color: Color(0xFFeceeef),
height: 3,
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildUnits(),
),
],
);
}
}
class Face extends StatelessWidget {
Face({
this.color = const Color(0xFF616154),
this.animationValue,
});
final Color color;
final double animationValue;
#override
Widget build(BuildContext context) {
return Container(
height: circleDiameter,
width: circleDiameter,
child: CustomPaint(
size: Size(300, 300),
painter: MyPainter(animationValue, color: color),
),
);
}
}
class MyPainter extends CustomPainter {
MyPainter(
animationValue, {
this.color = const Color(0xFF615f56),
}) : activeIndex = animationValue.floor(),
unitAnimatingValue = (animationValue * 10 % 10 / 10);
Color color;
final int activeIndex;
final double unitAnimatingValue;
#override
void paint(Canvas canvas, Size size) {
_drawEye(canvas, size);
_drawMouth(canvas, size);
}
_drawEye(canvas, size) {
var angle = 0.0;
var wide = 0.0;
switch (activeIndex) {
case 0:
angle = 55 - unitAnimatingValue * 50;
wide = 80.0;
break;
case 1:
wide = 80 - unitAnimatingValue * 80;
angle = 5;
break;
}
var degree1 = 90 * 3 + angle;
var degree2 = 90 * 3 - angle + wide;
var x1 = size.width / 2 * 0.65;
var x2 = size.width - x1;
var y = size.height * 0.41;
var eyeRadius = 5.0;
var paint = Paint()..color = color;
canvas.drawArc(
Rect.fromCircle(
center: Offset(x1, y),
radius: eyeRadius,
),
v_math.radians(degree1),
v_math.radians(360 - wide),
false,
paint,
);
canvas.drawArc(
Rect.fromCircle(
center: Offset(x2, y),
radius: eyeRadius,
),
v_math.radians(degree2),
v_math.radians(360 - wide),
false,
paint,
);
}
_drawMouth(Canvas canvas, size) {
var upperY = size.height * 0.70;
var lowerY = size.height * 0.77;
var middleY = (lowerY - upperY) / 2 + upperY;
var leftX = size.width / 2 * 0.65;
var rightX = size.width - leftX;
var middleX = size.width / 2;
double y1, y3, x2, y2;
Path path2;
switch (activeIndex) {
case 0:
y1 = lowerY;
x2 = middleX;
y2 = upperY;
y3 = lowerY;
break;
case 1:
y1 = lowerY;
x2 = middleX;
y2 = unitAnimatingValue * (middleY - upperY) + upperY;
y3 = lowerY - unitAnimatingValue * (lowerY - upperY);
break;
case 2:
y1 = unitAnimatingValue * (upperY - lowerY) + lowerY;
x2 = middleX;
y2 = unitAnimatingValue * (lowerY + 3 - middleY) + middleY;
y3 = upperY;
break;
case 3:
y1 = upperY;
x2 = middleX;
y2 = lowerY + 3;
y3 = upperY;
path2 = Path()
..moveTo(leftX, y1)
..quadraticBezierTo(
x2,
y2,
upperY - 2.5,
y3 - 2.5,
)
..quadraticBezierTo(
x2,
y2 - unitAnimatingValue * (y2 - upperY + 2.5),
leftX,
upperY - 2.5,
)
..close();
break;
case 4:
y1 = upperY;
x2 = middleX;
y2 = lowerY + 3;
y3 = upperY;
path2 = Path()
..moveTo(leftX, y1)
..quadraticBezierTo(
x2,
y2,
upperY - 2.5,
y3 - 2.5,
)
..quadraticBezierTo(
x2,
upperY - 2.5,
leftX,
upperY - 2.5,
)
..close();
break;
}
var path = Path()
..moveTo(leftX, y1)
..quadraticBezierTo(
x2,
y2,
rightX,
y3,
);
canvas.drawPath(
path,
Paint()
..color = color
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round
..strokeWidth = 5);
if (path2 != null) {
canvas.drawPath(
path2,
Paint()
..color = color
..style = PaintingStyle.fill
..strokeCap = StrokeCap.round,
);
}
}
#override
bool shouldRepaint(MyPainter oldDelegate) {
return unitAnimatingValue != oldDelegate.unitAnimatingValue ||
activeIndex != oldDelegate.activeIndex;
}
}
class MyIndicator extends StatelessWidget {
MyIndicator({
this.animationValue,
width,
this.onDrag,
this.onDragStart,
this.onDragEnd,
}) : width = width - circleDiameter,
possition = animationValue == 0 ? 0 : animationValue / 4;
final double possition;
final Function onDrag;
final Function onDragStart;
final Function onDragEnd;
final double width;
final double animationValue;
#override
Widget build(BuildContext context) {
return Container(
child: Positioned(
top: 0,
left: width * possition,
child: _buildIndicator(),
),
);
}
_buildIndicator() {
var opacityOfYellow = possition > 0.5 ? 1.0 : possition * 2;
return GestureDetector(
onPanDown: onDragStart,
onPanUpdate: onDrag,
onPanStart: onDrag,
onPanEnd: onDragEnd,
child: Container(
width: circleDiameter,
height: circleDiameter,
child: Stack(
children: <Widget>[
Head(
color: Color(0xFFf4b897),
hasShadow: true,
),
Opacity(
opacity: opacityOfYellow,
child: Head(
color: Color(0xFFfee385),
),
),
Face(
animationValue: animationValue,
)
],
),
),
);
}
}
class Head extends StatelessWidget {
Head({this.color = const Color(0xFFc9ced2), this.hasShadow = false});
final Color color;
final bool hasShadow;
#override
Widget build(BuildContext context) {
return Container(
height: circleDiameter,
width: circleDiameter,
decoration: BoxDecoration(
boxShadow: hasShadow
? [BoxShadow(color: Colors.black26, offset: Offset(0, 2), blurRadius: 5.0)]
: null,
color: color,
shape: BoxShape.circle,
),
);
}
}
https://github.com/kherel/review_slider