Flutter - How to scale and draw over image in custompainter? - flutter

I am trying to draw over image on custompainter. I am using the example on flutter custompainter video and here is what I have so far. I can draw in the image but I cannot scale image. How do I scale image on gesture and draw in image? I would prefer not to use any package.
Container(
height: double.infinity,
width: double.infinity,
color: Colors.black87,
child: FittedBox(
child: GestureDetector(
onScaleStart: _scaleStartGesture,
onScaleUpdate: _scaleUpdateGesture,
onScaleEnd: (_) => _scaleEndGesture(),
child: SizedBox(
height: _image.height.toDouble(),
width: _image.width.toDouble(),
child: CustomPaint(
willChange: true,
painter: ImagePainter(
image: _image,
points: points
),
),
),
),
),
),

Merge LongPressDraggable or Draggable and GestureDetector's onScaleUpdate;
onScaleUpdate: (s) {
if (!(s.scale == 1 && s.rotation == 0)) {
controller
..setImageRotate(s.rotation)
..setImageScale(s.scale)
..setImageOffset(s.focalPoint);
setState(() {
message = controller.selectedController.toString();
});
}
},
Controller Class ;
final StreamController<ImageController> _controllerStreamController =
StreamController<ImageController>.broadcast();
Stream<ImageController> get controllerTypeStream =>
_controllerStreamController.stream;
double rotateSync;
void setImageRotate(double rotate) {
if (selectedController == null) {
rotateSync = rotate;
_controllerStreamController.sink.add(this);
}
}
Offset offset;
void setImageOffset(Offset rotate) {
if (selectedController == null) {
offset = rotate;
_controllerStreamController.sink.add(this);
}
}
double scaleSync;
void setImageScale(double scale) {
if (selectedController == null) {
scaleSync = scale;
_controllerStreamController.sink.add(this);
}
}
And than set image widget in 'Stack' widget ;
Stack -> GestureDetector -> Draggable -> Transform.scale -> Transform.translate -> Tranform.rotate -> SizedBox(ImageWidget)

Related

How to get InteractiveViewer image pixel coordinate in flutter?

i want to get pixel coordinate when i tapped the screen, i use interactiveView and GestureDetect in Fultter, i am so confused about the matrix transform , i am new to App develop, please give some advice if you could, very appraciate, Below is my code, which now i can zoom the image, but i can't calculate the coorect pixel coordiante when i click the screen. and since i have no idea how to calculate the ratio between pixel distance<->screen distance, i was stucked there. please help me.
What i am doing is i need pick a pixel position from the image, so i need zoom image first to get precise position,that's why i need ineractiveViewer . and at the same time i need record the gesture behavior, to monitor the behavior, then i wrapper InteractiveView to GestureDetect.
it look like this for now:
enter image description here
Widget mapView() {
double offsetX, offsetY;
return Stack(
children: <Widget>[
Positioned.fill(
child:
GestureDetector(
onTapUp: (TapUpDetails details) {
offsetX = details.localPosition.dx;
offsetY = details.localPosition.dy;
// print(
//"tap local pos, X: ${details.localPosition.dx}, Y: ${details.localPosition.dy}");
// print(
// "tap global pos, X: ${details.globalPosition.dx}, Y: ${details.globalPosition.dy}");
_childWasTappedAt = _transformationController!.toScene(details.localPosition);
// print(
// "child pos to scene , X: ${_childWasTappedAt!.dx}, Y: ${_childWasTappedAt!.dy}");
//double origin_scree_pixel_radio = 17;
MediaQueryData queryData;
queryData = MediaQuery.of(context);
double pixel_ratio = queryData.devicePixelRatio;
double scree_pixel_radio = (1.0 / _cur_scale_value!)*pixel_ratio;
double trans_x = -1.0 * _transformationController!.value.getTranslation().x;
double local_offset_x = offsetX;
double pixel_x = trans_x + local_offset_x * scree_pixel_radio;
print("scale: ${_cur_scale_value}");
print("radio: ${pixel_ratio}");
print("view tran x: ${trans_x}");
print("offset x: ${local_offset_x}");
//print("image_Info: ${_image_info.toString()}");
print("Pixel X: ${pixel_x}");
},
child:
InteractiveViewer(
transformationController: _transformationController,
minScale: 0.001,
maxScale: 200.0,
constrained: false,
child: Image.asset(
imagePath,
filterQuality:FilterQuality.high,
),
onInteractionEnd: (ScaleEndDetails details) {
_cur_scale_value = _transformationController!.value.getMaxScaleOnAxis();
//print("current scale: ${_cur_scale_value}");
},
onInteractionUpdate: (ScaleUpdateDetails details){
//print('onInteractionUpdate----' + details.toString());
},
),
),
),
Positioned(
top: 0.0,//_vehicle_y,
left: 0.0,//_vehicle_x,
child: Icon(Icons.favorite, color: Colors.red,),
),
],
);
}
Use this onInteractionUpdate method to get Coordinates. use also use different methods.
Vist This site for more info:-
https://api.flutter.dev/flutter/widgets/InteractiveViewer/onInteractionUpdate.html
https://api.flutter.dev/flutter/gestures/ScaleStartDetails/localFocalPoint.html
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: InteractiveViewer(
onInteractionUpdate: (v) {
print(v.localFocalPoint.dx);
print(v.localFocalPoint.dy);
},
child: Image.network(
"https://images.unsplash.com/photo-1643832678771-fdd9ed7638ae?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHx0b3BpYy1mZWVkfDd8Ym84alFLVGFFMFl8fGVufDB8fHx8&auto=format&fit=crop&w=2400&q=60",
fit: BoxFit.fitHeight,
),
),
),
);
}

Flutter: Zoom and pan a widget

I'm fighting with zoom/pan gestures.
I've found a partially suitable example in this book:
https://books.google.bg/books?id=ex-tDwAAQBAJ&pg=PA284&lpg=PA284&dq=flutter+_startLastOffset&source=bl&ots=YUQna09jIf&sig=ACfU3U0QrHwl2RdrVUv5EtpHaHFKx_cXhA&hl=en&sa=X&ved=2ahUKEwid9uPJ8abnAhVnlosKHTKQBn4Q6AEwAHoECAMQAQ#v=onepage&q=flutter%20_startLastOffset&f=false
which, I guess was based on this:
https://chromium.googlesource.com/external/github.com/flutter/flutter/+/refs/heads/dev/examples/layers/widgets/gestures.dart
And my code based on it:
Offset _startLastOffset = Offset.zero;
Offset _lastOffset = Offset.zero;
Offset _currentOffset = Offset.zero;
double _lastScale = 1.0;
double _currentScale = 1.0;
void _onScaleStart(ScaleStartDetails details) {
_startLastOffset = details.focalPoint;
_lastOffset = _currentOffset;
_lastScale = _currentScale;
}
void _onScaleUpdate(ScaleUpdateDetails details) {
if (details.scale != 1.0) {
// Scaling
double currentScale = _lastScale * details.scale;
if (currentScale < 0.5) {
currentScale = 0.5;
}
_currentScale = currentScale;
_bloc.setScale(_currentScale);
} else if (details.scale == 1.0) {
// We are not scaling but dragging around screen
// Calculate offset depending on current Image scaling.
Offset offsetAdjustedForScale = (_startLastOffset - _lastOffset) / _lastScale;
Offset currentOffset = details.focalPoint - (offsetAdjustedForScale * _currentScale);
_currentOffset = currentOffset;
_bloc.setOffset(_currentOffset);
}
}
---------------
child: StreamBuilder<Object>(
stream: _bloc.scale,
builder: (context, snapshot1) {
double _cscale = snapshot1.hasData? snapshot1.data : _currentScale;
return StreamBuilder<Object>(
stream: _bloc.offset,
builder: (context, snapshot2) {
Offset _coffset = snapshot2.hasData? snapshot2.data : _currentOffset;
return Transform(
transform: Matrix4.identity()
..scale(_cscale, _cscale)
..translate(_coffset.dx, _coffset.dy),
alignment: FractionalOffset.center,
child: Stack(
key: _imgStack,
fit: StackFit.expand,
children: <Widget>[
Image(image: AssetImage('assets/images/image.png')),
SvgPicture.asset(svgFile)
],
),
);
}
);
}
),
The Pan of which, however, behaves unnatural when the widget is zoomed - the widget moves much further than the finger, in the moving direction.
I managed to fix this by changing:
_currentOffset = currentOffset;
_bloc.setOffset(_currentOffset);
to:
_bloc.setOffset(currentOffset/_currentScale);
_currentOffset = currentOffset;
Which works exactly as expected, but just until the Second zoom.
After the second time I zoom, the widget get shifted on first touch.
On zoom-in it shifts to the right, on zoom-out it shifts to the left.
Any ideas?
Use InteractiveViewer widget. Here is an example.
#override
Widget build(BuildContext context) {
return Center(
child: InteractiveViewer(
boundaryMargin: EdgeInsets.all(100),
minScale: 0.1,
maxScale: 1.5,
child: Container(width: 200, height: 200, color: Colors.blue),
),
);
}

Flutter: Drag Draggable Stack item inside a Draggable Stack Item

I have a Draggable on a DragTarget as part of a Stack. Inside is another Stack with Draggables, again on DragTargets and so on... (Stack over Stack over Stack etc.).
The Draggable is a Positioned with a Listener telling where to be placed.
homeView.dart
body: Stack(children: [
DraggableWidget(parentKey, Offset(0, 0)),
]),
draggableWidget.dart
class DraggableWidget extends StatefulWidget {
final Key itemKey;
final Offset itemPosition;
DraggableWidget(this.itemKey, this.itemPosition);
#override
_DraggableWidgetState createState() => _DraggableWidgetState();
}
class _DraggableWidgetState extends State<DraggableWidget> {
Offset tempDelta = Offset(0, 0);
Window<List<Key>> item;
List<DraggableWidget> childList = [];
Map<Key, Window<List>> structureMap;
initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
structureMap = Provider.of<Data>(context).structureMap;
if (structureMap[widget.itemKey] != null) {
structureMap[widget.itemKey].childKeys.forEach(
(k) => childList.add(
DraggableWidget(k, item.position),
),
);
} else {
structureMap[widget.itemKey] = Window<List<Key>>(
title: 'App',
key: widget.itemKey,
size: Size(MediaQuery.of(context).size.width,
MediaQuery.of(context).size.height),
position: Offset(0, 0),
color: Colors.blue,
childKeys: []);
}
item = Provider.of<Data>(context).structureMap[widget.itemKey];
return Positioned(
top: item.position.dx,
left: item.position.dy,
child: DragTarget(
builder:
(buildContext, List<Window<List<Key>>> candidateData, rejectData) {
return Listener(
onPointerDown: (PointerDownEvent event) {},
onPointerUp: (PointerUpEvent event) {
setState(() {
item.position = Offset(item.position.dx + tempDelta.dx,
item.position.dy + tempDelta.dy);
tempDelta = Offset(0, 0);
});
},
onPointerMove: (PointerMoveEvent event) {
tempDelta = Offset((event.delta.dy + tempDelta.dx),
(event.delta.dx + tempDelta.dy));
},
child: Draggable(
childWhenDragging: Container(),
feedback: Container(
color: item.color,
height: item.size.height,
width: item.size.width,
),
child: Column(children: [
Text(item.title),
Container(
color: item.color,
height: item.size.height,
width: item.size.width,
child: ItemStackBuilder(widget.itemKey, item.position),
),
]),
data: item),
);
},
),
);
}
}
itemStackBuilder.dart
class ItemStackBuilder extends StatelessWidget {
final Key itemKey;
final Offset itemPosition;
ItemStackBuilder(this.itemKey, this.itemPosition);
#override
Widget build(BuildContext context) {
Map<Key, Window<List<Key>>> structureMap =
Provider.of<Data>(context).structureMap;
if (structureMap[itemKey] == null) {
structureMap[itemKey] = Window(size: Size(20, 20), childKeys: []);
}
return Stack(overflow: Overflow.visible, children: [
...stackItems(context),
Container(
height: structureMap[itemKey].size.height,
width: structureMap[itemKey].size.width,
color: Colors.transparent),
]);
}
List<Widget> stackItems(BuildContext context) {
List<Key> childKeyList =
Provider.of<Data>(context).structureMap[itemKey].childKeys;
var stackItemDraggable;
List<Widget> stackItemsList = [];
if (childKeyList == null || childKeyList.length < 1) {
stackItemsList = [Container()];
} else {
for (int i = 0; i < childKeyList.length; i++) {
stackItemDraggable = DraggableWidget(childKeyList[i], itemPosition);
stackItemsList.add(stackItemDraggable);
}
}
return stackItemsList;
}
}
When I want to move the Draggable item on top, the underlying Stack moves.
I tried it with a Listener widget and was able to detect all RenderBoxes inside the Stack.
But how can I select the specific Draggable and/or disable all the other layers? Is it a better idea to forget about Draggables and do it all with Positioned and GestureDetector?
Ok, it was my mistake not of the framework:
on itemStackBuilder.dart I used an additional Container to size the Stack. I was not able to recognise, because color was transparent:
Container(
height: structureMap[itemKey].size.height,
width: structureMap[itemKey].size.width,
color: Colors.transparent),
]);
}
After deleting this part, all works fine for now.

Flutter : Change size of stack when children overflow

I have a draggable widget (from https://medium.com/flutter-community/create-a-draggable-widget-in-flutter-50b61f12635d ) with a stack in a container (red color) consists of moveable children. Here is the widget tree:
I wanted to add a Gesture Transformations as FormBuilder ( https://github.com/flutter/flutter/blob/master/examples/flutter_gallery/lib/demo/transformations/transformations_demo.dart ) to transform the matrix, as you can see in the GIF, mainly zoom in/out and transform x/y.
class _HomeViewState extends State<HomeView> {
final _stackKey;
_HomeViewState(this._stackKey);
List<Widget> movableItems = [];
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: AppBar(
title: Text('SynApp'),
actions: <Widget>[
IconButton(
onPressed: () {
x = 200.0;
y = 200.0;
setState(() {
movableItems.add(
MoveableStackItem(_stackKey),
);
});
},
icon: Icon(Icons.add),
),
],
),
body: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
// Draw the scene as big as is available, but allow the user to
// translate beyond that to a visibleSize that's a bit bigger.
final Size size = Size(constraints.maxWidth, constraints.maxHeight);
final Size visibleSize = Size(size.width * 3, size.height * 2);
return GestureTransformable(
reset: _reset,
onResetEnd: () {
setState(() {
_reset = false;
});
},
child: new Container(
color: Colors.red,
child: Stack(
key: _stackKey,
overflow: Overflow.visible,
fit: StackFit.expand,
children: movableItems),
),
boundaryRect: Rect.fromLTWH(
-visibleSize.width / 2,
-visibleSize.height / 2,
visibleSize.width,
visibleSize.height,
),
initialTranslation: Offset(size.width, size.height),
size: size,
);
}),
);
}
}
The problem is:
a) the size of the stack is equal to the initial screen.
b)when I move items out of the screen, gestureDetection stops, the items are no longer moveable.
What I want:
I want to dynamically resize the size of the stack (the red box) depending on where I move the items.
I was able to find the position and size of the stack widget.
Size stackSize;
Offset stackPosition;
_MoveableStackItemState(this._stackKey);
getSizeAndPosition() {
RenderStack _stackStack = _stackKey.currentContext.findRenderObject();
stackSize = _stackStack.size;
stackPosition = _stackStack.localToGlobal(Offset.zero);
print('stackSize $stackSize');
print('stackPosition $stackPosition');
}
But I'm starting to get lost in advanced UI object orientated stateful widget manipulation.
You can wrap the red stack with an AnimatedContainer.
LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
// Draw the scene as big as is available, but allow the user to
// translate beyond that to a visibleSize that's a bit bigger.
final Size size = Size(constraints.maxWidth, constraints.maxHeight);
final Size visibleSize = Size(size.width * 3, size.height * 2);
return GestureTransformable(
reset: _reset,
onResetEnd: () {
setState(() {
_reset = false;
});
},
child: new AnimatedContainer(
color: Colors.red,
duration: Duration(milliseconds: 200),
width: _stackWidth,
height: _stackHeight,
child: Stack(
key: _stackKey,
overflow: Overflow.visible,
fit: StackFit.expand,
children: movableItems),
),
boundaryRect: Rect.fromLTWH(
-visibleSize.width / 2,
-visibleSize.height / 2,
visibleSize.width,
visibleSize.height,
),
initialTranslation: Offset(size.width, size.height),
size: size,
);
}),
Try to listen the following event of the GestureTransformable
onPanUpdate: (DragUpdateDetails details){
var deltaX = details.delta.dx;
var deltaY = details.delta.dy;
}
DragUpdateDetails object let you to know the delta
the amount the pointer has moved in the coordinate space of the event
receiver since the previous update
on the x and y axis.
Inside the "onPanUpdate" you can update the width and the height of the animated container related to the delta of the gesture.
setState((){
_stackHeight = /* ... */
_stackWidth = /* ... */
});

Flutter: inverted ClipOval

I am new to Flutter and I am trying to write a library to allow users to pan/zoom their profile picture.
In order to make it visual, I would like to stack their picture with an "inverted" ClipOval, to show the boundaries.
So far, this is the result I obtain:
This shows the boundaries but this is not user friendly and I would like to "invert" the ClipOval so that the center of the clip is "clear" and the outside is grayed out (something like a mask).
Is there any way to achieve this?
Here is the code I have so far (part of it comes from flutter_zoomable_image):
import 'dart:ui' as ui;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class ImagePanner extends StatefulWidget {
ImagePanner(this.image, {Key key}) : super(key: key);
/// The image to be panned
final ImageProvider image;
#override
_ImagePannerState createState() => new _ImagePannerState();
}
class _ImagePannerState extends State<ImagePanner> {
ImageStream _imageStream;
ui.Image _image;
double _zoom = 1.0;
Offset _offset = Offset.zero;
double _scale = 16.0;
#override
void didChangeDependencies() {
_resolveImage();
super.didChangeDependencies();
}
#override
void reassemble() {
_resolveImage();
super.reassemble();
}
#override
Widget build(BuildContext context) {
if (_image == null) {
return new Container();
}
return new Container(
width: double.INFINITY,
color: Colors.amber,
child: new Padding(
padding: new EdgeInsets.all(50.0),
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new AspectRatio(
aspectRatio: 1.0,
child: new Stack(
children: [
_child(),
new Opacity(
opacity: 0.5,
child: new ClipOval(
child: new Container(
color: Colors.black,
),
),
),
],
),
),
],
)),
);
}
Widget _child() {
Widget bloated = new CustomPaint(
child: new Container(),
painter: new _ImagePainter(
image: _image,
offset: _offset,
zoom: _zoom / _scale,
),
);
bloated = new Stack(
children: [
new Container(
),
bloated
],
);
return new Transform(
transform: new Matrix4.diagonal3Values(_scale, _scale, _scale),
child: bloated);
}
void _resolveImage() {
_imageStream = widget.image.resolve(createLocalImageConfiguration(context));
_imageStream.addListener(_handleImageLoaded);
}
void _handleImageLoaded(ImageInfo info, bool synchronousCall) {
print("image loaded: $info $synchronousCall");
setState(() {
_image = info.image;
});
}
}
class _ImagePainter extends CustomPainter {
const _ImagePainter({this.image, this.offset, this.zoom});
final ui.Image image;
final Offset offset;
final double zoom;
#override
void paint(Canvas canvas, Size size) {
paintImage(canvas: canvas, rect: offset & (size * zoom), image: image);
}
#override
bool shouldRepaint(_ImagePainter old) {
return old.image != image || old.offset != offset || old.zoom != zoom;
}
}
The outcome I would like to obtain is the following so that users will directly see the boundaries and will be able to center, pan, zoom their profile picture INSIDE the oval.
(I made this via Photoshop, since I don't know how to achieve this with Flutter)
Many thanks for your help.
There's a couple other ways you could do this - you could simply draw an overlay in a CustomCanvas using a path that has a circle & rectangle, as all you really need is a rectangular semi-transparent rectangle with a hole in it. But you can also use a CustomClipper which gives you more flexibility in the future without having to draw stuff manually.
void main() {
int i = 0;
runApp(new MaterialApp(
home: new SafeArea(
child: new Stack(
children: <Widget>[
new GestureDetector(
onTap: () {
print("Tapped! ${i++}");
},
child: new Container(
color: Colors.white,
child: new Center(
child: new Container(
width: 400.0,
height: 300.0,
color: Colors.red.shade100,
),
),
),
),
new IgnorePointer(
child: new ClipPath(
clipper: new InvertedCircleClipper(),
child: new Container(
color: new Color.fromRGBO(0, 0, 0, 0.5),
),
),
)
],
),
),
));
}
class InvertedCircleClipper extends CustomClipper<Path> {
#override
Path getClip(Size size) {
return new Path()
..addOval(new Rect.fromCircle(
center: new Offset(size.width / 2, size.height / 2),
radius: size.width * 0.45))
..addRect(new Rect.fromLTWH(0.0, 0.0, size.width, size.height))
..fillType = PathFillType.evenOdd;
}
#override
bool shouldReclip(CustomClipper<Path> oldClipper) => false;
}
IgnorePointer is needed, or events won't be propagated through the semi-transparent part (assuming you need touch events).
How this works is that the Path used by clipPath is a circle in the middle (you need to adjust the size manually) with a rectangle taking up the entire size. fillType = PathFillType.evenOdd is important because it tells the path's fill should be between the circle and the rectangle.
If you wanted to use a customPainter instead, the path would be the same and you'd just draw it instead.
This all results in this: