Closed Dev-Owl closed 4 years ago
@renancaraujo, can you please help us with this one, I found that in this comment you mentioned this was solved but we are unable to understand how to get the exact position within PhotoView when the scale is bigger than 1.
I think this question is about the same thing so any help from you or @SergeShkurko who created that pull request would be very useful.
@icelija Example:
// helper for get image resolution
Future<ui.Image> getImageInfo(ImageProvider<dynamic> provider) {
final completer = Completer<ui.Image>();
provider.resolve(ImageConfiguration()).addListener(
ImageStreamListener(
(ImageInfo info, bool _) => completer.complete(info.image)),
);
return completer.future;
}
// setup helpers in stateful widget
static final _initialScale = PhotoViewComputedScale.contained * 1;
static final _minScale = PhotoViewComputedScale.contained * 1;
static final _maxScale = PhotoViewComputedScale.covered * 4;
PageController _pageController = PageController();
bool isScaled = true;
void _spreadViewStateChanged(PhotoViewScaleState value) => setState(() {
isScaled = value == PhotoViewScaleState.initial ||
value == PhotoViewScaleState.covering;
});
// now setup PhotoViewGallery in build Builder( builder: (_) => { final mediaQuery = MediaQuery.of(context); final size = mediaQuery.size; final widthPercentsPx = size.width / 100; return PhotoViewGallery.builder( scaleStateChangedCallback: spreadViewStateChanged, builder: (, index) => _buildItem( index, size: size, widthPercentsPx: widthPercentsPx, ), pageController: _pageController, itemCount: 100, ); } );
// build item method
PhotoViewGalleryPageOptions _buildItem(
int index, {
Size size,
double widthPercentsPx,
}) =>
PhotoViewGalleryPageOptions.customChild(
child: YouWidget(),
childSize: Size(
400,
600,
),
initialScale: _initialScale,
minScale: _minScale,
maxScale: _maxScale,
onTapUp: (
BuildContext context,
TapUpDetails details,
PhotoViewControllerValue controllerValue,
) {
context.visitChildElements((element) {
// Find image widget inside YouWidget()
findChild
// get image resolution
final info = await getImageInfo(imageProvider);
// magic working for images where the height prevails
// over the width, honestly forgot what is happening here,
// you can play around with the values
final imageBoxWidth = widthPercentsPx *
(((info.width / info.height) + 0.2) * 100);
final imageSize = Size(imageBoxWidth, box.size.height);
_handleImageTapUp(
box,
context,
details,
controllerValue,
size,
imageSize,
);
});
});
});
// tap handler
double _calculateLeftOffset( Size fullSizePercentUnits, Size visibleArea, Offset visiblePosition, ) => ((fullSizePercentUnits.width * 100 - visibleArea.width) / 2) - visiblePosition.dx;
double _calculateRightOffset( Size fullSizePercentUnits, Size visibleArea, Offset visiblePosition, ) => ((fullSizePercentUnits.height * 100 - visibleArea.height) / 2) - visiblePosition.dy;
void _handleImageTapUp( RenderBox box, BuildContext context, TapUpDetails details, PhotoViewControllerValue controllerValue, Size size, Size imageSize, ) { final localPosition = box.globalToLocal(details.globalPosition); // ~1.375 - initial scale in [PhotoView] // 7.27 - unit multiplier final scale = (controllerValue.scale ?? 1.375) 7.27 0.1;
// Left & right padding between image and outside container
// ignore: omit_local_variable_types
final double horizontalOffset =
isScaled ? (size.width - imageSize.width) / 2 : 0;
// Tapped outside image (on omage padding)
if (isScaled &&
(localPosition.dx < horizontalOffset ||
localPosition.dx > horizontalOffset + imageSize.width)) {
return;
}
final fullWidthPercent = (imageSize.width / 100) * scale,
fullHeightPercent = (imageSize.height / 100) * scale;
final fullSizePercentUnits = Size(fullWidthPercent, fullHeightPercent);
final leftOffset = _calculateLeftOffset( fullSizePercentUnits, visibleArea, visiblePosition, ), topOffset = _calculateRightOffset( fullSizePercentUnits, visibleArea, visiblePosition, );
// real image tap position final tapPositionX = position.dy + topOffset, tapPositionY = position.dx + leftOffset; }
Hi, First of all thanks for the reply, it came out to be more work as I thought but I came up with a solution that covers this request but only 50% of my final goal. Let's start with my solution:
Let's start with the required inputs, all except one are coming from the PhotoViewControllerValue provided in the update stream. They are mapped to a little utility class as shown below:
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;
}
}
Future<ImageInfo> _getImage() {
final Completer completer = Completer<ImageInfo>();
final ImageStream stream = widget.imageProvider.resolve(
const ImageConfiguration(),
);
final listener = ImageStreamListener((
ImageInfo info,
bool synchronousCall,
) {
if (!completer.isCompleted) {
completer.complete(info);
if (mounted) {
final setupCallback = () {
imageSize = Size(
info.image.width.toDouble(),
info.image.height.toDouble(),
);
_loading = false;
};
synchronousCall ? setupCallback() : setState(setupCallback);
}
}
});
stream.addListener(listener);
completer.future.then((_) {
stream.removeListener(listener);
});
return completer.future;
}
Inside my FlatmapState the followin position maps the absolute point to draw an element to the current view port:
class FlatMapState {
final Offset viewPortOffsetToCenter;
final double imageScale;
final Size initialViewPort;
final Size imageSize;
final Offset viewPortDelta;
final Offset currentViewPort;
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;
}
}
As I build some things are around I try to make it as easy as possible, the drawing part is done like this:
Widget _buildBody(BuildContext context) {
if (_loading) {
return Center(
child: CircularProgressIndicator(),
);
} else {
return Container(
child: Center(
child: CustomPaint(
foregroundPainter: FlatMapPainter(
drawableElements: hideMarker ? null : elementsToDraw,
flatMapState: _currentState,
),
child: PhotoView(
key: stickyKey,
imageProvider: imageProvider,
tightMode: true,
controller: _controller,
onTapUp: (buildContext, tap, value) {
if (mapTapped != null) {
mapTapped(FlatMapTappedEvent(
context: buildContext,
flatMapState: _currentState,
photoViewControllerValue: value,
tapUpDetails: tap,
tappedMarker: elementsToDraw
.where((drawable) => drawable.touched(
_currentState?.viewPortToAbsolutionPosition(
tap.localPosition),
_currentState))
.toList()));
}
},
),
),
),
);
}
}
The painter works like this:
class FlatMapPainter extends CustomPainter {
final List<FlatMapDrawable> drawableElements;
final FlatMapState flatMapState;
FlatMapPainter(
{@required this.flatMapState, @required this.drawableElements});
@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,
));
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true; //TODO evaluate what is the right way here
}
}
I'm currently very limited with free time sorry for not providing a complete project but I hope it helps somebody.
Sorry guys, I've been really, really busy those days preparing stuff for flutter europe. I see your question and I have an ideia to solve this problem with custom child, once it is ready I post it here.
@SergeShkurko I had a look at your solution and got some issues giving it a try. I had to change a few things like the findChild method (unable to find where this was comming from). Also I didn't found the origin of:
Could you maybe provide a little insight on this?
@renancaraujo Did you had any time / luck to get something in that direction ?
@Dev-Owl – are you trying to keep the overlayed widget at the same size relative to the screen when scaling occurs, or to scale in size proportional to the PhotoView child? I'm trying to get the widget to remain the same size relative to the screen, as Google Maps markers do when scaling occurs, but haven't had any luck – I have managed to keep overlayed widgets in the same position and have them scale proportionally.
final size = Size(someWidth, someHeight);
PhotoView.customChild(
child: CustomWidget(size: size),
childSize: size,
)
class CustomWidget extends StatelessWidget {
CustomWidget({this.size});
final Size size;
@override
Widget build(BuildContext context) {
return OverflowBox(
maxWidth: size.width,
maxHeight: size.height,
child: Stack(
children: <Widget>[
Image.asset(
"images/some-image.png",
width: size.width,
height: size.height,
),
Positioned(
top: size.height / 3,
left: size.width / 3,
child: Container(
color: Colors.pink,
width: 20,
height: 20,
),
),
],
),
);
}
}
@Dev-Owl Sorry for the delay.
Managed to work on this tonight and I identified some pretty nasty issues withing our tap callbacks.
It is really difficult to get a tap position regarding the image. So I had to wrap the image on another gesture detector wrapping the image only one custom child.
The code can be found here: https://github.com/renancaraujo/photo_view/blob/de9f12f42961f5154da316036715a66a972da1b9/example/lib/screens/examples/ontap_callbacks.dart (you can use something else to display the dots)
This gif shows the result:
We shall change the internal gesture detector to avoid this nasty workaround.
Hi Guys,
Sorry busy over here, @scopendo actually my code would like to do the same as @renancaraujo beside that its not so smooth and doesn't support rotation. Big thanks to @renancaraujo I will definitely have a look at your code, for me the last thing I couldn't get to work was recognizing a tap on my drawn element/marker. It would be great if this could be build into the tool, I think it would open up some nice use cases. My case by the way is using it as a simple map (to display a layout of a vessel with all decks/rooms etc. ) to put hotspots on it and let user interact with it. For the web solution we use Leaflet unfortunately the leaflet implementation for Flutter has some issues with "simple" coordinates (plain pixel) and did not work for this use case.
Again, thanks for the response and effort from your side :)
Looks like this got solved. I've created the related issues for the tap detection location. I shall close this issue for now. If you wanna retake the discussion, feel free to open it again.
Sorry, so many things pilled up the last weeks I just had the time to check this today. Your solution works. Just wondering how I get the absolute position of the tap (regarding the image)? Let's say you want to exchange the information with another solution (or get the data from 3rd party) I would get a list of positions in absolute points on the image, somehow this needs to be fitted to and from the view. Still looking at the code and trying to wrap my head around, any help is welcome
I found a way to do it, just in case somebody else needs this. The below code recalculates the position from/to absolute position based on the example provided from @renancaraujo (with a small change to get the max width). Please note that the below code is the complete view, just take the parts you need, yes its kind of messy as it's my development try-out project:
class _ComponentLocationState extends State<ComponentLocation> {
File _loadedFile;
PhotoViewControllerBase controller;
double currentScale = 1;
final double pinSize = 50;
final GlobalKey _centerKey = GlobalKey();
double maxWidth = 0;
Size imageSize;
@override
void initState() {
super.initState();
controller = PhotoViewController()
..outputStateStream.listen(onControllerState);
}
void onControllerState(PhotoViewControllerValue value) {
setState(() {
currentScale = value.scale;
});
}
Future<File> getLayoutPicture() async {
if (_loadedFile != null) return _loadedFile;
var dirToSave = await getApplicationDocumentsDirectory();
var localFileName =
File(dirToSave.path + "/layout_${globals.currentWorkingVessel.id}.jpg");
if (!await localFileName.exists()) {
return null;
} else {
painting.imageCache.evict(new FileImage(localFileName));
painting.imageCache.evict(localFileName.path);
_loadedFile = localFileName;
var info = await _getImage();
imageSize =
Size(info.image.width.toDouble(), info.image.height.toDouble());
return localFileName;
}
}
Future<ImageInfo> _getImage() {
final Completer completer = Completer<ImageInfo>();
final ImageStream stream = Image.file(_loadedFile).image.resolve(
const ImageConfiguration(),
);
final listener = ImageStreamListener((
ImageInfo info,
bool synchronousCall,
) {
if (!completer.isCompleted) {
completer.complete(info);
if (mounted) {
final setupCallback = () {
imageSize = Size(
info.image.width.toDouble(),
info.image.height.toDouble(),
);
};
synchronousCall ? setupCallback() : setState(setupCallback);
}
}
});
stream.addListener(listener);
completer.future.then((_) {
stream.removeListener(listener);
});
return completer.future;
}
Widget pointToWidget(double x, double y) {
if (x == null || y == null) return null;
var currentSize =
(currentScale > 1 ? pinSize / currentScale : 25.0).roundToDouble();
var scaledX = x / (imageSize.width / maxWidth);
var scaledY = y / (imageSize.width / maxWidth);
return Positioned(
top: (scaledY - (currentSize / 1.5)).roundToDouble(),
left: (scaledX - (currentSize / 2)).roundToDouble(),
child: Icon(
Icons.location_on,
color: Colors.blue,
size: currentSize,
));
}
void onTapUp(TapUpDetails details) {
print(details.localPosition);
widget.currentComponent.locationX = details.localPosition.dx * (imageSize.width / maxWidth);
widget.currentComponent.locationY =details.localPosition.dy * (imageSize.width / maxWidth);
print("X:${details.localPosition.dx * (imageSize.width / maxWidth)}");
print("Y:${details.localPosition.dy * (imageSize.width / maxWidth)}");
widget.currentComponent.saveToDb().then((_) {
setState(() {});
});
}
Widget _buildInner(BuildContext context) {
if (_loadedFile == null) {
return FutureBuilder<File>(
future: getLayoutPicture(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == null) {
return Center(child: Text("No layout picture found"));
}
return _buildGADrawing(snapshot.data);
} else {
return CircularProgressIndicator();
}
});
} else {
return _buildGADrawing(_loadedFile);
}
}
Widget _buildGADrawing(File file) {
var layoutChilds = <Widget>[
Image.file(file),
];
return Container(
child: PhotoView.customChild(
enableRotation: false,
initialScale: 1.0,
controller: controller,
onTapUp: (context, tapUp, controllerVlaue) {
print(tapUp);
},
child: LayoutBuilder(builder: (buildContext, boxConstraints) {
maxWidth = boxConstraints.maxWidth;
var currentPoistion = pointToWidget(widget.currentComponent.locationX,
widget.currentComponent.locationY);
if (currentPoistion != null) {
layoutChilds.add(currentPoistion);
}
return Center(
key: _centerKey,
child: GestureDetector(
onTapUp: onTapUp,
child: Stack(
children: layoutChilds,
)),
);
}),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Edit component location"),
),
body: _buildInner(context),
);
}
}
Hi,
I have one problem I simply don't get a solution for, using the onTap handler I can draw a circle at a position and move it according to the changes provided from PhotoViewController to keep it on the same spot in the image. I store the local position, the offset and scale when clicked, I draw the circle using position + forPoint.
Calculating the move for the point is done in the listen method of the controller stream:
My issue is as soon as I change the scale the point moves, I spend more or less two days and a lot of paper but I can't get it right. Could somebody help me out and explain how to apply the scaling to keep the point at the right place in the image? Thanks