Flutter : Draw over a photo and save the photo with my drawing - flutter

I am stuck to find a solution for drawing over a photo and then save this photo including the drawing on the phone.
I am able to draw over it, but how to save the canvas with the photo ?
I tried different solution as below, but in this example I am stuck to get an ui.Image from my photo for adding it to the canvas.
Any angel for help on this ?
class PhotoPainterW extends ConsumerStatefulWidget {
PhotoPainterW({Key? key, required this.photoPath});
final String photoPath;
#override
_PhotoPainterWState createState() => new _PhotoPainterWState();
}
class _PhotoPainterWState extends ConsumerState<PhotoPainterW> {
List<Offset?> _points = <Offset?>[];
void saveImage(String photoPath) async {
ui.Image? image = await getUiImage(photoPath);
PictureRecorder recorder = PictureRecorder();
Canvas canvas = Canvas(recorder);
canvas.drawImage(image, new Offset(50.0, 50.0), new Paint());
SignaturePainter painter = SignaturePainter(points: _points);
var size = context.size;
painter.paint(canvas, size!);
final picture = recorder.endRecording();
final img = await picture.toImage(500,200);
final pngBytes = await img.toByteData(format: ImageByteFormat.png);
if (pictureAppDirectory == null) await initPhotoDirectory(context);
String drawPath = '${pictureAppDirectory!.path}/Drawing';
await Directory(drawPath).create(recursive: true);
File('$drawPath/test.png').writeAsBytesSync(pngBytes!.buffer.asInt8List());
OpenFile.open('$drawPath/test.png');
}
Future<ui.Image> getUiImage(String imageAssetPath) async {
Uint8List bytes = File(imageAssetPath).readAsBytesSync();
final ByteData assetImageByteData = ByteData.view(bytes.buffer);
image.Image? baseSizeImage = image.decodeImage(assetImageByteData.buffer.asUint8List());
ui.Codec codec = await ui.instantiateImageCodec(baseSizeImage!.getBytes());
ui.FrameInfo frameInfo = await codec.getNextFrame();
return frameInfo.image;
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children:[
Image.file(File(widget.photoPath)),
Container(
child: new GestureDetector(
onPanUpdate: (DragUpdateDetails details) {
setState(() {
RenderBox? renderBox = context.findRenderObject() as RenderBox;
Offset _localPosition = renderBox.globalToLocal(details.globalPosition);
_points = new List.from(_points)..add(_localPosition);
});
},
onPanEnd: (DragEndDetails details) => _points.add(null),
child: new CustomPaint(
painter: new SignaturePainter(points: _points),
size: Size.infinite,
),
),
),
]),
),
);
}
}
class SignaturePainter extends CustomPainter {
SignaturePainter({required this.points});
List<Offset?>? points;
#override
void paint(Canvas canvas, Size size) {
Paint paint = new Paint()
..color = Colors.blue
..strokeCap = StrokeCap.round
..strokeWidth = 10.0;
for (int i = 0; i < points!.length - 1; i++) {
if (points![i] != null && points![i + 1] != null) {
canvas.drawLine(points![i]!, points![i + 1]!, paint);
}
}
}
#override
bool shouldRepaint(SignaturePainter oldDelegate) => oldDelegate.points != points;
}

One option you have is use screenshot, and take screenshot from Stack widget.like this:
First wrap your Stack widget with Screenshot:
Screenshot(
controller: screenshotController,
child: Stack(
children:[...],
),
),
when you need your image do this:
screenshotController.capture().then((Uint8List image) {
//your image
}).catchError((onError) {
print(onError);
});
Note that this way may affect you image quality.

Related

Flutter moving point over image

I want to create a moving point that moves across an image. The picture is drawn on canvas. The point should approach the specified x and y coordinates.Image and point are generated. The point should move to the new specified position every 500ms by a timer.But it only moves when I move the mouse over buttons on the app.
class five_og extends StatefulWidget {
#override
State createState() {
return _five_og();
}
}
class _five_og extends State<five_og> {
ui.Image? image;
Timer? timer;
void refreshImage() {
if (i >= xList.length - 1) {
i = 0;
}
ImagePainter(image!);
i++;
}
#override
void dispose() {
super.dispose();
timer!.cancel();
}
#override
void initState() {
super.initState();
loadImage('assets/images/Erdgeschoss.png');
timer = Timer.periodic(
Duration(milliseconds: 500),
(timer) {
refreshImage();
},
);
}
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;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Test'),
),
body: Center(
child: image == null
? CircularProgressIndicator()
: Container(
height: 600,
width: 600,
child: FittedBox(
child: SizedBox(
width: image!.width.toDouble(),
height: image!.width.toDouble(),
child: CustomPaint(
painter: ImagePainter(image!),
),
),
)
)
),
);
}
}
class ImagePainter extends CustomPainter {
final ui.Image image;
const ImagePainter(this.image);
#override
void paint(Canvas canvas, Size size) {
//final paint = Paint();
canvas.drawImage(image, Offset.zero, Paint());
final paint = Paint()
..color = Color.fromARGB(255, 15, 182, 253)
..style = PaintingStyle.fill;
canvas.drawCircle(Offset(xList[i], yList[i]), 13, paint);
}
#override
bool shouldRepaint(ImagePainter oldDelegate) {
return false;
}
}

find ontap position of an element inside a container in canvas flutter

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.

Is it possible to add a Draggable and DragTarget widget inside the Customized Containers in Flutter?

Here is the code of Customized Three Containers using ClipPath for obtaining alphabet "A". This Code can also draw inside these three containers. But the drawing can be done in any direction. I want to make this code into drawing from top to bottom inside first container ,when the first drawing done then the second container is activated, after drawing of second Container then the third one to be activated (Tracing Alphabet). I think there should be 3 pairs of Draggable and DragTarget. Drawing is done by using GestureDetector. Is there is any way draw line using Draggable widget, Please respond anyone konws the solution
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'dart:ui' as ui;
import 'package:flutter/services.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setEnabledSystemUIOverlays ([]);
SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeRight,DeviceOrientation.landscapeLeft]).then((_){
runApp(MyApp());
});
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
List<Offset> points1 = [];
List<Offset> points2 = [];
List<Offset> points3 = [];
#override
Widget build(BuildContext context) {
final double width = MediaQuery.of(context).size.width;
final double height = MediaQuery.of(context).size.height;
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: (){
points1.clear();
points2.clear();
points3.clear();
},
),
backgroundColor: Colors.teal,
body: SafeArea(
child:Center(
child: Stack(
children: [
Positioned(
left: MediaQuery.of(context).size.width*0.381,
top:MediaQuery.of(context).size.height*0.560,
child: ClipPath(
clipper: CPath3(),
child: Container(
height: height*0.07,
width: width*0.18,
child: GestureDetector(
onPanStart: (details){
this.setState(() {
points3.add(details.localPosition);
});
},
onPanUpdate: (details){
this.setState(() {
points3.add(details.localPosition);
});
},
onPanEnd: (details){
this.setState(() {
points3.add(null);
});
},
child: CustomPaint(
painter: MyPainter3(points3: points3),
),
),
),
),
),//THIRD PATH
Positioned(
left: MediaQuery.of(context).size.width*0.283,
// bottom: 0,
top: MediaQuery.of(context).size.height*0.194,
//right: 0,
child: ClipPath(
clipper: CPath1(),
child: Container(
height: height*0.60,
width: width*0.20,
child: GestureDetector(
onPanStart: (details){
this.setState(() {
points1.add(details.localPosition);
});
},
onPanUpdate: (details){
this.setState(() {
points1.add(details.localPosition);
});
},
onPanEnd: (details){
this.setState(() {
points1.add(null);
});
},
child: CustomPaint(
painter: MyPainter1(points1: points1),
),
),
),
),
),//FIRST PATH
Positioned(
left: MediaQuery.of(context).size.width*0.467,
// bottom: 0,
top: MediaQuery.of(context).size.height*0.194,
//right: 0,
child: ClipPath(
clipper: CPath2(),
child: Container(
height: height*0.60,
width: width*0.20,
child: GestureDetector(
onPanStart: (details){
this.setState(() {
points2.add(details.localPosition);
});
},
onPanUpdate: (details){
this.setState(() {
points2.add(details.localPosition);
});
},
onPanEnd: (details){
this.setState(() {
points2.add(null);
});
},
child: CustomPaint(
painter: MyPainter2(points2: points2),
),
),
),
),
),//SECOND PATH
],
),
),
),
);
}
}
class CPath1 extends CustomClipper<Path> {
#override
Path getClip(Size size) {
var path_0 = Path();
path_0.moveTo(0,size.height);
path_0.lineTo(size.width*0.2983628,size.height);
path_0.lineTo(size.width,0);
path_0.lineTo(size.width*0.7591676,0);
path_0.lineTo(0,size.height);
path_0.close();
return path_0;
}
#override
bool shouldReclip(covariant CustomClipper<Path> oldClipper) {
// TODO: implement shouldReclip
return true;
}
}
class MyPainter1 extends CustomPainter {
List<Offset> points1;
MyPainter1({this.points1});
#override
void paint(Canvas canvas, Size size) {
Paint background = Paint()..color = Colors.white;
Rect rect = Rect.fromLTWH(0, 0, size.width, size.height);
canvas.drawRect(rect, background);
Paint paint = Paint();
paint.color = Colors.deepPurpleAccent;
paint.strokeWidth = 70;
paint.isAntiAlias = true;
paint.strokeCap = StrokeCap.round;
for(int x=0;x<points1.length-1;x++)
{
if(points1[x] != null && points1[x+1] != null)
{
canvas.drawLine(points1[x], points1[x+1], paint);
}
else if(points1[x] != null && points1[x+1] == null)
{
canvas.drawPoints(ui.PointMode.points,[points1[x]], paint);
}
}
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
// TODO: implement shouldRepaint
return true;
}
}
class CPath2 extends CustomClipper<Path> {
#override
Path getClip(Size size) {
var path_0 = Path();
path_0.moveTo(size.width*0.6738603,size.height);
path_0.lineTo(size.width,size.height);
path_0.lineTo(size.width*0.1934179,0);
path_0.lineTo(size.width*0.08001079,0);
path_0.lineTo(0,size.height*0.1078064);
path_0.lineTo(size.width*0.6738603,size.height);
path_0.close();
return path_0;
}
#override
bool shouldReclip(covariant CustomClipper<Path> oldClipper) {
// TODO: implement shouldReclip
return true;
}
}
class MyPainter2 extends CustomPainter {
List<Offset> points2;
MyPainter2({this.points2});
#override
void paint(Canvas canvas, Size size) {
Paint background = Paint()..color = Colors.white;
Rect rect = Rect.fromLTWH(0, 0, size.width, size.height);
canvas.drawRect(rect, background);
Paint paint = Paint();
paint.color = Colors.deepPurpleAccent;
paint.strokeWidth = 70;
paint.isAntiAlias = true;
paint.strokeCap = StrokeCap.round;
for(int x=0;x<points2.length-1;x++)
{
if(points2[x] != null && points2[x+1] != null)
{
canvas.drawLine(points2[x], points2[x+1], paint);
}
else if(points2[x] != null && points2[x+1] == null)
{
canvas.drawPoints(ui.PointMode.points,[points2[x]], paint);
}
}
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
// TODO: implement shouldRepaint
return true;
}
}
class CPath3 extends CustomClipper<Path> {
#override
Path getClip(Size size) {
var path_0 = Path();
path_0.moveTo(size.width*0.9108546,0);
path_0.lineTo(size.width*0.08746101,0);
path_0.lineTo(0,size.height);
path_0.lineTo(size.width,size.height*0.9771505);
path_0.lineTo(size.width*0.9108546,0);
path_0.close();
return path_0;
}
#override
bool shouldReclip(covariant CustomClipper<Path> oldClipper) {
// TODO: implement shouldReclip
return true;
}
}
class MyPainter3 extends CustomPainter {
List<Offset> points3;
MyPainter3({this.points3});
#override
void paint(Canvas canvas, Size size) {
Paint background = Paint()..color = Colors.white;
Rect rect = Rect.fromLTWH(0, 0, size.width, size.height);
canvas.drawRect(rect, background);
Paint paint = Paint();
paint.color = Colors.deepPurpleAccent;
paint.strokeWidth = 70;
paint.isAntiAlias = true;
paint.strokeCap = StrokeCap.round;
for(int x=0;x<points3.length-1;x++)
{
if(points3[x] != null && points3[x+1] != null)
{
canvas.drawLine(points3[x], points3[x+1], paint);
}
else if(points3[x] != null && points3[x+1] == null)
{
canvas.drawPoints(ui.PointMode.points,[points3[x]], paint);
}
}
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
// TODO: implement shouldRepaint
return true;
}
}

Masking two images in Flutter using a Custom Painter

Hello can anyone tell me why the background to this masking attempt is black. This must be close but I just can't kill the background. I've seen others reference that saveLayer(rect, paint) is the key here as that shoves the whole canvas rect in to the masking operation. This question (no masking operation) and this one (no actual answer) are similar but were no use to me.
main.dart
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
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> {
ui.Image mask;
ui.Image image;
#override
void initState() {
super.initState();
load('images/squircle.png').then((i) {
setState(() {
mask = i;
});
});
load('images/noodlejpg.jpg').then((i) {
setState(() {
image = i;
});
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(backgroundColor: Colors.blue, title: Text('I am a title')),
body: SafeArea(
child: SizedBox(
width: 200.0,
height: 200.0,
child: CustomPaint(painter: OverlayPainter(mask, image)),
),
),
);
}
Future<ui.Image> load(String asset) async {
ByteData data = await rootBundle.load(asset);
ui.Codec codec = await ui.instantiateImageCodec(data.buffer.asUint8List());
ui.FrameInfo fi = await codec.getNextFrame();
return fi.image;
}
}
class OverlayPainter extends CustomPainter {
ui.Image mask;
ui.Image image;
OverlayPainter(this.mask, this.image);
#override
void paint(Canvas canvas, Size size) {
var paint = new Paint();
paint.isAntiAlias = true;
if (image != null) {
var rect = Rect.fromLTRB(0, 0, 200, 200);
Size outputSize = rect.size;
Size inputSize = Size(image.width.toDouble(), image.height.toDouble());
final FittedSizes fittedSizes =
applyBoxFit(BoxFit.cover, inputSize, outputSize);
final Size sourceSize = fittedSizes.source;
canvas.save();
final Rect sourceRect = Alignment.center.inscribe(
sourceSize,
Offset.zero & inputSize,
);
canvas.drawImageRect(image, sourceRect, rect, paint);
canvas.restore();
}
if (mask != null) {
var rect = Rect.fromLTRB(0, 0, 200, 200);
Size outputSize = rect.size;
Size inputSize = Size(mask.width.toDouble(), mask.height.toDouble());
final FittedSizes fittedSizes =
applyBoxFit(BoxFit.cover, inputSize, outputSize);
final Size sourceSize = fittedSizes.source;
canvas.saveLayer(rect, Paint()..blendMode = BlendMode.dstIn);
final Rect sourceRect = Alignment.center.inscribe(
sourceSize,
Offset.zero & inputSize,
);
canvas.drawImageRect(mask, sourceRect, rect, paint);
canvas.restore();
}
}
#override
bool shouldRepaint(OverlayPainter oldDelegate) {
return mask != oldDelegate.mask || image != oldDelegate.image;
}
}
noodlejpg.jpg
squircle.jpg
result
The key is in when to call saveLayer and when to call restore.
From here:
When using Canvas.saveLayer and Canvas.restore, the blend mode of the Paint given to the Canvas.saveLayer will be applied when Canvas.restore is called. Each call to Canvas.saveLayer introduces a new layer onto which shapes and images are painted; when Canvas.restore is called, that layer is then composited onto the parent layer, with the source being the most-recently-drawn shapes and images, and the destination being the parent layer. (For the first Canvas.saveLayer call, the parent layer is the canvas itself.)
Working code
#override
void paint(Canvas canvas, Size size) {
if (image != null && mask != null) {
var rect = Rect.fromLTRB(0, 0, 200, 200);
Size outputSize = rect.size;
Paint paint = new Paint();
//Mask
Size maskInputSize = Size(mask.width.toDouble(), mask.height.toDouble());
final FittedSizes maskFittedSizes =
applyBoxFit(BoxFit.cover, maskInputSize, outputSize);
final Size maskSourceSize = maskFittedSizes.source;
final Rect maskSourceRect = Alignment.center
.inscribe(maskSourceSize, Offset.zero & maskInputSize);
canvas.saveLayer(rect, paint);
canvas.drawImageRect(mask, maskSourceRect, rect, paint);
//Image
Size inputSize = Size(image.width.toDouble(), image.height.toDouble());
final FittedSizes fittedSizes =
applyBoxFit(BoxFit.cover, inputSize, outputSize);
final Size sourceSize = fittedSizes.source;
final Rect sourceRect =
Alignment.center.inscribe(sourceSize, Offset.zero & inputSize);
canvas.drawImageRect(
image, sourceRect, rect, paint..blendMode = BlendMode.srcIn);
canvas.restore();
}
}
Result:
as an option without any CustomPainters
import 'dart:typed_data';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(),
body: SafeArea(
child: Center(
child: SizedBox(
width: 300,
height: 300,
child: MaskedImage(asset: 'images/noodlejpg.jpeg', mask: 'images/circle.png'),
),
),
),
),
);
}
}
class MaskedImage extends StatelessWidget {
final String asset;
final String mask;
MaskedImage({#required this.asset, #required this.mask});
#override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
return FutureBuilder<List>(
future: _createShaderAndImage(asset, mask, constraints.maxWidth, constraints.maxHeight),
builder: (context, snapshot) {
if (!snapshot.hasData) return const SizedBox.shrink();
return ShaderMask(
blendMode: BlendMode.dstATop,
shaderCallback: (rect) => snapshot.data[0],
child: snapshot.data[1],
);
},
);
});
}
Future<List> _createShaderAndImage(String asset, String mask, double w, double h) async {
ByteData data = await rootBundle.load(asset);
ByteData maskData = await rootBundle.load(mask);
Codec codec = await instantiateImageCodec(maskData.buffer.asUint8List(), targetWidth: w.toInt(), targetHeight: h.toInt());
FrameInfo fi = await codec.getNextFrame();
ImageShader shader = ImageShader(fi.image, TileMode.clamp, TileMode.clamp, Matrix4.identity().storage);
Image image = Image.memory(data.buffer.asUint8List(), fit: BoxFit.cover, width: w, height: h);
return [shader, image];
}
}

How to add Signature in flutter?

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