Moving Circle in customer painter with gesture detector not working - flutter

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

Related

How to create a border like this in Flutter?

How to create a border like this:
You can achieve a similar clipped border using CustomClipper. Here is a simple CustomClipper I have created for you.
First Create a custom clipper.
class MyClipper extends CustomClipper<Path> {
#override
Path getClip(Size size) {
var smallLineLength = size.width / 20;
const smallLineHeight = 20;
var path = Path();
path.lineTo(0, size.height);
for (int i = 1; i <= 20; i++) {
if (i % 2 == 0) {
path.lineTo(smallLineLength * i, size.height);
} else {
path.lineTo(smallLineLength * i, size.height - smallLineHeight);
}
}
path.lineTo(size.width, 0);
path.close();
return path;
}
#override
bool shouldReclip(CustomClipper old) => false;
}
And wrap the created CustomClipper with ClipPath.
SizedBox(
height: 200,
width: 500,
child: ClipPath(
clipper: MyClipper(),
child: Container(
height: 200,
width: 500,
alignment: Alignment.center,
color: Colors.red,
child: const Text("abc"),
)),
),
Full Code:
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(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: MyWidget(),
),
);
}
}
class MyWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Center(
child: Column(crossAxisAlignment: CrossAxisAlignment.center, children: [
SizedBox(
height: 200,
width: 500,
child: ClipPath(
clipper: MyClipper(),
child: Container(
height: 200,
width: 500,
alignment: Alignment.center,
color: Colors.red,
child: const Text("abc"),
)),
),
]),
);
}
}
class MyClipper extends CustomClipper<Path> {
#override
Path getClip(Size size) {
var smallLineLength = size.width / 20;
const smallLineHeight = 20;
var path = Path();
path.lineTo(0, size.height);
for (int i = 1; i <= 20; i++) {
if (i % 2 == 0) {
path.lineTo(smallLineLength * i, size.height);
} else {
path.lineTo(smallLineLength * i, size.height - smallLineHeight);
}
}
path.lineTo(size.width, 0);
path.close();
return path;
}
#override
bool shouldReclip(CustomClipper old) => false;
}
You can run this code by copying/pasting in dartpad.
You can learn more about CustomClipper from here, medium article
Try below code hope its helpful to you. Used flutter_custom_clippers package here and used PointsClipper() for your expected design In this package you used more shapes see documentation in given link. add flutter_custom_clippers: ^2.0.0 dependency in your pubspec.yaml file
ClipPath(
clipper: PointsClipper(),
child: Container(
height: 100,
color: Colors.grey[300],
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
'Total',
style: TextStyle(
color: Colors.pink,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
Text(
'QAR 130.00',
style: TextStyle(
color: Colors.pink,
fontSize: 15,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
),
Your result screen->
import 'package:flutter/material.dart';
import 'a.dart';
void main() => runApp(VideoPlayerApp());
class VideoPlayerApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Zigzag',
home: SafeArea(
child: Scaffold(
body: ZigzagApp(),
),
),
);
}
}
class ZigzagApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
height: 200,
color: Colors.pinkAccent,
child: CustomPaint(
size: MediaQuery.of(context).size,
painter: MyPainter(),
),
);
}
}
//paint widget for zigzag
import 'package:flutter/material.dart';
import 'dart:math' as math;
class MyPainter extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
var paint = Paint();
paint.color = Colors.white;
paint.style = PaintingStyle.fill;
paintZigZag(canvas, paint, Offset(0, 200), Offset(400, 200), 35, 10);
}
void paintZigZag(Canvas canvas, Paint paint, Offset start, Offset end,
int zigs, double width) {
assert(zigs.isFinite);
assert(zigs > 0);
canvas.save();
canvas.translate(start.dx, start.dy);
end = end - start;
canvas.rotate(math.atan2(end.dy, end.dx));
final double length = end.distance;
final double spacing = length / (zigs * 2.0);
final Path path = Path()..moveTo(0.0, 0.0);
for (int index = 0; index < zigs; index += 1) {
final double x = (index * 2.0 + 1.0) * spacing;
final double y = width * ((index % 2.0) * 2.0 - 1.0);
path.lineTo(x, y);
}
path.lineTo(length, 0.0);
canvas.drawPath(path, paint);
canvas.restore();
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}

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:

Flutter : Need help to implement card with image

I need to implement a design similar to the image displayed below:
Here is a possible solution with a CustomPainter and a Path to generate the drawing. I used the cubicTo and a addRect functions of the Path class. For more information about these functions and path drawing, I refer to this: Paths in Flutter: A Visual Guide
This is the class which extends the CustomPainter
class CardPainter extends CustomPainter {
final Color color;
final double strokeWidth;
CardPainter({this.color = Colors.black, this.strokeWidth = 3});
#override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = color
..strokeWidth = this.strokeWidth
..style = PaintingStyle.stroke;
canvas.drawPath(getShapePath(size.width, size.height), paint);
}
// This is the path of the shape we want to draw
Path getShapePath(double x, double y) {
return Path()
..moveTo(2 * x / 3, y)
..cubicTo(x / 1.5, y, 0, y / 3, x, 0) // draw cubic curve
..addRect(Rect.fromLTRB(0, 0, x, y)); // draw rectangle
}
#override
bool shouldRepaint(CardPainter oldDelegate) {
return oldDelegate.color != color;
}
}
Here is the implementation of the CustomPainter
class CustomCard extends StatelessWidget {
CustomCard({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Custom Card Demo')),
body: Center(
child: Container(
color: Colors.grey[300],
width: 250,
height: 120,
padding: EdgeInsets.fromLTRB(0, 0, 0, 0),
child: CustomPaint(
painter: CardPainter(color: Colors.blueAccent, strokeWidth: 3), // this is your custom painter
child: Stack(
children: <Widget>[
Positioned(
top: 25,
left: 30,
child: Text('Text1'),
),
Positioned(
top: 55,
left: 150,
child: Text('Text2'),
),
],
),
),
),
),
);
}
}

How to add line dash in 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
),
),

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