Since a few days I'm trying to keep a drawn circle (using a CustomPainter) on an image while the image gets scaled (zoom in/out). To render the image and enable the zoom gesture I use photo_view. I can move the circle according to the change of the view port as long as nothing gets scaled. All values (scale,offset) are provided by the PhotoViewController as a stream as shown below:
main.dart
import 'package:backtrack/markerPainter.dart';
import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.dart';
import 'dart:math' as math;
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
Offset position;
Offset startPosition;
Offset scaleChange;
Offset offsetToCenter;
double initialScale;
Offset forPoint;
PhotoViewScaleStateController controller;
PhotoViewController controller2;
double lastscale;
@override
void initState() {
super.initState();
controller = PhotoViewScaleStateController();
controller2 = PhotoViewController();
controller2.outputStateStream.listen((onData) {
// print("position change: " + onData.position.toString());
// print("scale change: " + onData.scale.toString());
if (position != null) {
setState(() {
final scaleToUser = initialScale + (initialScale - onData.scale);
//Change to the unscaled point inverted to reflect the opposite movement of the user
final newOffset = (offsetToCenter - onData.position) * -1;
print("new offset: " + newOffset.toString());
if (onData.scale != lastscale) {
if (onData.scale > initialScale) {
//zoom in
var zoomeChnage = initialScale-onData.scale;
scaleChange = startPosition * zoomeChnage;
print("new scaleChange: " + scaleChange.toString());
} else {
//zoom out
}
lastscale = onData.scale;
}
forPoint = newOffset-scaleChange;
print("new forPoint: " + forPoint.toString());
});
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: _buildMap(context));
}
Widget _buildMap(BuildContext context) {
return Container(
child: CustomPaint(
foregroundPainter: MyPainter(position, forPoint),
child: PhotoView(
imageProvider: AssetImage("asset/map.JPG"),
onTapUp: (a, b, c) {
setState(() {
position = b.localPosition;
offsetToCenter = c.position;
forPoint = Offset.zero;
initialScale = c.scale;
print("localPosition: " + b.localPosition.toString());
print("offsetToCenter: " + offsetToCenter.toString());
print("initialScale: " + initialScale.toString());
});
},
scaleStateController: controller,
controller: controller2,
),
),
);
}
}
Just in case, below is the widget that renders the circle on top of the image. markerPainter.dart
class MyPainter extends CustomPainter {
final Offset position;
final Offset centerOffset;
final Paint line;
MyPainter(this.position, this.centerOffset)
: line = new Paint()
..color = Colors.pink
..style = PaintingStyle.fill
..strokeWidth = 5;
@override
void paint(Canvas canvas, Size size) {
if (position == null) {
return;
}
canvas.drawCircle(position + centerOffset, 5, line);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
I really tried a lot but it looks like my brain can't solve it, any help is very welcome.
I found a solution, using this helper class I can calculate a relative point for an absolute point:
FlatMapState(
{this.imageScale,
this.initialViewPort,
this.imageSize,
this.viewPortDelta,
this.viewPortOffsetToCenter,
this.currentViewPort});
Offset absolutePostionToViewPort(Offset absolutePosition) {
var relativeViewPortPosition =
((imageSize * imageScale).center(Offset.zero) -
absolutePosition * imageScale) *
-1;
relativeViewPortPosition += viewPortOffsetToCenter;
return relativeViewPortPosition + viewPortDelta / -2;
}
}
The class gets more or less all information from the PhotoViewController update event like this:
bool _photoViewValueIsValid(PhotoViewControllerValue value) {
return value != null && value.position != null && value.scale != null;
}
FlatMapState createCurrentState(PhotoViewControllerValue photoViewValue) {
if (_photoViewValueIsValid(photoViewValue) && imageSize != null) {
if (lastViewPort == null) {
lastViewPort = stickyKey.currentContext.size;
}
return FlatMapState(
imageScale: photoViewValue.scale,
imageSize: imageSize,
initialViewPort: lastViewPort,
viewPortDelta: Offset(
lastViewPort.width - stickyKey.currentContext.size.width,
lastViewPort.height - stickyKey.currentContext.size.height),
viewPortOffsetToCenter: photoViewValue.position,
currentViewPort: Offset(stickyKey.currentContext.size.width,
stickyKey.currentContext.size.height),
);
} else {
//The map or image is still loading, the state can't be generated
return null;
}
}
Please note that the calculated value has to be drawn by setting the point of origin to the center like this:
@override
void paint(Canvas canvas, Size size) {
//In case the state is null or nothing to draw leave
if(flatMapState == null || drawableElements == null){
return;
}
//Center the canvas (0,0) is now the center, done to have the same origion as photoview
var viewPortCenter = flatMapState.initialViewPort.center(Offset.zero);
canvas.translate(viewPortCenter.dx, viewPortCenter.dy);
drawableElements.forEach((drawable) => drawable.draw(
canvas,
size,
flatMapState,
));
}