Flutter: show only special part of an image - dart

This is how I show a picture:
return Scaffold(
body: Center(
child: Image.asset('man_face.jpg'),
),
);
And this the result: https://imgur.com/a/CPrQgvS
I want to show only special part of the picture. For example a rectangle with x: 250 and y: 360 and width: 200 and height: 150.
Which it should be something like this: https://imgur.com/a/p41y3nx
How can I do that?

you might want to look into this library. brendan-duncan/image. a great tool to manipulate images in flutter.

Here is a code I came up with
it accept an image url and a rect and display only the rect part of the image
import 'dart:async';
import 'package:flutter/material.dart';
import 'dart:ui' as ui;
class PartImagePainter extends StatefulWidget {
String imageUrl;
Rect rect;
PartImagePainter(this.imageUrl, this.rect);
#override
_PartImagePainterState createState() => _PartImagePainterState();
}
class _PartImagePainterState extends State<PartImagePainter> {
Future<ui.Image> getImage(String path) async {
Completer<ImageInfo> completer = Completer();
var img = new NetworkImage(path);
img
.resolve(ImageConfiguration())
.addListener(ImageStreamListener((ImageInfo info, bool _) {
completer.complete(info);
}));
ImageInfo imageInfo = await completer.future;
return imageInfo.image;
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: getImage(widget.imageUrl),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
// If the Future is complete, display the preview.
return paintImage(snapshot.data);
} else {
// Otherwise, display a loading indicator.
return Center(child: CircularProgressIndicator());
}
});
}
paintImage(image) {
return CustomPaint(
painter: ImagePainter(image, widget.rect),
child: SizedBox(
width: MediaQuery.of(context).size.width,
height: widget.rect.height,
),
);
}
}
class ImagePainter extends CustomPainter {
ui.Image resImage;
Rect rectCrop;
ImagePainter(this.resImage, this.rectCrop);
#override
void paint(Canvas canvas, Size size) {
if (resImage == null) {
return;
}
final Rect rect = Offset.zero & size;
final Size imageSize =
Size(resImage.width.toDouble(), resImage.height.toDouble());
FittedSizes sizes = applyBoxFit(BoxFit.fitWidth, imageSize, size);
Rect inputSubRect = rectCrop;
final Rect outputSubRect =
Alignment.center.inscribe(sizes.destination, rect);
final paint = Paint()
..color = Colors.white
..style = PaintingStyle.fill
..strokeWidth = 4;
canvas.drawRect(rect, paint);
canvas.drawImageRect(resImage, inputSubRect, outputSubRect, Paint());
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}

try using Alignment property and set fit to none.
Image.network(
"your image.png",
// move on the X axis to right 10% of the image and 0% on the Y Axis
alignment: const Alignment(0.1,0),
// set fit to none
fit: BoxFit.none,
// use scale to zoom out of the image
scale: 2,
)

One way suggested by the official doc of Flutter, is to:
To display a subpart of an image, consider using a CustomPainter and Canvas.drawImageRect.
ref: https://api.flutter.dev/flutter/painting/DecorationImage/alignment.html
Thus here is my full code. Use PartImage to show what you want.
class PartImage extends StatefulWidget {
const PartImage({
Key key,
#required this.imageProvider,
#required this.transform,
}) : assert(imageProvider != null),
super(key: key);
final ImageProvider imageProvider;
final Matrix4 transform;
#override
_PartImageState createState() => _PartImageState();
}
class _PartImageState extends State<PartImage> {
ImageStream _imageStream;
ImageInfo _imageInfo;
#override
void didChangeDependencies() {
super.didChangeDependencies();
_getImage();
}
#override
void didUpdateWidget(PartImage oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.imageProvider != oldWidget.imageProvider) _getImage();
}
void _getImage() {
final oldImageStream = _imageStream;
_imageStream = widget.imageProvider.resolve(createLocalImageConfiguration(context));
if (_imageStream.key != oldImageStream?.key) {
final listener = ImageStreamListener(_updateImage);
oldImageStream?.removeListener(listener);
_imageStream.addListener(listener);
}
}
void _updateImage(ImageInfo imageInfo, bool synchronousCall) {
setState(() {
_imageInfo = imageInfo;
});
}
#override
void dispose() {
_imageStream.removeListener(ImageStreamListener(_updateImage));
super.dispose();
}
#override
Widget build(BuildContext context) {
return RawPartImage(
image: _imageInfo?.image, // this is a dart:ui Image object
scale: _imageInfo?.scale ?? 1.0,
transform: widget.transform,
);
}
}
/// ref: [RawImage]
class RawPartImage extends StatelessWidget {
final ui.Image image;
final double scale;
final Matrix4 transform;
const RawPartImage({Key key, this.image, this.scale, this.transform}) : super(key: key);
#override
Widget build(BuildContext context) {
return CustomPaint(
painter: _RawPartImagePainter(
image: image,
scale: scale,
transform: transform,
),
);
}
}
class _RawPartImagePainter extends CustomPainter {
final ui.Image image;
final double scale;
final Matrix4 transform;
final painter = Paint();
_RawPartImagePainter({this.image, this.scale, this.transform});
#override
void paint(Canvas canvas, Size size) {
if (image == null) {
return;
}
final transformInv = Matrix4.inverted(transform);
final dst = Offset.zero & size;
final src = Rect.fromPoints(
transformOffset(transformInv, dst.topLeft),
transformOffset(transformInv, dst.bottomRight),
);
// print('src=$src dst=$dst');
canvas.drawImageRect(image, src, dst, painter);
}
#override
bool shouldRepaint(covariant _RawPartImagePainter oldDelegate) {
return oldDelegate.image != image || //
oldDelegate.scale != scale ||
oldDelegate.transform != transform;
}
}
Offset transformOffset(Matrix4 transform, Offset offset) {
Vector4 vecOut = transform * Vector4(offset.dx, offset.dy, 0, 1);
return Offset(vecOut.x, vecOut.y);
}
By the way, if you are interested in knowing what happens behind drawImageRect:
Have a search https://github.com/flutter/engine/search?q=drawImageRect
This seems the C++ code that drawImageRect (i.e. _drawImageRect actually calls: https://github.com/flutter/engine/blob/6bc70e4a114ff4c01b60c77bae754bace5683f6d/lib/ui/painting/canvas.cc#L330
It calls canvas_->drawImageRect. What is canvas_? From the header file we see it is of type SkCanvas* canvas_;.
Then we go into the world of Skia (not Flutter or Dart anymore). https://skia.org/user/api/skcanvas_overview for an overview of SkCanvas. And https://api.skia.org/classSkCanvas.html#a680ab85c3c7b5eab23b853b97f914334 for the actual SkCanvas.drawImageRect documentation.

Related

How to use setstate() in flutter to change the custompaint color at certain intervals inside a camera app?

I need to change color of the custompaint at certain intervals (like Timer.periodic) inside a camera app using foregroundpainter. How can I achieve it? I am able to print "timer" inside setstate() but color (r,g,b values) doesn't change.It appears that setstate() func gets processed (since I get "timer" printed) but custompaint doesn't change r,g,b values.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:camera/camera.dart';
List<CameraDescription> cameras;
Future<void> main() async {
cameras = await availableCameras();
runApp(CameraApp());
}
var temp;
class CameraApp extends StatefulWidget {
#override
_CameraAppState createState() => _CameraAppState();
}
var r = 255;
var g = 0;
var b = 0;
class _CameraAppState extends State<CameraApp> {
CameraController controller;
var crossPainter = CrossPainter(r, g, b);
#override
void initState() {
super.initState();
controller = CameraController(cameras[0],
ResolutionPreset.medium);
controller.initialize().then((_) {
if (!mounted) {
return;
}
setState(() {
Timer(new Duration(seconds: 5), () {
r = 0;
g = 255;
b = 0;
print('timer');
crossPainter = CrossPainter(r, g, b);
});
});
});
}
#override
void dispose() {
controller?.dispose();
super.dispose();
}
var t = Timer.periodic(new Duration(milliseconds: 500),
(Timer timer)=> CrossPainter(225,0,0));
#override
Widget build(BuildContext context) {
if (!controller.value.isInitialized) {
return Container();
}
return CustomPaint(
//foregroundPainter: CrossPainter(r, g, b),
foregroundPainter:crossPainter,
size:Size(100, 100),
willChange:true,
child: CameraPreview(controller));
}
}
class CrossPainter extends CustomPainter{
#override
Paint _paint;
CrossPainter(r,g,b){
_paint = Paint()
..color = Color.fromRGBO(r,g,b,1)
..strokeWidth = 2.0
..strokeCap = StrokeCap.round;
}
#override
void paint(Canvas canvas, Size size) {
var rect = Offset(100,0.0) & size;
canvas.drawRect(rect, _paint);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}

Drawing over an image downloaded from remote server

I need to do the following:
Download a PNG resource from a server
Draw several rectangles over that image with different color depending on state
Display that image in Zoomable Image View
I have a working code in an Android app using Canvas, but I cannot figure out how to do that with Flutter.
Here is the code that downloads the resource:
static Future<File> getImageFromUrl(String url) async {
final directory = await getApplicationDocumentsDirectory();
final file = File("$directory/${_getSHA(url)}.png");
if (await file.exists()) {
// Returns the cached file
} else {
final response = await http.get(url);
if (response.statusCode >= 200 && response.statusCode < 300) {
await file.writeAsBytes(response.bodyBytes);
} else {
return null;
}
}
return file;
}
What should I do next? I tried using PictureRecorder and Canvas, but I cannot find a way to draw image from the file on those canvas and then convert it to Image because I cannot extract width and height from the file.
EDIT:
Below is the Android code equivalent that I would like to implement in Flutter.
// Here we have a bitmap from a file
Bitmap mapBitmap = getBitmap();
Canvas mapCanvas = new Canvas(mapBitmap);
mapDrawable.setBounds(0, 0, mapCanvas.getWidth(), mapCanvas.getHeight());
mapDrawable.draw(mapCanvas);
canvasWidth = mapCanvas.getWidth();
canvasHeight = mapCanvas.getHeight();
Paint paint = new Paint();
for (java.util.Map.Entry<String, MapObject> entry : this.mapObjects.entrySet()) {
MapObject mapObject = entry.getValue();
paint.setColor(getContext().getResources().getColor(mapObject.getBackgroundColor()));
paint.setAlpha(100);
mapCanvas.drawRect((int) (mapObject.getPosX() * scaleX),
(int) (mapObject.getPosY() * scaleY),
(int) ((mapObject.getPosX() + mapObject.getWidth()) * scaleX),
(int) ((mapObject.getPosY() + mapObject.getHeight()) * scaleY),
paint);
}
photoView.setImageBitmap(mapBitmap);
I finally managed to solve the issue!
I created a renderer that creates a composite image (background from the remote resource and adds rectangles in the foreground).
The renderer:
class MapRenderer {
ui.Image _mapBackgroundImage;
Future<ui.Codec> renderMap(String url, List<Sensor> sensors) async {
await _loadMapBackground(url);
var renderedMapImage = await _updateSensors(sensors);
var byteD = await renderedMapImage.toByteData(
format: ui.ImageByteFormat.png);
return ui.instantiateImageCodec(Uint8List.view(byteD.buffer));
}
Future<ui.Image> _updateSensors(List<Sensor> sensors) async {
ui.PictureRecorder recorder = ui.PictureRecorder();
Canvas c = Canvas(recorder);
var paint = ui.Paint();
c.drawImage(_mapBackgroundImage, ui.Offset(0.0, 0.0), paint);
for (Sensor s in sensors) {
paint.color = (s.availability ? CustomColors.npSensorFree : CustomColors
.npSensorOccupied);
c.drawRect(
ui.Rect.fromPoints(ui.Offset(s.posX, s.posY),
ui.Offset(s.posX + s.width, s.posY + s.height)),
paint,
);
}
return recorder
.endRecording()
.toImage(_mapBackgroundImage.width, _mapBackgroundImage.height);
}
Future<void> _loadMapBackground(String url) async {
var imageBytes = await _getLocalCopyOrLoadFromUrl(url);
if (imageBytes != null) {
_mapBackgroundImage = await _getImageFromBytes(imageBytes);
} else {
return null;
}
}
Future<ui.Image> _getImageFromBytes(Uint8List bytes) async {
var imageCodec = await ui.instantiateImageCodec(bytes);
var frame = await imageCodec.getNextFrame();
return frame.image;
}
Future<Uint8List> _getLocalCopyOrLoadFromUrl(String url) async {
final directory = await getApplicationDocumentsDirectory();
final file = File("${directory.path}/${_getSHA(url)}.png");
if (await file.exists()) {
return await file.readAsBytes();
} else {
Uint8List resourceBytes = await _loadFromUrl(url);
if (resourceBytes != null) {
await file.writeAsBytes(resourceBytes);
return resourceBytes;
} else {
return null;
}
}
}
Future<Uint8List> _loadFromUrl(String url) async {
final response = await http.get(url);
if (response.statusCode >= 200 && response.statusCode < 300) {
return response.bodyBytes;
} else {
return null;
}
}
String _getSHA(String sth) {
var bytes = utf8.encode(sth);
var digest = sha1.convert(bytes);
return digest.toString();
}
void dispose() {
_mapBackgroundImage.dispose();
}
}
And to supply the image to the ZoomableImage I created a custom ImageProvider:
class MapImageProvider extends ImageProvider<MapImageProvider> {
final String url;
final List<Sensor> sensors;
final MapRenderer mapRenderer = MapRenderer();
MapImageProvider(this.url, this.sensors);
#override
ImageStreamCompleter load(MapImageProvider key) {
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key),
scale: 1.0,
informationCollector: (StringBuffer information) {
information.writeln('Image provider: $this');
information.write('Image key: $key');
});
}
Future<ui.Codec> _loadAsync(MapImageProvider key) async {
assert(key == this);
return await mapRenderer.renderMap(url, sensors);
}
#override
bool operator ==(Object other) =>
identical(this, other) ||
other is MapImageProvider &&
runtimeType == other.runtimeType &&
url == other.url;
#override
int get hashCode => url.hashCode;
#override
String toString() => '$runtimeType("$url")';
#override
Future<MapImageProvider> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<MapImageProvider>(this);
}
}
If anybody knows a better way to convert an Image to Codec or to even skip this step, please comment (MapRenderer.renderMap function).
In general to simply display an image from the internet you can use the Image.network constructor. If you want to further customize the interaction, for example showing rectangles based on its loading state, you can use the Image class and pass a NetworkImage to its constructor. The NetworkImage allows you to listen to loading and error events.
To draw above the Image I would simply suggest using the Stack Widget.
If you wanna add zooming functionality to the image, you should consider using the zoomable_image or photo_view package to replace the Image in the code below.
Also, if caching is necessary you can use the CachedNetworkImageProvider from the cached_network_image package.
The example below shows a yellow rectangle on a loading image, a green rectangle on a fully loaded image and a red rectangle if the loading crashed. It is a full application, you can copy & paste it in your IDE and try it out.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Network Image Download',
theme: ThemeData(),
home: MainPage(),
);
}
}
class MainPage extends StatefulWidget {
#override
State<StatefulWidget> createState() => MainPageState();
}
class MainPageState extends State<MainPage> {
ImageProvider provider;
bool loaded;
bool error;
#override
void initState() {
super.initState();
loaded = false;
error = false;
provider = NetworkImage('https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png');
provider.resolve(ImageConfiguration()).addListener((_, __) {
setState(() {
loaded = true;
});
}, onError: (_, __) {
setState(() {
error = true;
});
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Stack(
alignment: Alignment.center,
children: <Widget>[
Image(image: provider),
Container(
width: 75.0,
height: 75.0,
color: colorByState(),
)
],
),
),
);
}
Color colorByState() {
if (error) {
return Colors.red;
} else if (loaded) {
return Colors.green;
} else {
return Colors.yellow;
}
}
}

Flutter: Scratch card

Here is the example of a scratch card in react-native, I want to implement in Flutter, But I am not getting any way to do this. I tried with blendMode but it is not working, and even there is no clear functionality are given in CustomPaint in the flutter. https://github.com/aleksik/react-scratchcard.
Future<Null> main() async {
runApp(MaterialApp(home: Scaffold(body: new Roller())));
}
class Roller extends StatefulWidget {
#override
_ScratchCardState createState() => new _ScratchCardState();
}
class _ScratchCardState extends State<Roller> {
ui.Image _image;
String _urlImage = 'assets/BedRoom.png';
List<Offset> _points = <Offset>[];
#override
void initState() {
// TODO: implement initState
super.initState();
super.initState();
load(_urlImage).then((j) {
_image = j;
print('image:${_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;
}
void _onPanStart(DragStartDetails dt) {
print('drag start ');
setState(() {
});
}
Offset _localPosition;
void _onPanUpdate(DragUpdateDetails details) {
setState(() {
RenderBox object = context.findRenderObject();
_localPosition = object.globalToLocal(details.globalPosition);
_points = new List.from(_points)..add(_localPosition);
});
}
_onPanEnd(DragEndDetails dt) {
_points.add(null);
}
#override
void dispose() {
super.dispose();
}
Widget _scale(BuildContext context) {
return GestureDetector(
onPanStart: _onPanStart,
onPanUpdate: _onPanUpdate,
onPanEnd: _onPanEnd,
onDoubleTap: () {
setState(() {
_points.clear();
});
},
child: Container(
child: CustomPaint(
painter: ScratchCard(
imagePath: _image, points: _points,),
),
),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: _scale(context),
);
}
}
class ScratchCard extends CustomPainter {
final ui.Image imagePath;
List<Offset> points;
ScratchCard({Key key, this.imagePath, this.points}) : super();
Paint _paint = Paint();
Rect rect, inputSubrect, outputSubrect;
Path path = new Path();
Paint paint1 = new Paint();
#override
void paint(Canvas canvas, Size size) {
_paint.blendMode = BlendMode.src;
if (imagePath != null)
canvas.drawImage(imagePath, Offset(10.0, 100.0), _paint);
Paint paint = new Paint()
..color = Colors.white
..strokeCap = StrokeCap.round
..strokeWidth = 30.0;
_paint.blendMode = BlendMode.clear;
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);
path.reset();
}
}
}
#override
bool shouldRepaint(ScratchCard oldDelegate) {
return true;
}
}
I've created a library for scratch cards in Flutter which supports both - color and image scratchable overlays. You can get it from https://pub.dev/packages/scratcher Feedback is more than welcome!
Example:
Scratcher(
brushSize: 30,
threshold: 50,
color: Colors.red,
onChange: (value) { print("Scratch progress: $value%"); },
onThreshold: () { print("Threshold reached, you won!"); },
child: Container(
height: 300,
width: 300,
child: Text("Hidden text"),
),
)

How to get widget's absolute coordinates on a screen in Flutter?

How to get widget's absolute coordinates on a screen in Flutter?
Or its offset in a parent
Example:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Simple app',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: SimpleScreen(
title: 'Simple screen',
),
);
}
}
class SimpleScreen extends StatefulWidget {
final String title;
SimpleScreen({Key key, this.title}) : super(key: key);
#override
_SimpleScreenState createState() => new _SimpleScreenState();
}
class _SimpleScreenState extends State<SimpleScreen> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Container(
width: 48.0,
height: 48.0,
color: Colors.blue,
),
),
);
}
}
Goal is to get Container's offset in parent. If Center's size is 48.0, then Container's offset in Center's parent/root
Condition: you don't know the wrapper layout (it's a library widget), should be flexible, without hardcoded values
Thanks
You can use this extension I wrote (requires Dart 2.6):
extension GlobalKeyExtension on GlobalKey {
Rect? get globalPaintBounds {
final renderObject = currentContext?.findRenderObject();
final translation = renderObject?.getTransformTo(null).getTranslation();
if (translation != null && renderObject?.paintBounds != null) {
final offset = Offset(translation.x, translation.y);
return renderObject!.paintBounds.shift(offset);
} else {
return null;
}
}
}
Example how to use it:
final containerKey = GlobalKey();
Container(
key: containerKey,
width: 100,
height: 50,
)
void printWidgetPosition() {
print('absolute coordinates on screen: ${containerKey.globalPaintBounds}');
}
Also you can see similar solution from Daniel, but using BuildContext (using GlobalKey is relatively expensive).
I changed #vovahost extension function, because it's not working in transformed widgets(for example widgets inside RotatedBox). This is working on all widgets.
extension GlobalKeyExtension on GlobalKey {
Rect? get globalPaintBounds {
final renderObject = currentContext?.findRenderObject();
final matrix = renderObject?.getTransformTo(null);
if (matrix != null && renderObject?.paintBounds != null) {
final rect = MatrixUtils.transformRect(matrix, renderObject!.paintBounds);
return rect;
} else {
return null;
}
}
}
According to documentation using GlobalKey is relatively expensive. U can use just an Element(BuildContext) to calculate it's global bounds. I improved solution above from #vovahost:
extension GlobalPaintBounds on BuildContext {
Rect? get globalPaintBounds {
final renderObject = findRenderObject();
final translation = renderObject?.getTransformTo(null).getTranslation();
if (translation != null && renderObject?.paintBounds != null) {
final offset = Offset(translation.x, translation.y);
return renderObject!.paintBounds.shift(offset);
} else {
return null;
}
}
}
Maybe this simple widget is what you want :
rect_getter 0.0.1
This is a widget provide a simple way to get child's rectangle information after rendered.

How to touch paint a canvas?

Flutter newbie here. I'm currently trying to build a simple touch-drawing app with Flutter, but cannot work out how to trigger a canvas re-draw.
What I have is this:
I have a CustomPaint widget which contains a GestureDetector child. The CustomPaint's painter is getting a message whenever a touch event occurs, and stores the touch coordinates to draw a path on re-paint. Problem is, the paint method is never called.
This is the code I have so far:
import 'package:flutter/material.dart';
class WriteScreen extends StatefulWidget {
#override
_WriteScreenState createState() => new _WriteScreenState();
}
class KanjiPainter extends CustomPainter {
Color strokeColor;
var strokes = new List<List<Offset>>();
KanjiPainter( this.strokeColor );
void startStroke(Offset position) {
print("startStroke");
strokes.add([position]);
}
void appendStroke(Offset position) {
print("appendStroke");
var stroke = strokes.last;
stroke.add(position);
}
void endStroke() {
}
#override
void paint(Canvas canvas, Size size) {
print("paint!");
var rect = Offset.zero & size;
Paint fillPaint = new Paint();
fillPaint.color = Colors.yellow[100];
fillPaint.style = PaintingStyle.fill;
canvas.drawRect(
rect,
fillPaint
);
Paint strokePaint = new Paint();
strokePaint.color = Colors.black;
strokePaint.style = PaintingStyle.stroke;
for (var stroke in strokes) {
Path strokePath = new Path();
// Iterator strokeIt = stroke.iterator..moveNext();
// Offset start = strokeIt.current;
// strokePath.moveTo(start.dx, start.dy);
// while (strokeIt.moveNext()) {
// Offset off = strokeIt.current;
// strokePath.addP
// }
strokePath.addPolygon(stroke, false);
canvas.drawPath(strokePath, strokePaint);
}
}
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
class _WriteScreenState extends State<WriteScreen> {
GestureDetector touch;
CustomPaint canvas;
KanjiPainter kanjiPainter;
void panStart(DragStartDetails details) {
print(details.globalPosition);
kanjiPainter.startStroke(details.globalPosition);
}
void panUpdate(DragUpdateDetails details) {
print(details.globalPosition);
kanjiPainter.appendStroke(details.globalPosition);
}
void panEnd(DragEndDetails details) {
kanjiPainter.endStroke();
}
#override
Widget build(BuildContext context) {
touch = new GestureDetector(
onPanStart: panStart,
onPanUpdate: panUpdate,
onPanEnd: panEnd,
);
kanjiPainter = new KanjiPainter( const Color.fromRGBO(255, 255, 255, 1.0) );
canvas = new CustomPaint(
painter: kanjiPainter,
child: touch,
// child: new Text("Custom Painter"),
// size: const Size.square(100.0),
);
Container container = new Container(
padding: new EdgeInsets.all(20.0),
child: new ConstrainedBox(
constraints: const BoxConstraints.expand(),
child: new Card(
elevation: 10.0,
child: canvas,
)
)
);
return new Scaffold(
appBar: new AppBar(
title: new Text("Draw!")
),
backgroundColor: const Color.fromRGBO(200, 200, 200, 1.0),
body: container,
);
}
}
According to CustomPainter docs you must notify paint widget whenever repainting is needed
The most efficient way to trigger a repaint is to either extend this class and supply a repaint argument to the constructor of the CustomPainter, where that object notifies its listeners when it is time to repaint, or to extend Listenable (e.g. via ChangeNotifier) and implement CustomPainter, so that the object itself provides the notifications directly. In either case, the CustomPaint widget or RenderCustomPaint render object will listen to the Listenable and repaint whenever the animation ticks, avoiding both the build and layout phases of the pipeline.
E.g. KanjiPainter should extend ChangeNotifier and implement CustomPainter. And when you change strokes, invoke notifyListeners
And also build function always creates new KanjiPainter, this will remove all old data. You can init painter in initState once.
Working example:
class WriteScreen extends StatefulWidget {
#override
_WriteScreenState createState() => _WriteScreenState();
}
class KanjiPainter extends ChangeNotifier implements CustomPainter {
Color strokeColor;
var strokes = <List<Offset>>[];
KanjiPainter(this.strokeColor);
bool hitTest(Offset position) => true;
void startStroke(Offset position) {
print("startStroke");
strokes.add([position]);
notifyListeners();
}
void appendStroke(Offset position) {
print("appendStroke");
var stroke = strokes.last;
stroke.add(position);
notifyListeners();
}
void endStroke() {
notifyListeners();
}
#override
void paint(Canvas canvas, Size size) {
print("paint!");
var rect = Offset.zero & size;
Paint fillPaint = Paint();
fillPaint.color = Colors.yellow[100]!;
fillPaint.style = PaintingStyle.fill;
canvas.drawRect(rect, fillPaint);
Paint strokePaint = new Paint();
strokePaint.color = Colors.black;
strokePaint.style = PaintingStyle.stroke;
for (var stroke in strokes) {
Path strokePath = new Path();
// Iterator strokeIt = stroke.iterator..moveNext();
// Offset start = strokeIt.current;
// strokePath.moveTo(start.dx, start.dy);
// while (strokeIt.moveNext()) {
// Offset off = strokeIt.current;
// strokePath.addP
// }
strokePath.addPolygon(stroke, false);
canvas.drawPath(strokePath, strokePaint);
}
}
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
#override
// TODO: implement semanticsBuilder
SemanticsBuilderCallback? get semanticsBuilder => null;
#override
bool shouldRebuildSemantics(covariant CustomPainter oldDelegate) {
// TODO: implement shouldRebuildSemantics
return true;
}
}
class _WriteScreenState extends State<WriteScreen> {
late GestureDetector touch;
late CustomPaint canvas;
late KanjiPainter kanjiPainter;
void panStart(DragStartDetails details) {
print(details.globalPosition);
kanjiPainter.startStroke(details.globalPosition);
}
void panUpdate(DragUpdateDetails details) {
print(details.globalPosition);
kanjiPainter.appendStroke(details.globalPosition);
}
void panEnd(DragEndDetails details) {
kanjiPainter.endStroke();
}
#override
void initState() {
super.initState();
kanjiPainter = new KanjiPainter(const Color.fromRGBO(255, 255, 255, 1.0));
}
#override
Widget build(BuildContext context) {
touch = new GestureDetector(
onPanStart: panStart,
onPanUpdate: panUpdate,
onPanEnd: panEnd,
);
canvas = new CustomPaint(
painter: kanjiPainter,
child: touch,
// child: new Text("Custom Painter"),
// size: const Size.square(100.0),
);
Container container = new Container(
padding: new EdgeInsets.all(20.0),
child: new ConstrainedBox(
constraints: const BoxConstraints.expand(),
child: new Card(
elevation: 10.0,
child: canvas,
)));
return new Scaffold(
appBar: new AppBar(title: new Text("Draw!")),
backgroundColor: const Color.fromRGBO(200, 200, 200, 1.0),
body: container,
);
}
}
this library does it well.
example :
myCanvas.drawRect(Rect.fromLTRB(x * 0.88, y / 2, 25, 25), paint,
onTapDown: (t) {
print('tap');
});

Resources