The point where I tap creates a line from the center to that point. But currently I can't get the coordinates of the tap that match the canvas coordinates. Is there any way to change the scaffold origin (0,0) and set its origin to the same as the container origin?
If bastman hits run then I want to show the direction where he/she runs with a black line.
import 'dart:ffi';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'dart:ui' as ui;
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> {
final GlobalKey _cardkey = GlobalKey();
double posx = 100.0;
double posy = 100.0;
Size? cardSize;
Offset? cardPosition;
var height = AppBar().preferredSize.height;
ui.Image? image;
void onTapDown(BuildContext context, TapDownDetails details) {
// print('${details.globalPosition}');
final RenderBox box = context.findRenderObject() as RenderBox;
final Offset localOffset = box.globalToLocal(details.globalPosition);
setState(() {
posx = localOffset.dx;
posy = localOffset.dy;
});
print(posx);
print(posy);
}
#override
void initState() {
// TODO: implement initState
super.initState();
// WidgetsBinding.instance!.addPostFrameCallback((_) {getSizeAndPosition(); });
loadImage('assets/images/wheel.png');
}
Future loadImage(String path) async {
final data = await rootBundle.load(path);
final bytes = data.buffer.asUint8List();
final image = await decodeImageFromList(bytes);
setState(() {
this.image = image;
});
}
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
// appBar: AppBar(
// title: Text("Canvas"),
// ),
body: Center(
child: Container(
decoration: BoxDecoration(
border: Border.all(),
shape: BoxShape.circle,
color: Colors.green),
height: double.infinity,
width: double.infinity,
child: GestureDetector(
key: _cardkey,
// onTapDown: (TapDownDetails details){
// onTapDown(context, details);
// },
onTap: (){},
onPanStart: (details) {
Offset position = details.localPosition;
setState(() {
posx = position.dx;
posy = position.dy;
});
print(posx);
print(posy);
},
onPanUpdate: (DragUpdateDetails details) {
Offset position = details.localPosition;
setState(() {
posx = position.dx;
posy = position.dy;
});
},
child: FittedBox(
child: SizedBox(
width: image!.width.toDouble(),
height: image!.height.toDouble(),
child: CustomPaint(
// key: _cardkey,
painter: ImagePainter(posx, posy, image),
),
),
),
),
),
),
),
);
}
}
class ImagePainter extends CustomPainter {
double? posX;
double? posY;
ui.Image? images;
ImagePainter(double posx, double posy, this.images) {
posX = posx;
posY = posy;
}
#override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final CenterX = size.width / 2;
final CenterY = size.height / 2;
final paintCircle = Paint()
..strokeWidth = 5
..color = Colors.green
..style = PaintingStyle.stroke;
// canvas.translate(size.width/2, -size.height/2);
// canvas.scale(1,-1);
final paintImage = Paint()
..strokeWidth = 5
..color = Colors.white
..style = PaintingStyle.stroke;
// canvas.translate(size.width/2, -size.height/2);
// canvas.scale(1,-1);
final paintLine = Paint()
..strokeWidth = 5
..color = Colors.black
..style = PaintingStyle.stroke;
canvas.drawCircle(center, size.width / 2, paintCircle);
var circleCenter = Offset(size.width / 2, size.height / 2);
var radius = size.width / 8;
canvas.drawImage(images!, Offset(0, 0), paintImage);
var line = canvas.drawLine(center, Offset(posX!, posY!), paintLine);
// print("$line");
}
#override
bool shouldRepaint(CustomPainter olddelegate) => true;
}
Dont use a GestureDetector. It won't give you the position of a single tap. If you want the position of a onTap you need to use a Listener:
Listener(
onPointerDown: (event) {
setState(() {
posx = event.localPosition.dx;
posy = event.localPosition.dy;
}
}
),
The localPosition property should match the coordinates of the container IF you don't resize the child of the container. If they are not correct for some reason use event.position instead which will give you coordinates in the global coordinate space.
Related
In my flutter application I want to animate the color change (the color is a gradient) of two elements that inherit from a custom painter, this happens when a button is clicked.
My problem is that I can't find how to do it, since I understand that to animate an element a Tween is needed.
I cannot find a Tween that is correct for my case, since the color change is done in the CustomPainter conditionally.
The TweenColor class works, however I can't integrate it with a gradient and this doesn't work for me.
This effect is the one I want:
https://vimeo.com/578534813/e0f2d4ed73
This is what I have without using the Tween Color, it is fine, however, it is not the animated effect that I want.
https://vimeo.com/578538292/6e63a51060
As you can see, it is close to what is required, but it is not.
Thanks for your help, I leave the code.
ScreenClass
class AuthScreen extends StatefulWidget {
#override
_AuthScreenState createState() => _AuthScreenState();
}
class _AuthScreenState extends State<AuthScreen> with SingleTickerProviderStateMixin
{
late AnimationController controller;
late Animation<double> opacity;
#override
void initState() {
controller = new AnimationController(vsync: this,duration: Duration(seconds:2 ));
opacity = Tween(begin: 0.1 ,end: 1.0).animate(controller);
super.initState();
}
#override
void dispose() {
controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
controller.forward();
final authProvider = Provider.of<AuthProvider>(context,listen: false);
return Scaffold(
body: Stack(
children: [
Container(
width: double.infinity,
height: double.infinity,
child: AnimatedBuilder(
animation: controller,
builder: (_,Widget? child){
return Opacity(
opacity: opacity.value,
child: CustomPaint(
painter: BackgroundPainter(provider:authProvider),
),
);
},
),
),
Center(
child: MaterialButton(
onPressed: (){
controller.reset();
controller.forward();
authProvider.flag = !authProvider.flag;
},
shape: RoundedRectangleBorder(),
elevation: 12,
color: Colors.white,
child: Text('Ingresar'),
),
),
])
);
}
}
ProviderClass
class AuthProvider extends ChangeNotifier {
bool _flag = false;
bool get flag {
print(_flag);
return this._flag;
}
set flag(bool state) => this._flag = state;
}
CustomPaintClass
class BackgroundPainter extends CustomPainter {
AuthProvider provider;
BackgroundPainter({required this.provider});
final Gradient redColor = new LinearGradient(colors: <Color>[
Colors.white12,
Color(0xffF7A2A3),
]);
final Gradient blueColor = new LinearGradient(colors: <Color>[
Colors.white12,
Color(0xff74BDD7),
]);
#override
void paint(Canvas canvas, Size size) {
final Rect rect = Rect.fromCircle(
center: Offset(55.0, 155.0),
radius: 180);
/**
* *Header*
*/
Gradient color ;
if(!provider.flag)
color = redColor;
else
color = blueColor;
final paint = new Paint()..shader = color.createShader(rect);
paint.style = PaintingStyle.fill; // .fill .stroke
paint.strokeWidth = 20;
final path = new Path();
path.moveTo(size.width, 0);
path.lineTo(size.width, size.height * 0.20);
path.quadraticBezierTo(
size.width * 0.95, size.height * 0.05, 0, size.height * 0.06);
path.lineTo(0, 0);
canvas.drawPath(path, paint);
final Rect rect2 = Rect.fromCircle(
center: Offset(55.0, 155.0), // x = horizontal y = vertical
radius: 180);
/**
* *Footer*
*/
Gradient color2 ;
if(!provider.flag)
color2 = blueColor;
else
color2 = redColor;
final paint2 = new Paint()..shader = color2.createShader(rect2);
paint2.style = PaintingStyle.fill;
paint2.strokeWidth = 30;
final path2 = new Path();
path2.moveTo(0, size.height);
path2.lineTo(0, size.height * 0.80);
path2.quadraticBezierTo(
size.width * 0.05, size.height * 0.95, size.width, size.height * 0.94);
path2.lineTo(size.width, size.height);
canvas.drawPath(path2, paint2);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
How do you draw a diagram style circle border with multiple values? Also animated that each value in circle expands dynamically filling 100% of the circle?
Animation can be handled by TweenAnimationBuilder and it will be played on build.
To achieve desired result we must use customPainter.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'dart:math' as math;
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
debugShowCheckedModeBanner: false,
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: TweenAnimationBuilder(
duration: const Duration(seconds: 2),
tween: Tween(begin: 0.0, end: 1.0),
curve: Curves.easeOutCubic,
builder: (BuildContext context, dynamic value, Widget child) {
return CustomPaint(
painter: OpenPainter(
totalQuestions: 300,
learned: 75,
notLearned: 75,
range: value),
);
},
),
),
);
}
}
class OpenPainter extends CustomPainter {
final learned;
final notLearned;
final range;
final totalQuestions;
double pi = math.pi;
OpenPainter({this.learned, this.totalQuestions, this.notLearned, this.range});
#override
void paint(Canvas canvas, Size size) {
double strokeWidth = 7;
Rect myRect = const Offset(-50.0, -50.0) & const Size(100.0, 100.0);
var paint1 = Paint()
..color = Colors.red
..strokeWidth = strokeWidth
..style = PaintingStyle.stroke;
var paint2 = Paint()
..color = Colors.green
..strokeWidth = strokeWidth
..style = PaintingStyle.stroke;
var paint3 = Paint()
..color = Colors.yellow
..strokeWidth = strokeWidth
..style = PaintingStyle.stroke;
double firstLineRadianStart = 0;
double _unAnswered = (totalQuestions - notLearned - learned) * range / totalQuestions;
double firstLineRadianEnd = (360 * _unAnswered) * math.pi / 180;
canvas.drawArc(
myRect, firstLineRadianStart, firstLineRadianEnd, false, paint1);
double _learned = (learned) * range / totalQuestions;
double secondLineRadianEnd = getRadians(_learned);
canvas.drawArc(myRect, firstLineRadianEnd, secondLineRadianEnd, false, paint2);
double _notLearned = (notLearned) * range / totalQuestions;
double thirdLineRadianEnd = getRadians(_notLearned);
canvas.drawArc(myRect, firstLineRadianEnd + secondLineRadianEnd, thirdLineRadianEnd, false, paint3);
// drawArc(Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint)
}
double getRadians(double value) {
return (360 * value) * pi / 180;
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
Hopefully someone will find this helpfull :) Feel free to improve on this! Happy coding !
Thanks to Paulius Greičiūnas answer, I implemented a more general way to paint a circle in different colors. You only have to specify the occurrences of the colors as a map and a size of the circle.
class MultipleColorCircle extends StatelessWidget {
final Map<Color, int> colorOccurrences;
final double height;
final Widget? child;
#override
MultipleColorCircle(
{required this.colorOccurrences, this.height = 20, this.child});
Widget build(BuildContext context) => Container(
height: height,
width: height,
child: CustomPaint(
size: Size(20, 20),
child: Center(child: child),
painter: _MultipleColorCirclePainter(
colorOccurrences: colorOccurrences,
height: height,
)),
);
}
class _MultipleColorCirclePainter extends CustomPainter {
final Map<Color, int> colorOccurrences;
final double height;
#override
_MultipleColorCirclePainter(
{required this.colorOccurrences, required this.height});
double pi = math.pi;
#override
void paint(Canvas canvas, Size size) {
double strokeWidth = 1;
Rect myRect =
Rect.fromCircle(center: Offset(height / 2, height / 2), radius: height);
double radianStart = 0;
double radianLength = 0;
int allOccurrences = 0;
//set denominator
colorOccurrences.forEach((color, occurrence) {
allOccurrences += occurrence;
});
colorOccurrences.forEach((color, occurrence) {
double percent = occurrence / allOccurrences;
radianLength = 2 * percent * math.pi;
canvas.drawArc(
myRect,
radianStart,
radianLength,
false,
Paint()
..color = color
..strokeWidth = strokeWidth
..style = PaintingStyle.stroke);
radianStart += radianLength;
});
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
With a map e.g. {Colors.blue: 2, Colors.green: 1} you will get a circle with 1/3 green and 2/3 blue.
Note, that you can also define a child, so that the circle has content in it. Here is an example, which I used in my calendar, of multiple circles with content in it.
What I have is a play button that plays back user recorded message. Once the user hits the play button it changes into a stop button that displays a circular progress indicator that progresses based on a percentage of the recorded message total time and the current track time.
What I have somewhat worked, but isn't exact enough, depending on how long the track is (4 seconds vs 6.5 seconds) the track will run a bit longer after the circular progress indicator is done or the track will end before the progress indicator is done.
I would also love the progression to be smooth and not jump in intervals.
Here is the code for the stop button which takes a double totalTime which is the total time of the track playing and then starts a timer and AnimationController.
Also full transparency, the custom painter is something I found online and don't fully understand how it works, so even if there isn't an issue there if someone could break that down it would be seriously appreciated :D
import 'dart:async';
import 'dart:math';
import 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_icons/flutter_icons.dart';
class StopButton extends StatefulWidget {
#override
_StopButtonState createState() => _StopButtonState();
final double totalTime;
final dynamic onClickFunction;
StopButton(this.totalTime, this.onClickFunction);
}
class _StopButtonState extends State<StopButton> with TickerProviderStateMixin {
double percentage = 0.0;
double newPercentage = 0.0;
AnimationController percentageAnimationController;
Timer timer;
#override
void initState() {
// TODO: implement initState
super.initState();
setState(() {
percentage = 0.0;
});
percentageAnimationController =
AnimationController(vsync: this, duration: Duration(milliseconds: 1000))
..addListener(() {
setState(() {
percentage = lerpDouble(percentage, newPercentage,
percentageAnimationController.value);
});
});
startTime();
}
#override
void dispose() {
// TODO: implement dispose
timer.cancel();
percentageAnimationController.dispose();
super.dispose();
}
void startTime() {
setState(() {
percentage = newPercentage;
newPercentage += 0.0;
if (newPercentage > widget.totalTime) {
percentage = 0.0;
newPercentage = 0.0;
timer.cancel();
}
percentageAnimationController.forward(from: 0.0);
});
timer = Timer.periodic(Duration(seconds: 1), (timer) {
print(timer.tick);
setState(() {
percentage = newPercentage;
newPercentage += 1.0;
if (newPercentage > widget.totalTime) {
percentage = 0.0;
newPercentage = 0.0;
timer.cancel();
}
percentageAnimationController.forward(from: 0.0);
});
});
}
#override
Widget build(BuildContext context) {
return CustomPaint(
foregroundPainter: MyPainter(
lineColor: Colors.transparent,
completeColor: Color(0xFF133343),
completePercent: percentage,
width: 3.0,
totalTime: widget.totalTime,
),
child: Padding(
padding: EdgeInsets.all(0.0),
child: FloatingActionButton(
onPressed: () async {
await widget.onClickFunction();
},
backgroundColor: Colors.white,
child: Icon(
MaterialCommunityIcons.stop,
color: Color(0xFF133343),
),
),
),
);
}
}
class MyPainter extends CustomPainter {
Color lineColor;
Color completeColor;
double completePercent;
double width;
double totalTime;
MyPainter(
{this.lineColor,
this.completeColor,
this.completePercent,
this.width,
this.totalTime});
#override
void paint(Canvas canvas, Size size) {
Paint line = Paint()
..color = lineColor
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke
..strokeWidth = width;
Paint complete = Paint()
..color = completeColor
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke
..strokeWidth = width;
Offset center = Offset(size.width / 2, size.height / 2);
double radius = min(size.width / 2, size.height / 2);
canvas.drawCircle(center, radius, line);
double arcAngle = 2 * pi * (completePercent / totalTime);
canvas.drawArc(Rect.fromCircle(center: center, radius: radius), -pi / 2,
arcAngle, false, complete);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
you have add below custom CustomTimerPainter for create circular indicator
class CustomTimerPainter extends CustomPainter {
CustomTimerPainter({
this.animation,
this.backgroundColor,
this.color,
}) : super(repaint: animation);
final Animation<double> animation;
final Color backgroundColor, color;
#override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = backgroundColor
..strokeWidth = 6.0
..strokeCap = StrokeCap.butt
..style = PaintingStyle.stroke;
canvas.drawCircle(size.center(Offset.zero), size.width / 2.0, paint);
paint.color = color;
double progress = (1.0 - animation.value) * 2 * math.pi;
canvas.drawArc(Offset.zero & size, math.pi * 1.5, progress, false, paint);
}
#override
bool shouldRepaint(CustomTimerPainter old) {
return animation.value != old.animation.value ||
color != old.color ||
backgroundColor != old.backgroundColor;
}
}
after adding indicator define controller
AnimationController controller;
#override
void initState() {
super.initState();
controller = AnimationController(
vsync: this,
duration: Duration(seconds: 5),
);
}
last step is add our custom painter
floatingActionButton: Container(
height: 60,
width: 60,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white
),
child: GestureDetector(
child: CustomPaint(
painter: CustomTimerPainter(
animation: controller,
backgroundColor: Colors.white,
color: themeData.indicatorColor,
)),
onTap: (){
controller.reverse(
from: controller.value == 0.0
? 1.0
: controller.value);
},
),
),
I am trying to create some custom widgets using CustomPaint and GestureDetector. However, I am unable to properly interact with the drawn shapes using the GestureDetector. I can detect onTap for only one shape if the container is screen width and height, or none at all.
Please note that I have tried all HitTestBehavior types.
Here is the code whilst it's not detecting anything onTap:
class CanvasObject {
Color color = Colors.green[800];
double strokeWidth = 10.0;
PaintingStyle style = PaintingStyle.stroke;
}
class CanvasNodeObject extends CanvasObject {
double x;
double y;
double radius = 20;
PaintingStyle style = PaintingStyle.fill;
CanvasNodeObject({#required this.x, #required this.y});
}
class CanvasNodePainter extends CustomPainter {
CanvasNodeObject node;
CanvasNodePainter({this.node});
#override
void paint(Canvas canvas, Size size) {
Path path;
Paint nodePaint = Paint()
..color = node.color
..style = node.style;
path = Path();
path.moveTo(0, 0);
path.addOval(
Rect.fromCircle(center: Offset(0, 0), radius: node.radius));
canvas.drawPath(path, nodePaint);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
class TestLineDrawing extends StatefulWidget {
CanvasNodeObject startNode;
CanvasNodeObject endNode;
CanvasLineObject line;
TestLineDrawing({List<double> x, List<double> y})
: assert(x.length == 2),
assert(y.length == 2) {
startNode = CanvasNodeObject(x: x[0], y: y[0]);
endNode = CanvasNodeObject(x: x[1], y: y[1]);
line = CanvasLineObject(x: x, y: y);
}
#override
_TestLineDrawingState createState() => _TestLineDrawingState();
}
class _TestLineDrawingState extends State<TestLineDrawing> {
List<Widget> line() {
return [
Positioned(
top: widget.endNode.x,
left: widget.endNode.y,
child:
GestureDetector(
behavior: HitTestBehavior.opaque,
child:
Container(
height: widget.endNode.radius,
width: widget.endNode.radius,
child: CustomPaint(painter: CanvasNodePainter(node: widget.endNode))),
onTap: () {
setState(() {
Random random = Random();
widget.endNode.color = Color.fromARGB(255, random.nextInt(255), random.nextInt(255), random.nextInt(255));
debugPrint("EndNodeOnPress " + widget.endNode.color.toString());
});
},
)),
];
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: line(),
));
}
On screen it is:
I resolved this issue by doing the following:
Adding all paths to a top level path called path using Path::addPath
Overriding hitTest and using Path::contains
#override
bool hitTest(Offset position) {
bool hit = path.contains(position);
return hit;
}
I have implemented signature_pad in my flutter project and it works fine.
Unfortunately when I place it inside SingleChildScrollView, the signature was not drawn. It scrolled instead of signed.
It seems like is the GestureDetector but I have no idea how to fix it.
Can someone give me some clue on this?
Thanks.
Signature Class need to be modified to respond to VerticalDrag , I renamed it to Signature1
now signature area pad should not scroll , you can check the complete code below as it behaves. you will find out that Signature area is no more scrolling with the SingleChildScrollView.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'dart:async';
import 'dart:ui' as ui;
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
var color = Colors.black;
var strokeWidth = 3.0;
final _sign = GlobalKey<Signature1State>();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body:
SingleChildScrollView(
child: Column(
children: <Widget>[
_showCategory(),
SizedBox(height: 15),
_showCategory(),
SizedBox(height: 15),
_showCategory(),
SizedBox(height: 15),
_showCategory(),
SizedBox(height: 15),
_showCategory(),
_showCategory(),
SizedBox(height: 15),
_showCategory(),
SizedBox(height: 15),
_showCategory(),
SizedBox(height: 15),
_showCategory(),
SizedBox(height: 15),
_showCategory(),
_showSignaturePad()
],
),
)
,
);
}
Widget _showCategory() {
return TextField(
onTap: () {
FocusScope.of(context).requestFocus(FocusNode());
},
style: TextStyle(fontSize: 12.0, height: 1.0),
decoration: InputDecoration(hintText: "TextView"));
}
Widget _showSignaturePad() {
return Container(
width: double.infinity,
height: 200,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
height: 200,
//color: Colors.red,
child: Signature1(
color: color,
key: _sign,
strokeWidth: strokeWidth,
),
),
),
color: Colors.grey.shade300,
);
}
}
class Signature1 extends StatefulWidget {
final Color color;
final double strokeWidth;
final CustomPainter backgroundPainter;
final Function onSign;
Signature1({
this.color = Colors.black,
this.strokeWidth = 5.0,
this.backgroundPainter,
this.onSign,
Key key,
}) : super(key: key);
Signature1State createState() => Signature1State();
static Signature1State of(BuildContext context) {
return context.findAncestorStateOfType<Signature1State>();
}
}
class _SignaturePainter extends CustomPainter {
Size _lastSize;
final double strokeWidth;
final List<Offset> points;
final Color strokeColor;
Paint _linePaint;
_SignaturePainter({#required this.points, #required this.strokeColor, #required this.strokeWidth}) {
_linePaint = Paint()
..color = strokeColor
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round;
}
#override
void paint(Canvas canvas, Size size) {
_lastSize = size;
for (int i = 0; i < points.length - 1; i++) {
if (points[i] != null && points[i + 1] != null) canvas.drawLine(points[i], points[i + 1], _linePaint);
}
}
#override
bool shouldRepaint(_SignaturePainter other) => other.points != points;
}
class Signature1State extends State<Signature1> {
List<Offset> _points = <Offset>[];
_SignaturePainter _painter;
Size _lastSize;
Signature1State();
void _onDragStart(DragStartDetails details){
RenderBox referenceBox = context.findRenderObject();
Offset localPostion = referenceBox.globalToLocal(details.globalPosition);
setState(() {
_points = List.from(_points)
..add(localPostion)
..add(localPostion);
});
}
void _onDragUpdate (DragUpdateDetails details) {
RenderBox referenceBox = context.findRenderObject();
Offset localPosition = referenceBox.globalToLocal(details.globalPosition);
setState(() {
_points = List.from(_points)..add(localPosition);
if (widget.onSign != null) {
widget.onSign();
}
});
}
void _onDragEnd (DragEndDetails details) => _points.add(null);
#override
Widget build(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) => afterFirstLayout(context));
_painter = _SignaturePainter(points: _points, strokeColor: widget.color, strokeWidth: widget.strokeWidth);
return ClipRect(
child: CustomPaint(
painter: widget.backgroundPainter,
foregroundPainter: _painter,
child: GestureDetector(
onVerticalDragStart: _onDragStart,
onVerticalDragUpdate: _onDragUpdate,
onVerticalDragEnd: _onDragEnd,
onPanStart: _onDragStart,
onPanUpdate: _onDragUpdate,
onPanEnd: _onDragEnd
),
),
);
}
Future<ui.Image> getData() {
var recorder = ui.PictureRecorder();
var origin = Offset(0.0, 0.0);
var paintBounds = Rect.fromPoints(_lastSize.topLeft(origin), _lastSize.bottomRight(origin));
var canvas = Canvas(recorder, paintBounds);
if(widget.backgroundPainter != null) {
widget.backgroundPainter.paint(canvas, _lastSize);
}
_painter.paint(canvas, _lastSize);
var picture = recorder.endRecording();
return picture.toImage(_lastSize.width.round(), _lastSize.height.round());
}
void clear() {
setState(() {
_points = [];
});
}
bool get hasPoints => _points.length > 0;
List<Offset> get points => _points;
afterFirstLayout(BuildContext context) {
_lastSize = context.size;
}
}
you need to create a CustomGestureDetector.
Check this updated version of Signature that I just changed to you:
import 'dart:async';
import 'dart:ui' as ui;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
class Signature extends StatefulWidget {
final Color color;
final double strokeWidth;
final CustomPainter backgroundPainter;
final Function onSign;
Signature({
this.color = Colors.black,
this.strokeWidth = 5.0,
this.backgroundPainter,
this.onSign,
Key key,
}) : super(key: key);
SignatureState createState() => SignatureState();
static SignatureState of(BuildContext context) {
return context.findAncestorStateOfType<SignatureState>();
}
}
class CustomPanGestureRecognizer extends OneSequenceGestureRecognizer {
final Function onPanStart;
final Function onPanUpdate;
final Function onPanEnd;
CustomPanGestureRecognizer({#required this.onPanStart, #required this.onPanUpdate, #required this.onPanEnd});
#override
void addPointer(PointerEvent event) {
onPanStart(event.position);
startTrackingPointer(event.pointer);
resolve(GestureDisposition.accepted);
}
#override
void handleEvent(PointerEvent event) {
if (event is PointerMoveEvent) {
onPanUpdate(event.position);
}
if (event is PointerUpEvent) {
onPanEnd(event.position);
stopTrackingPointer(event.pointer);
}
}
#override
String get debugDescription => 'customPan';
#override
void didStopTrackingLastPointer(int pointer) {}
}
class _SignaturePainter extends CustomPainter {
Size _lastSize;
final double strokeWidth;
final List<Offset> points;
final Color strokeColor;
Paint _linePaint;
_SignaturePainter({#required this.points, #required this.strokeColor, #required this.strokeWidth}) {
_linePaint = Paint()
..color = strokeColor
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round;
}
#override
void paint(Canvas canvas, Size size) {
_lastSize = size;
for (int i = 0; i < points.length - 1; i++) {
if (points[i] != null && points[i + 1] != null) canvas.drawLine(points[i], points[i + 1], _linePaint);
}
}
#override
bool shouldRepaint(_SignaturePainter other) => other.points != points;
}
class SignatureState extends State<Signature> {
List<Offset> _points = <Offset>[];
_SignaturePainter _painter;
Size _lastSize;
SignatureState();
#override
Widget build(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) => afterFirstLayout(context));
_painter = _SignaturePainter(points: _points, strokeColor: widget.color, strokeWidth: widget.strokeWidth);
return ClipRect(
child: CustomPaint(
painter: widget.backgroundPainter,
foregroundPainter: _painter,
child: RawGestureDetector(
gestures: {
CustomPanGestureRecognizer: GestureRecognizerFactoryWithHandlers<CustomPanGestureRecognizer>(
() => CustomPanGestureRecognizer(
onPanStart: (position) {
RenderBox referenceBox = context.findRenderObject();
Offset localPostion = referenceBox.globalToLocal(position);
setState(() {
_points = List.from(_points)..add(localPostion)..add(localPostion);
});
return true;
},
onPanUpdate: (position) {
RenderBox referenceBox = context.findRenderObject();
Offset localPosition = referenceBox.globalToLocal(position);
setState(() {
_points = List.from(_points)..add(localPosition);
if (widget.onSign != null) {
widget.onSign();
}
});
},
onPanEnd: (position) {
_points.add(null);
},
),
(CustomPanGestureRecognizer instance) {},
),
},
),
),
);
}
Future<ui.Image> getData() {
var recorder = ui.PictureRecorder();
var origin = Offset(0.0, 0.0);
var paintBounds = Rect.fromPoints(_lastSize.topLeft(origin), _lastSize.bottomRight(origin));
var canvas = Canvas(recorder, paintBounds);
if (widget.backgroundPainter != null) {
widget.backgroundPainter.paint(canvas, _lastSize);
}
_painter.paint(canvas, _lastSize);
var picture = recorder.endRecording();
return picture.toImage(_lastSize.width.round(), _lastSize.height.round());
}
void clear() {
setState(() {
_points = [];
});
}
bool get hasPoints => _points.length > 0;
List<Offset> get points => _points;
afterFirstLayout(BuildContext context) {
_lastSize = context.size;
}
}
Special attention to CustomPanGestureRecognizer
You can read more in:
Gesture Disambiguation
This is happening because the gesture from SingleChildScrollView overrides your Signature widget’s gesture as the SingleChildScrollView is the parent. There are few ways to solve it as in the other responses in this thread. But the easiest one is using the existing package. You can simply use the below Syncfusion's Flutter SignaturePad widget which I am using now for my application. This widget will work on Android, iOS, and web platforms.
Package - https://pub.dev/packages/syncfusion_flutter_signaturepad
Features - https://www.syncfusion.com/flutter-widgets/flutter-signaturepad
Documentation - https://help.syncfusion.com/flutter/signaturepad/getting-started