How to add an eraser in custom paint? - flutter

I'm currently building a painting app with custom paint and every works great, I have a button to clear everything has been painted on the screen, but I'm looking for a normal eraser that can clear any specific line have been drawn not all of them at once how can this be done with custom paint, here is an example code of a painting app near to what I'm working on
import 'dart:io';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
class Draw extends StatefulWidget {
#override
_DrawState createState() => _DrawState();
}
class _DrawState extends State<Draw> {
Color selectedColor = Colors.black;
Color pickerColor = Colors.black;
double strokeWidth = 3.0;
List<DrawingPoints> points = List();
bool showBottomList = false;
double opacity = 1.0;
StrokeCap strokeCap = (Platform.isAndroid) ? StrokeCap.butt : StrokeCap.round;
SelectedMode selectedMode = SelectedMode.StrokeWidth;
List<Color> colors = [
Colors.red,
Colors.green,
Colors.blue,
Colors.amber,
Colors.black
];
#override
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
padding: const EdgeInsets.only(left: 8.0, right: 8.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(50.0),
color: Colors.greenAccent),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
IconButton(
icon: Icon(Icons.album),
onPressed: () {
setState(() {
if (selectedMode == SelectedMode.StrokeWidth)
showBottomList = !showBottomList;
selectedMode = SelectedMode.StrokeWidth;
});
}),
IconButton(
icon: Icon(Icons.opacity),
onPressed: () {
setState(() {
if (selectedMode == SelectedMode.Opacity)
showBottomList = !showBottomList;
selectedMode = SelectedMode.Opacity;
});
}),
IconButton(
icon: Icon(Icons.color_lens),
onPressed: () {
setState(() {
if (selectedMode == SelectedMode.Color)
showBottomList = !showBottomList;
selectedMode = SelectedMode.Color;
});
}),
IconButton(
icon: Icon(Icons.clear),
onPressed: () {
setState(() {
showBottomList = false;
points.clear();
});
}),
],
),
Visibility(
child: (selectedMode == SelectedMode.Color)
? Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: getColorList(),
)
: Slider(
value: (selectedMode == SelectedMode.StrokeWidth)
? strokeWidth
: opacity,
max: (selectedMode == SelectedMode.StrokeWidth)
? 50.0
: 1.0,
min: 0.0,
onChanged: (val) {
setState(() {
if (selectedMode == SelectedMode.StrokeWidth)
strokeWidth = val;
else
opacity = val;
});
}),
visible: showBottomList,
),
],
),
)),
),
body: GestureDetector(
onPanUpdate: (details) {
setState(() {
RenderBox renderBox = context.findRenderObject();
points.add(DrawingPoints(
points: renderBox.globalToLocal(details.globalPosition),
paint: Paint()
..strokeCap = strokeCap
..isAntiAlias = true
..color = selectedColor.withOpacity(opacity)
..strokeWidth = strokeWidth));
});
},
onPanStart: (details) {
setState(() {
RenderBox renderBox = context.findRenderObject();
points.add(DrawingPoints(
points: renderBox.globalToLocal(details.globalPosition),
paint: Paint()
..strokeCap = strokeCap
..isAntiAlias = true
..color = selectedColor.withOpacity(opacity)
..strokeWidth = strokeWidth));
});
},
onPanEnd: (details) {
setState(() {
points.add(null);
});
},
child: CustomPaint(
size: Size.infinite,
painter: DrawingPainter(
pointsList: points,
),
),
),
);
}
getColorList() {
List<Widget> listWidget = List();
for (Color color in colors) {
listWidget.add(colorCircle(color));
}
Widget colorPicker = GestureDetector(
onTap: () {
showDialog(
context: context,
child: AlertDialog(
title: const Text('Pick a color!'),
content: SingleChildScrollView(
child: ColorPicker(
pickerColor: pickerColor,
onColorChanged: (color) {
pickerColor = color;
},
enableLabel: true,
pickerAreaHeightPercent: 0.8,
),
),
actions: <Widget>[
FlatButton(
child: const Text('Save'),
onPressed: () {
setState(() => selectedColor = pickerColor);
Navigator.of(context).pop();
},
),
],
),
);
},
child: ClipOval(
child: Container(
padding: const EdgeInsets.only(bottom: 16.0),
height: 36,
width: 36,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.red, Colors.green, Colors.blue],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
)),
),
),
);
listWidget.add(colorPicker);
return listWidget;
}
Widget colorCircle(Color color) {
return GestureDetector(
onTap: () {
setState(() {
selectedColor = color;
});
},
child: ClipOval(
child: Container(
padding: const EdgeInsets.only(bottom: 16.0),
height: 36,
width: 36,
color: color,
),
),
);
}
}
class DrawingPainter extends CustomPainter {
DrawingPainter({this.pointsList});
List<DrawingPoints> pointsList;
List<Offset> offsetPoints = List();
#override
void paint(Canvas canvas, Size size) {
for (int i = 0; i < pointsList.length - 1; i++) {
if (pointsList[i] != null && pointsList[i + 1] != null) {
canvas.drawLine(pointsList[i].points, pointsList[i + 1].points,
pointsList[i].paint);
} else if (pointsList[i] != null && pointsList[i + 1] == null) {
offsetPoints.clear();
offsetPoints.add(pointsList[i].points);
offsetPoints.add(Offset(
pointsList[i].points.dx + 0.1, pointsList[i].points.dy + 0.1));
canvas.drawPoints(PointMode.points, offsetPoints, pointsList[i].paint);
}
}
}
#override
bool shouldRepaint(DrawingPainter oldDelegate) => true;
}
class DrawingPoints {
Paint paint;
Offset points;
DrawingPoints({this.points, this.paint});
}
enum SelectedMode { StrokeWidth, Opacity, Color }

I faced the same issue, what I did was changing the brush color to the background color, you can change the strokeWidth too, you will draw more strokes but it will work as intended.

Remove this line from your code:
showBottomList = false;

Related

Cannot perform drawing after canvas is zoom-in, zoom-out and translated in flutter with a an image on it

Aim : I want to make an image marking functionality i.e drawing on an image along with zoom-in and zoom-out functions. For that I have used a canvas with an image on it and flutter Transform widget to achieve zoom and translation. I have succeeded in making the image zoom and translate. Also I can perform drawing on that image.
Problem : The problem is I can perform drawing only on the area which was set when Image was in original size, i.e when Image expands then I can draw on only that area(original image area) which comes under the new expanded(zoomed) image, same problem occurs when I move the image.
NOTE - I have set the size of canvas to the size of my image.
On the below screen-shots 1.before_zoom displays my image when I load it from assets. On that I had drawn a rectangle which shows drawing can be done on any part of image. In after_zoom image I had again drawn a rectangle which show the available screen for drawing. If I try to perform drawing outside that rectangle, it doesn't works same problem occurs when I move the image.
The problem I think is that canvas drawing space gets fixed when image first loads and when the image expands or moves it just lets drawing on the area which was first set on time of image loading.
So anyone knows how to fix these problem or any other method to achieve these functionality.
Code :
/// DrawingScreen :
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'dart:ui' as ui;
import 'package:flutter/services.dart';
class _DrawingScreenState extends State<DrawingScreen> {
ui.Image _image;
Future _future;
Color _selectedColor;
double _strokeWidth;
List<TouchPoints> points = [];
#override
void initState() {
super.initState();
_selectedColor = Colors.yellow;
_strokeWidth = 14.0;
_future = load();
}
String imageLocation = 'images/2.jpg';
/// FUNCTION TO LOAD THE IMAGE FROM THE ASSETS FOLDER
Future<void> load() async {
ByteData data = await rootBundle.load(imageLocation);
ui.Codec codec1 = await ui.instantiateImageCodec(data.buffer.asUint8List());
ui.FrameInfo fi = await codec1.getNextFrame();
_image = fi.image;
tempIW = _image.width.toDouble();
tempIH = _image.height.toDouble();
}
double translateX = 0;
double translateY = 0;
double scaleFactor = 1;
double tempIW;
double tempIH;
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: _future,
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return Scaffold(
bottomNavigationBar: Container(
color: Color(0xffF8F8FF),
child: Row(children: [
Flexible(
child: IconButton(
padding: EdgeInsets.zero,
iconSize: 20,
onPressed: () {
setState(() {
if (scaleFactor > 0.11)
setState(() {
scaleFactor = scaleFactor - 0.1;
});
});
},
icon: Icon(Icons.zoom_in),
),
),
Flexible(
child: IconButton(
iconSize: 20,
padding: EdgeInsets.zero,
onPressed: () {
setState(() {
if (scaleFactor < 0.91) {
scaleFactor = scaleFactor + 0.1;
}
});
},
icon: Icon(Icons.zoom_out),
),
),
Flexible(
child: IconButton(
iconSize: 20,
padding: EdgeInsets.zero,
onPressed: () {
setState(() {
translateX = translateX - 100;
});
},
icon: Icon(Icons.arrow_back),
),
),
Flexible(
child: IconButton(
iconSize: 20,
padding: EdgeInsets.zero,
onPressed: () {
setState(() {
translateX = translateX + 100;
});
},
icon: Icon(Icons.arrow_forward),
),
),
Flexible(
child: IconButton(
iconSize: 20,
padding: EdgeInsets.zero,
onPressed: () {
setState(() {
translateY = translateY - 100;
});
},
icon: Icon(Icons.arrow_upward),
),
),
Flexible(
child: IconButton(
iconSize: 20,
padding: EdgeInsets.zero,
onPressed: () {
setState(() {
translateY = translateY + 100;
});
},
icon: Icon(Icons.arrow_downward),
),
),
Flexible(
child: IconButton(
iconSize: 20,
padding: EdgeInsets.zero,
onPressed: () {
this.setState(() {
points.clear();
});
},
icon: Icon(Icons.layers_clear),
),
),
Flexible(
child: IconButton(
iconSize: 20,
padding: EdgeInsets.zero,
onPressed: () {
undo();
setState(() {
points;
});
},
icon: Icon(Icons.wifi_protected_setup),
),
),
]),
),
body: SafeArea(
child: Center(
child: FittedBox(
fit: BoxFit.fill,
child: Transform(
transform: Matrix4(
1, 0, 0, 0, //
0, 1, 0, 0, //
0, 0, 1, 0, //
translateX, translateY, 0, scaleFactor,
),
child: GestureDetector(
onPanStart: (details) {
setState(() {
if (details.localPosition.dx < _image.width.toDouble() && details.localPosition.dy < _image.height.toDouble())
points.add(
TouchPoints(
points: Offset(
details.localPosition.dx,
details.localPosition.dy,
),
paint: Paint()
..color = _selectedColor
..strokeWidth = _strokeWidth
..strokeCap = StrokeCap.round,
),
);
});
},
onPanUpdate: (details) {
setState(() {
if (details.localPosition.dx < _image.width.toDouble() && details.localPosition.dy < _image.height.toDouble())
points.add(
TouchPoints(
points: Offset(
details.localPosition.dx,
details.localPosition.dy,
),
paint: Paint()
..color = _selectedColor
..strokeWidth = _strokeWidth
..strokeCap = StrokeCap.round,
),
);
});
},
onPanEnd: (details) {
setState(() {
points.add(null);
});
print(details.velocity);
},
child: RepaintBoundary(
child: SizedBox(
width: _image.width.toDouble(),
height: _image.height.toDouble(),
child: ClipRect(
child: CustomPaint(
painter: FacePainter(
_image,
points,
),
),
),
),
),
),
),
),
),
),
);
} else {
return CircularProgressIndicator();
}
},
);
}
}
class FacePainter extends CustomPainter {
final ui.Image _image;
List<TouchPoints> pointsList = [];
FacePainter(
this._image,
this.pointsList,
) : super();
#override
void paint(Canvas canvas, Size size) async {
canvas.drawImage(_image, Offset(0.0, 0.0), Paint());
for (int i = 0; i < pointsList.length - 1; i++) {
if (pointsList[i] != null && pointsList[i + 1] != null) {
if (pointsList[i].points.dx < size.width && pointsList[i].points.dy < size.height)
canvas.drawLine(pointsList[i].points, pointsList[i + 1].points, pointsList[i].paint);
}
}
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
class TouchPoints {
Paint paint;
Offset points;
TouchPoints({this.paint, this.points});
}
Images for reference.
Before Zoom
After Zoom

Flutter, slider_button

I am new to Flutter and I am trying to create a draggable button to be able to accept or deny requests, I have used the Dtaggable Widget but it is giving me many problems, the last one is to limit the Dragable(button) to the container and therefore I've seen Dragabble is not designed for this and I should have used GestureDetector instead, but when I tried, I can't position the button in the center and let me take the distances well, if someone could give me a hand I would appreciate it.
class SliderButton extends StatefulWidget {
final double containerWidth;
final double containerHeight;
final Color containerColor;
final double buttonSize;
final Color buttonColor;
final Color textColor;
final double textSize;
final String leftText;
final String rightText;
final String textResultDeny;
final String textResultAccept;
final IconData icon;
final Color iconColor;
final Function(BuildContext context)? sliderPrimaryFunction;
final Function(BuildContext context)? sliderSecondaryFunction;
SliderButton(
{required this.containerWidth,
required this.containerHeight,
this.containerColor = Colors.black,
this.buttonSize = 50,
this.buttonColor = Colors.white,
this.rightText = 'Aceptar',
this.textResultAccept = 'Aceptado',
this.leftText = 'Denegar',
this.textResultDeny = 'Denegado',
this.sliderPrimaryFunction,
this.sliderSecondaryFunction,
this.textColor = Colors.white,
this.textSize = 24,
this.icon = Icons.add_circle_outline,
this.iconColor = Colors.black});
#override
_SliderButtonState createState() => _SliderButtonState();
}
class _SliderButtonState extends State<SliderButton> {
Offset position = Offset(0, 0);
bool started = false;
int switchOptions = 0;
Color? _containerColor = Colors.black;
#override
Widget build(BuildContext context) {
return Center(
child: sliderContainer(),
);
}
sliderContainer() => Container(
width: this.widget.containerWidth,
height: this.widget.containerHeight,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(50.00),
color: _containerColor,
boxShadow: [
BoxShadow(
color: Colors.black,
offset: Offset(0.0, 1.0),
blurRadius: 6.0,
),
],
),
child: sliderContainerContent());
sliderContainerContent() {
if (switchOptions == 0) return Center(child: containerContent());
if (switchOptions == 1)
return textResult(this.widget.textResultAccept);
else if (switchOptions == 2) return textResult(this.widget.textResultDeny);
}
containerContent() => Container(
width: this.widget.containerWidth,
height: this.widget.containerHeight,
child: Row(
children: [
started == false
? primaryText(this.widget.leftText, Alignment.centerLeft)
: Container(),
Center(
child: Container(
child: Draggable(
axis: Axis.horizontal,
feedback: roundedButton(),
child: started == false ? roundedButton() : Container(),
onDragStarted: () => setState(() {
started = true;
}),
onDragUpdate: (details) => _sequentialColor(details),
onDragEnd: (details) => _onSlideDragUpdate(details),
),
),
),
started == false
? primaryText(this.widget.rightText, Alignment.centerRight)
: Container(),
],
),
);
roundedButton() => Align(
child: Container(
width: this.widget.buttonSize,
height: this.widget.buttonSize,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: this.widget.buttonColor,
boxShadow: [
BoxShadow(
color: Colors.grey,
offset: Offset(0.0, 1.0),
blurRadius: 6.0,
),
],
),
child: Icon(this.widget.icon,
color: this.widget.iconColor, size: this.widget.buttonSize),
),
);
primaryText(String text, Alignment alignment) => Container(
alignment: alignment,
padding: EdgeInsets.all(14.0),
child: Text(text,
style: Theme.of(context)
.textTheme
.headline5
?.copyWith(color: Colors.white)),
);
textResult(String text) => Center(
child: Text(text,
style: Theme.of(context)
.textTheme
.headline3
?.copyWith(color: Colors.white)));
void _sequentialColor(DragUpdateDetails details) {
print(details.localPosition.dx);
var initColor = 200;
var algo = 240;
var algo2 = 200;
for (var i = details.localPosition.dx; i > algo; i++) {
setState(() {
_containerColor = Colors.green[initColor];
initColor += 100;
algo += 30;
});
}
for (var i = details.localPosition.dx; i < algo2; i--) {
setState(() {
_containerColor = Colors.red[initColor];
initColor += 100;
algo2 -= 30;
});
}
}
void _onSlideDragUpdate(DraggableDetails details) {
if (details.offset.distance > 470) {
setState(() {
switchOptions = 1;
_containerColor = Colors.lightGreen;
Future.delayed(const Duration(milliseconds: 500), () {
this.widget.sliderPrimaryFunction ?? Navigator.pop(context);
});
});
} else if (details.offset.distance < 400) {
setState(() {
switchOptions = 2;
_containerColor = Theme.of(context).errorColor;
Future.delayed(const Duration(milliseconds: 500), () {
this.widget.sliderPrimaryFunction ?? Navigator.pop(context);
});
});
} else
setState(() {
_containerColor = Theme.of(context).primaryColorDark;
started = false;
});
}
}
You can do this in this package.
https://pub.dev/packages/slider_button
Here is an example for you for the plugin. If you are having issue regarding the package. I will be happy to help you configure your own code
Center(child: SliderButton(
action: () {
///Do something here
Navigator.of(context).pop();
},
label: Text(
"Slide to cancel Event",
style: TextStyle(
color: Color(0xff4a4a4a), fontWeight: FontWeight.w500, fontSize: 17),
),
icon: Text(
"x",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w400,
fontSize: 44,
),
),
));
Thank you very much for the reply. I was trying to do it without the help of external packages, I ended up changing the Draggable for GestureDetector and it already works correctly.
Here I leave my solution in case it could be of help to someone.
import 'package:flutter/material.dart';
class SliderButton extends StatefulWidget {
final ValueChanged<double> valueChanged;
final double containerWidth;
final double containerHeight;
final Color containerColor;
final double buttonSize;
final Color buttonColor;
final Color textColor;
final double textSize;
final String leftText;
final String rightText;
final String textResultDeny;
final String textResultAccept;
final IconData icon;
final Color iconColor;
final Function(BuildContext context)? sliderPrimaryFunction;
final Function(BuildContext context)? sliderSecondaryFunction;
SliderButton({
required this.valueChanged,
required this.containerWidth,
required this.containerHeight,
this.containerColor = Colors.black,
this.buttonSize = 50,
this.buttonColor = Colors.white,
this.rightText = 'Aceptar',
this.textResultAccept = 'Aceptado',
this.leftText = 'Denegar',
this.textResultDeny = 'Denegado',
this.sliderPrimaryFunction,
this.sliderSecondaryFunction,
this.textColor = Colors.white,
this.textSize = 24,
this.icon = Icons.add_circle_outline,
this.iconColor = Colors.black,
});
#override
_SliderButtonState createState() => _SliderButtonState();
}
class _SliderButtonState extends State<SliderButton> {
ValueNotifier<double> valueListener = ValueNotifier(.0);
bool started = false;
int switchOptions = 0;
Color? _containerColor = Colors.black;
IconData _icon = Icons.lock;
#override
void initState() {
valueListener.addListener(notifyParent);
valueListener.value = 0.5;
super.initState();
}
void notifyParent() {
this.widget.valueChanged(valueListener.value);
}
#override
Widget build(BuildContext context) {
return Center(
child: sliderContainer(),
);
}
sliderContainer() => Stack(
children: [
Container(
width: this.widget.containerWidth,
height: this.widget.containerHeight,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(50.00),
color: _containerColor,
),
child: sliderContainerContent(),
),
Positioned(bottom: 2, child: slider())
],
);
sliderContainerContent() {
if (switchOptions == 0) return Center(child: containerContent());
if (switchOptions == 1)
return textResult(this.widget.textResultAccept);
else if (switchOptions == 2) return textResult(this.widget.textResultDeny);
}
containerContent() => Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
started == false ? primaryText(this.widget.leftText) : Container(),
started == false ? primaryText(this.widget.rightText) : Container(),
],
);
primaryText(String text) => Container(
padding: EdgeInsets.all(14.0),
child: Text(text,
style: Theme.of(context)
.textTheme
.headline5
?.copyWith(color: Colors.white)),
);
textResult(String text) => Center(
child: Text(text,
style: Theme.of(context)
.textTheme
.headline3
?.copyWith(color: Colors.white)));
slider() => Container(
width: this.widget.containerWidth,
height: this.widget.containerHeight,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(50.00),
color: Colors.transparent,
),
child: Builder(
builder: (context) {
final handle = gestureDetector();
return animatedBuilder(handle);
},
),
);
gestureDetector() => GestureDetector(
onHorizontalDragUpdate: _slideColor,
onHorizontalDragStart: (details) {
setState(() {
started = true;
});
},
onHorizontalDragEnd: _onSlideDragUpdate,
child: roundedButton(),
);
animatedBuilder(handle) => AnimatedBuilder(
animation: valueListener,
builder: (context, child) {
return AnimatedAlign(
duration:
Duration(milliseconds: valueListener.value == 0.5 ? 300 : 0),
alignment: Alignment(valueListener.value * 2 - 1, .5),
child: child,
);
},
child: handle,
);
roundedButton() => Container(
width: this.widget.buttonSize,
height: this.widget.buttonSize,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: this.widget.buttonColor,
boxShadow: [
BoxShadow(
color: Colors.grey,
offset: Offset(0.0, 1.0),
blurRadius: 6.0,
),
],
),
child: Icon(_icon,
color: this.widget.iconColor, size: this.widget.buttonSize - 5),
);
void _slideColor(DragUpdateDetails details) {
valueListener.value =
(valueListener.value + details.delta.dx / this.widget.containerWidth)
.clamp(.0, 1.0);
var sliderColor = 200;
var slideRight = 0.5;
var slideLeft = 0.5;
var i = valueListener.value;
for (; i > slideRight;) {
setState(() {
this._containerColor = Colors.green[sliderColor];
sliderColor += 100;
slideRight += 0.1;
_icon = Icons.lock_open_sharp;
});
}
for (; i < slideLeft;) {
setState(() {
this._containerColor = Colors.red[sliderColor];
sliderColor += 100;
slideLeft -= 0.1;
_icon = Icons.lock_outline_sharp;
});
}
}
void _onSlideDragUpdate(DragEndDetails details) {
if (valueListener.value >= 0.9) {
valueListener.value = 1;
setState(() {
switchOptions = 1;
_containerColor = Colors.lightGreen;
Future.delayed(const Duration(milliseconds: 500), () {
this.widget.sliderPrimaryFunction ?? Navigator.pop(context);
});
});
} else if (valueListener.value <= 0.1) {
valueListener.value = 0;
setState(() {
switchOptions = 2;
_containerColor = Theme.of(context).errorColor;
Future.delayed(const Duration(milliseconds: 500), () {
this.widget.sliderPrimaryFunction ?? Navigator.pop(context);
});
});
} else {
valueListener.value = 0.5;
setState(() {
_containerColor = Theme.of(context).primaryColorDark;
_icon = Icons.lock;
started = false;
});
}
}
}
'''
it still needs some improves but its working.

How to draw a single line in a certain area, and it continues from where it stopped

I just figured out how to draw a line with arrow buttons. However, it doesn't draw the line correctly. Therefore, I was wondering if someone can help me out with drawing just one line in a drawing area(Ideally in a scrollable box) and each time when releasing the button and tapping again, it should continue from where it stopped please?
This is what it looks like rn:
Drawing lines with arrow buttons
Here's part of my code:
Widget build(BuildContext context) {
return Transform.rotate(
angle: radians(0),
child: Stack(
alignment:
Alignment.lerp(Alignment.topCenter, Alignment.center, 0.50),
children: [
ImageInput(_selectImage),
_buildButton(0, color: Colors.red, icon: Icons.east),
_buildButton(45, color: Colors.green, icon: Icons.south_east),
_buildButton(90, color: Colors.orange, icon: Icons.south),
_buildButton(135, color: Colors.blue, icon: Icons.south_west),
_buildButton(180, color: Colors.black, icon: Icons.west),
_buildButton(225, color: Colors.indigo, icon: Icons.north_west),
_buildButton(270, color: Colors.pink, icon: Icons.north),
_buildButton(315, color: Colors.yellow, icon: Icons.north_east),
]));
}
_buildButton(double angle, {Color color, IconData icon}) {
final double rad = radians(angle);
return Transform(
transform: Matrix4.identity()
..translate(
100 * cos(rad),
100 * sin(rad),
),
child: Column(children: <Widget>[
SizedBox(
height: 150,
width: 150,
child: Container(),
),
CustomPaint(
foregroundPainter: LinePainter(_offset),
child: FloatingActionButton(
child: Icon(icon),
backgroundColor: color,
onPressed: () {
HoldDetector(
onHold: _incrementCounter(angle),
holdTimeout: Duration(milliseconds: 200),
enableHapticFeedback: true,
child: FloatingActionButton(
child: Icon(Icons.add),
onPressed: _incrementCounter(angle),
),
);
},
),
),
]),
);
}
_incrementCounter(double angle) {
_cnt += 10;
switch (angle.round()) {
case 0:
setState(() {
_offset = Offset(_cnt, 0);
});
break;
case 45:
setState(() {
_offset = Offset(_cnt, _cnt);
});
break;
case 90:
setState(() {
_offset = Offset(0, _cnt);
});
break;
case 135:
setState(() {
_offset = Offset(-_cnt, _cnt);
});
break;
case 180:
setState(() {
_offset = Offset(-_cnt, 0);
});
break;
case 225:
setState(() {
_offset = Offset(-_cnt, -_cnt);
});
break;
case 270:
setState(() {
_offset = Offset(0, -_cnt);
});
break;
case 315:
setState(() {
_offset = Offset(_cnt, -_cnt);
});
break;
}
}
}
class LinePainter extends CustomPainter {
Paint _paint;
Offset _offset;
LinePainter(Offset offset) {
_offset = offset;
_paint = Paint()
..color = Colors.black
..strokeWidth = 8.0;
}
#override
void paint(Canvas canvas, Size size) {
canvas.drawLine(Offset(0, 0), _offset, _paint);
}
#override
bool shouldRepaint(LinePainter oldDelegate) {
return true;
}
}

Flutter canvas : how to avoid drawing outside image area

import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
import 'dart:ui' as ui;
class EditImage extends StatefulWidget {
final String filePath;
EditImage({this.filePath});
#override
_EditImageState createState() => _EditImageState();
}
class _EditImageState extends State<EditImage> {
ui.Image decodedImage;
String newFilePath;
GlobalKey myCanvasKey = GlobalKey();
ImageEditor editor;
Color color = Colors.blue;
#override
void initState() {
loadImage(File(widget.filePath));
super.initState();
}
void loadImage(File image) async {
final data = await image.readAsBytes();
decodedImage = await decodeImageFromList(data);
editor = ImageEditor(image: decodedImage, strokeColor: color);
setState(() {});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: InkWell(
onTap: () {
Navigator.pop(context, newFilePath ?? widget.filePath);
},
child: Icon(Icons.close),
),
//centerTitle: true,
title: Text('Edit'),
actions: [
InkWell(
onTap: () {
editor.undo();
myCanvasKey.currentContext.findRenderObject().markNeedsPaint();
},
child: Icon(Icons.undo),
),
SizedBox(
width: 10.0,
),
InkWell(
onTap: () async {
Color pickedColor;
bool isSelected = false;
await showDialog(
context: context,
child: AlertDialog(
contentPadding: const EdgeInsets.all(8.0),
title: const Text('Stroke Color'),
content: SingleChildScrollView(
child: ColorPicker(
pickerColor: color,
onColorChanged: (color) {
pickedColor = color;
},
enableAlpha: false,
showLabel: false,
pickerAreaHeightPercent: 0.6,
),
),
actions: <Widget>[
FlatButton(
child: const Text('Cancel'),
onPressed: () => Navigator.pop(context),
),
FlatButton(
child: const Text('Select'),
onPressed: () {
isSelected = true;
Navigator.of(context).pop();
},
),
],
),
);
if (isSelected) {
editor.updateStrokeColor(pickedColor);
setState(() {
color = pickedColor;
});
}
},
child: Container(
decoration: BoxDecoration(
//borderRadius: BorderRadius.circular(15.0),
border: Border.all(color: Colors.grey),
shape: BoxShape.circle,
color: color,
),
child: Padding(
padding: const EdgeInsets.all(5.0),
child: Container(
decoration: BoxDecoration(
color: Colors.black26, shape: BoxShape.circle),
child: Padding(
padding: const EdgeInsets.all(3.0),
child: Icon(
Icons.edit_outlined,
color: Colors.white,
semanticLabel: 'Stroke',
),
),
),
),
),
),
SizedBox(
width: 10.0,
),
InkWell(
onTap: () {
Navigator.pop(context, newFilePath ?? widget.filePath);
},
child: Icon(Icons.done),
),
SizedBox(
width: 10.0,
),
],
),
body: decodedImage == null
? Center(child: CircularProgressIndicator())
: Center(
child: FittedBox(
child: SizedBox(
height: decodedImage.height.toDouble(),
width: decodedImage.width.toDouble(),
child: GestureDetector(
onPanDown: (detailData) {
editor.update(detailData.localPosition);
myCanvasKey.currentContext
.findRenderObject()
.markNeedsPaint();
},
onPanUpdate: (detailData) {
editor.update(detailData.localPosition);
myCanvasKey.currentContext
.findRenderObject()
.markNeedsPaint();
},
onPanEnd: (detailData) {
editor.addNewPointsList();
},
child: CustomPaint(
key: myCanvasKey,
painter: editor,
),
),
),
),
),
);
}
}
class ImageEditor extends CustomPainter {
ImageEditor({
this.image,
this.strokeColor = Colors.black,
}) {
_strokes.add(_StrokeData());
}
final ui.Image image;
Color strokeColor;
List<_StrokeData> _strokes = [];
void updateStrokeColor(Color color) {
strokeColor = color;
}
void update(Offset offset) {
if (_strokes.last.color == null) _strokes.last.color = strokeColor;
_strokes.last.points.add(offset);
}
void addNewPointsList() {
print('pan end');
_strokes.add(_StrokeData());
}
void undo() {
if (_strokes.length > 1) _strokes.removeAt(_strokes.length - 2);
}
#override
void paint(Canvas canvas, Size size) {
canvas.drawImage(image, Offset.zero, Paint());
for (int i = 0; i < _strokes.length; i++) {
Paint _painter = Paint();
_painter.color = _strokes[i].color ?? Colors.transparent;
_painter.style = PaintingStyle.stroke;
_painter.strokeWidth = 15;
_painter.isAntiAlias = true;
for (int j = 0; j < _strokes[i].points.length; j++) {
if (j > 0)
canvas.drawLine(
_strokes[i].points[j - 1], _strokes[i].points[j], _painter);
}
}
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
class _StrokeData {
Color color;
List<Offset> points;
_StrokeData() {
points = List<Offset>();
}
}
How to restrict drawing within the image area ?
Use ClipRRect() as parent of FittedBox. This solves the problem
ClipRRect(child: FittedBox(child: SizedBox(...Painter widget goes here...) ))

How to erase/clip from Canvas CustomPaint?

I have already tried to use Canvas.clipPath along with GestureDetector to be like eraser on the canvas where i use the CustomPaint inside a Container with imageDecoration set, so i thought maybe there is another workaround this by using Canvas.drawPath along setting
final Paint _eraserPaint = Paint()
..color = Colors.transparent
..blendMode = BlendMode.clear
..strokeWidth = 8
..style = PaintingStyle.stroke
..isAntiAlias = true;
but it draws black lines instead of erasing
any idea how to get around this?
thanks
The key is to call saveLayer before drawing anything that might require erasing. After that's done (thus creating a new layer for you to use), you can then draw with any Color to fill, or draw with BlendMode.clear to erase. Lastly, call restore to "merge" the new layer into other existing layers.
For example, let's draw a red square and subtract a circle from it:
void paint(Canvas canvas, Size size) {
canvas.saveLayer(Rect.largest, Paint());
canvas.drawRect(Rect.fromLTWH(0, 0, 80, 80), Paint()..color = Colors.red);
canvas.drawCircle(Offset(40, 40), 40, Paint()..blendMode = BlendMode.clear);
canvas.restore();
}
Sample result:
May this code can help you!
class DrawingPainter extends CustomPainter {
List<DrawingPoints> pointsList;
List<Offset> offsetPoints = List();
DrawingPainter({
this.pointsList,
});
#override
void paint(Canvas canvas, Size size) {
canvas.saveLayer(Rect.fromLTWH(0, 0, size.width, size.height), Paint());
for (int i = 0; i < pointsList.length - 1; i++) {
if (pointsList[i] != null && pointsList[i + 1] != null) {
canvas.drawLine(pointsList[i].points, pointsList[i + 1].points, pointsList[i].paint);
canvas.drawCircle(pointsList[i].points, pointsList[i].paint.strokeWidth/2, pointsList[i].paint);
}
}
canvas.restore();
}
#override
bool shouldRepaint(DrawingPainter oldDelegate) => true;
}
class DrawingPoints {
Paint paint;
Offset points;
DrawingPoints({this.points, this.paint});
}
You need saveLayer, then restores to save Paint
Maybe you need to add this code to te Statefull widget.
void changeBrush(bool isErease){
setState(() {
if ( isErease ){
paint = Paint();
paint.blendMode = BlendMode.clear;
paint.color = Colors.white;
paint.strokeWidth = strokeWidth;
}else{
paint = Paint();
paint.isAntiAlias = true;
paint.color = selectedColor.withOpacity(opacity);
paint.strokeWidth = strokeWidth;
}
});
}
may this code help you...
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:animated_floatactionbuttons/animated_floatactionbuttons.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:image_picker/image_picker.dart';
import 'package:permission_handler/permission_handler.dart';
var appbarcolor = Colors.blue;
class CanvasPainting_test extends StatefulWidget {
#override
_CanvasPainting_testState createState() => _CanvasPainting_testState();
}
class _CanvasPainting_testState extends State<CanvasPainting_test> {
GlobalKey globalKey = GlobalKey();
List<TouchPoints> points = List();
double opacity = 1.0;
StrokeCap strokeType = StrokeCap.round;
double strokeWidth = 3.0;
double strokeWidthforEraser = 3.0;
Color selectedColor;
Future<void> _pickStroke() async {
//Shows AlertDialog
return showDialog<void>(
context: context,
//Dismiss alert dialog when set true
barrierDismissible: true, // user must tap button!
builder: (BuildContext context) {
//Clips its child in a oval shape
return ClipOval(
child: AlertDialog(
//Creates three buttons to pick stroke value.
actions: <Widget>[
//Resetting to default stroke value
FlatButton(
child: Icon(
Icons.clear,
),
onPressed: () {
strokeWidth = 3.0;
Navigator.of(context).pop();
},
),
FlatButton(
child: Icon(
Icons.brush,
size: 24,
),
onPressed: () {
strokeWidth = 10.0;
Navigator.of(context).pop();
},
),
FlatButton(
child: Icon(
Icons.brush,
size: 40,
),
onPressed: () {
strokeWidth = 30.0;
Navigator.of(context).pop();
},
),
FlatButton(
child: Icon(
Icons.brush,
size: 60,
),
onPressed: () {
strokeWidth = 50.0;
Navigator.of(context).pop();
},
),
],
),
);
},
);
}
Future<void> _opacity() async {
//Shows AlertDialog
return showDialog<void>(
context: context,
//Dismiss alert dialog when set true
barrierDismissible: true,
builder: (BuildContext context) {
//Clips its child in a oval shape
return ClipOval(
child: AlertDialog(
//Creates three buttons to pick opacity value.
actions: <Widget>[
FlatButton(
child: Icon(
Icons.opacity,
size: 24,
),
onPressed: () {
//most transparent
opacity = 0.1;
Navigator.of(context).pop();
},
),
FlatButton(
child: Icon(
Icons.opacity,
size: 40,
),
onPressed: () {
opacity = 0.5;
Navigator.of(context).pop();
},
),
FlatButton(
child: Icon(
Icons.opacity,
size: 60,
),
onPressed: () {
//not transparent at all.
opacity = 1.0;
Navigator.of(context).pop();
},
),
],
),
);
},
);
}
Future<void> _pickStrokeforEraser() async {
//Shows AlertDialog
return showDialog<void>(
context: context,
//Dismiss alert dialog when set true
barrierDismissible: true, // user must tap button!
builder: (BuildContext context) {
//Clips its child in a oval shape
return ClipOval(
child: AlertDialog(
//Creates three buttons to pick stroke value.
actions: <Widget>[
//Resetting to default stroke value
FlatButton(
child: Icon(
Icons.clear,
),
onPressed: () {
strokeWidthforEraser = 3.0;
Navigator.of(context).pop();
},
),
FlatButton(
child: Icon(
Icons.brush,
size: 24,
),
onPressed: () {
strokeWidthforEraser = 10.0;
Navigator.of(context).pop();
},
),
FlatButton(
child: Icon(
Icons.brush,
size: 40,
),
onPressed: () {
strokeWidthforEraser = 30.0;
Navigator.of(context).pop();
},
),
FlatButton(
child: Icon(
Icons.brush,
size: 60,
),
onPressed: () {
strokeWidthforEraser = 50.0;
Navigator.of(context).pop();
},
),
],
),
);
},
);
}
Future<void> _save() async {
RenderRepaintBoundary boundary =
globalKey.currentContext.findRenderObject();
ui.Image image = await boundary.toImage();
ByteData byteData = await image.toByteData(format: ui.ImageByteFormat.png);
Uint8List pngBytes = byteData.buffer.asUint8List();
//Request permissions if not already granted
if (!(await Permission.storage.status.isGranted))
await Permission.storage.request();
final result = await ImageGallerySaver.saveImage(
Uint8List.fromList(pngBytes),
quality: 60,
name: "canvas_image");
print(result);
}
String erase = 'yes';
List<Widget> fabOption() {
return <Widget>[
FloatingActionButton(
backgroundColor: appbarcolor,
heroTag: "camera",
child: Icon(Icons.camera),
tooltip: 'camera',
onPressed: () {
//min: 0, max: 50
setState(() {
erase = 'yes';
this._showDialog();
// _save();
});
},
),
FloatingActionButton(
backgroundColor: appbarcolor,
heroTag: "paint_save",
child: Icon(Icons.file_download),
tooltip: 'Save',
onPressed: () {
//min: 0, max: 50
setState(() {
erase = 'yes';
_save();
});
},
),
FloatingActionButton(
backgroundColor: appbarcolor,
heroTag: "paint_stroke",
child: Icon(Icons.brush),
tooltip: 'Stroke',
onPressed: () {
//min: 0, max: 50
setState(() {
erase = 'yes';
_pickStroke();
});
},
),
// FloatingActionButton(
// heroTag: "paint_opacity",
// child: Icon(Icons.opacity),
// tooltip: 'Opacity',
// onPressed: () {
// //min:0, max:1
// setState(() {
// _opacity();
// });
// },
// ),
FloatingActionButton(
backgroundColor: appbarcolor,
heroTag: "Erase",
child: Icon(Icons.ac_unit),
tooltip: 'Erase',
onPressed: () {
//min: 0, max: 50
setState(() {
// _save();
// selectedColor = Colors.transparent;
// print(Platform.isAndroid);
erase = 'no';
_pickStrokeforEraser();
});
},
),
FloatingActionButton(
backgroundColor: appbarcolor,
heroTag: "Clear All",
child: Icon(Icons.clear),
tooltip: "Clear All",
onPressed: () {
setState(() {
erase = 'yes';
points.clear();
});
}),
FloatingActionButton(
backgroundColor: Colors.white,
heroTag: "color_red",
child: colorMenuItem(Colors.red),
tooltip: 'Color',
onPressed: () {
setState(() {
selectedColor = Colors.red;
});
},
),
FloatingActionButton(
backgroundColor: Colors.white,
heroTag: "color_green",
child: colorMenuItem(Colors.green),
tooltip: 'Color',
onPressed: () {
setState(() {
erase = 'yes';
selectedColor = Colors.green;
});
},
),
FloatingActionButton(
backgroundColor: Colors.white,
heroTag: "color_pink",
child: colorMenuItem(Colors.pink),
tooltip: 'Color',
onPressed: () {
setState(() {
selectedColor = Colors.pink;
});
},
),
FloatingActionButton(
backgroundColor: Colors.white,
heroTag: "color_blue",
child: colorMenuItem(Colors.blue),
tooltip: 'Color',
onPressed: () {
setState(() {
erase = 'yes';
selectedColor = Colors.blue;
});
},
),
];
}
void _showDialog() {
// flutter defined function
showDialog(
context: context,
builder: (BuildContext context) {
// return object of type Dialog
return AlertDialog(
// title: new Text("Alert Dialog title"),
content: Row(
// mainAxisSize: MainAxisSize.min,
children: <Widget>[
RaisedButton(
onPressed: getImageCamera,
child: Text('From Camera'),
),
SizedBox(
width: 5,
),
RaisedButton(
onPressed: getImageGallery,
child: Text('From Gallery'),
)
],
),
);
},
);
}
File _image;
Future getImageCamera() async {
var image = await ImagePicker.pickImage(source: ImageSource.camera);
print(image);
if (image != null) {
setState(() {
_image = image;
});
Navigator.of(context, rootNavigator: true).pop('dialog');
}
}
Future getImageGallery() async {
var image = await ImagePicker.pickImage(source: ImageSource.gallery);
print(image);
if (image != null) {
setState(() {
_image = image;
print(_image);
});
Navigator.of(context, rootNavigator: true).pop('dialog');
}
}
/*-------------------------------------*/
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('paint on image and erase'),backgroundColor: Colors.blueGrey
// leading: IconButton(
// icon: Icon(Icons.arrow_back_ios),onPressed: (){
// Navigator.pop(context);
// },),
),
body: GestureDetector(
onPanUpdate: (details) {
setState(() {
RenderBox renderBox = context.findRenderObject();
erase!='no'? points.add(TouchPoints(
points: renderBox.globalToLocal(details.globalPosition),
paint: Paint()
..strokeCap = strokeType
..isAntiAlias = true
..color = selectedColor.withOpacity(opacity)
..strokeWidth = strokeWidth))
: points.add(TouchPoints(
points: renderBox.globalToLocal(details.globalPosition),
paint: Paint()
..color = Colors.transparent
..blendMode = BlendMode.clear
..strokeWidth = strokeWidthforEraser
..style = PaintingStyle.stroke
..isAntiAlias = true
));
});
},
onPanStart: (details) {
setState(() {
RenderBox renderBox = context.findRenderObject();
erase!='no'? points.add(TouchPoints(
points: renderBox.globalToLocal(details.globalPosition),
paint: Paint()
..strokeCap = strokeType
..isAntiAlias = true
..color = selectedColor.withOpacity(opacity)
..strokeWidth = strokeWidth))
: points.add(TouchPoints(
points: renderBox.globalToLocal(details.globalPosition),
paint: Paint()
..color = Colors.transparent
..blendMode = BlendMode.clear
..strokeWidth = strokeWidthforEraser
..style = PaintingStyle.stroke
..isAntiAlias = true
));
});
},
onPanEnd: (details) {
setState(() {
points.add(null);
});
},
child: RepaintBoundary(
key: globalKey,
child: Stack(
children: <Widget>[
Center(
child: _image == null
? Image.asset(
"assets/images/helo.jfif",
)
: Image.file(_image),
),
CustomPaint(
size: Size.infinite,
painter: MyPainter(
pointsList: points,
),
),
],
),
),
),
floatingActionButton: AnimatedFloatingActionButton(
fabButtons: fabOption(),
colorStartAnimation: appbarcolor,
colorEndAnimation: Colors.red[300],
animatedIconData: AnimatedIcons.menu_close),
);
}
Widget colorMenuItem(Color color) {
return GestureDetector(
onTap: () {
setState(() {
selectedColor = color;
});
},
child: ClipOval(
child: Container(
padding: const EdgeInsets.only(bottom: 8.0),
height: 36,
width: 36,
color: color,
),
),
);
}
}
class MyPainter extends CustomPainter {
MyPainter({this.pointsList});
//Keep track of the points tapped on the screen
List<TouchPoints> pointsList;
List<Offset> offsetPoints = List();
//This is where we can draw on canvas.
#override
void paint(Canvas canvas, Size size) {
canvas.saveLayer(Rect.fromLTWH(0, 0, size.width, size.height), Paint());
for (int i = 0; i < pointsList.length - 1; i++) {
if (pointsList[i] != null && pointsList[i + 1] != null) {
canvas.drawLine(pointsList[i].points, pointsList[i + 1].points, pointsList[i].paint);
canvas.drawCircle(pointsList[i].points, pointsList[i].paint.strokeWidth/2, pointsList[i].paint);
}
}
canvas.restore();
}
//Called when CustomPainter is rebuilt.
//Returning true because we want canvas to be rebuilt to reflect new changes.
#override
bool shouldRepaint(MyPainter oldDelegate) => true;
}
//Class to define a point touched at canvas
class TouchPoints {
Paint paint;
Offset points;
TouchPoints({this.points, this.paint});
}
Wrap your custom paint widget into an Opacity widget with opacity of 0.99:
Opacity(
opacity: .99,
child: CustomPaint(
size: const Size(double.infinity, 100),
painter: MyPainter(),
),
),
I don't know why, but this fix the problem without any change in your painter class.
Came here looking for the opposite (clip the corners of the square and keep the inside). The goal was to draw a rounded rectangle out of several non rounded overlapping rectangles.
This is what worked for this:
canvas.save();
RRect clipRectangle = RRect.fromRectAndRadius(
Rect.fromLTWH(0, 0, 80, 80),
Radius.circular(4),
);
canvas.clipRRect(clipRectangle);
canvas.drawRect(Rect.fromLTWH(0, 0, 80, 80), Paint()..color = Colors.red);
canvas.drawRect(Rect.fromLTWH(0, 20, 80, 80), Paint()..color = Colors.blue);
canvas.restore();