I'm developing a Flutter app where I need to overlay icons on specific points on an SVG image. These icons represent locations which are normalized to (x,y) coordinates between 0 and 100 and must align accurately with what is on the image irrespective of screen size or the available space of the InteractiveViewer
. However, I'm facing the issue where the icons misalign when the available space changes. I am trying it out on my phone and an emulator and the points are offset slightly, but clearly.
Below is a minimal example illustrating the problem. The goal is for the icon to stay aligned with a specific point on the SVG (at a normalized position of x = 50 = y) regardless of the available space for the InteractiveViewer
.
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: CustomInteractiveMap(),
),
),
);
}
}
class CustomInteractiveMap extends StatefulWidget {
@override
_CustomInteractiveMapState createState() => _CustomInteractiveMapState();
}
class _CustomInteractiveMapState extends State<CustomInteractiveMap> {
@override
Widget build(BuildContext context) {
return Expanded(
child: LayoutBuilder(
builder: (context, constraints) => InteractiveViewer(
minScale: 1,
maxScale: 5,
child: Stack(
children: [
SvgPicture.asset('assets/images/background.svg', fit: BoxFit.contain),
Positioned(
left: (50 / 100) * constraints.maxWidth, // exemplary x coordinate of 50 (out of [0, 100])
top: (50 / 100) * constraints.maxHeight, // exemplary y coordinate of 50 (out of [0, 100])
child: const Icon(Icons.location_pin, color: Colors.redAccent),
),
],
),
),
),
);
}
}
I have tried to wrap the LayoutBuilder in an AspectRatio like so: return Expanded(child: AspectRatio(aspectRatio: 1,child: LayoutBuilder(...)
to make sure that the ratio of the available space has no impact. On first glance this improved the situation, but has not resolved the offset entirely.
Edit: I have also tried to use MediaQuery.of(context).size
instead of using the LayoutBuilder
, but this does not solve the situation either.
With Ivo's help, the minimal example is working! But I am still struggling to implement the same functionality for my actual app. In my app, the Icons in the emulator are offset towards the top. The x coordinate seems to work properly.
The following function creates the Icons that are then later stacked on top of the main SVG image.
List<Widget> buildWidgets(Size size) {
List<Widget> widgets = list.map((item) {
final coordinates = maps[item.id]![item.location];
if (coordinates != null) {
final xPos = (coordinates.dx / 100) * size.width;
final yPos = (coordinates.dy / 100) * size.height;
return Positioned(
left: xPos - (iconSize / 2),
top: yPos - (iconSize / 2),
child: FractionalTranslation(
translation: const Offset(-0.5, -0.5),
child: SvgPicture.asset(
'assets/images/icons/${item.type.toLowerCase()}.svg',
width: iconSize,
height: iconSize,
colorFilter: const ColorFilter.mode(Colors.red, BlendMode.srcIn),
),
),
);
} else {
return Container();
}
}).toList();
return widgets;
}
The rest of the code should be the same:
return Expanded(
child: AspectRatio(
aspectRatio: 1,
child: LayoutBuilder(
builder: (context, constraints) {
double width = constraints.maxWidth;
double height = constraints.maxHeight;
iconSize = width * 0.05;
return InteractiveViewer(
onInteractionEnd: (details) => setState(() {}),
minScale: 1,
maxScale: 5,
child: Stack(
children: [
SvgPicture.asset('assets/images/background.svg', fit: BoxFit.contain),
...buildWidgets(Size(width, height))
],
),
);
}
),
),
);
Is there anything that I am missing, that I am doing differently? I am correcting for the Icon Size as suggested by Ivo...
Thank you in advance!
I'm not entirely sure if this is the problem but you can try it out. I believe the main problem is that the values you give to left
and top
in the positioned correspond to the top left corner of the icons. But ideally you would want the coordinates for the center of the icons because the centers might be different for different screens even with the same top-left coordinate. What might solve this is to translate the icons in such a way that their centers are now where their top-left corner used to be. Something like
child: Stack(
children: [
SvgPicture.asset('assets/images/background.svg', fit: BoxFit.contain),
Positioned(
left: (50 / 100) * constraints.maxWidth, // exemplary x coordinate of 50 (out of [0, 100])
top: (50 / 100) * constraints.maxHeight, // exemplary y coordinate of 50 (out of [0, 100])
child: FractionalTranslation(
translation: const Offset(-0.5, -0.5),
child: const Icon(Icons.location_pin, color: Colors.redAccent),
),
),
],
),
You probably will need to figure out the correct coordinates again for all the icons, but I think that when you then get it right it will be right for all screen sizes. I haven't tried it myself, so it's a bit of a speculative answer, but it's worth a try.
EDIT: given that it's a Icons.location_pin
which has the point down in the center you might want a const Offset(-0.5, -1)
instead