I'm trying to build a simple Flutter application that displays a full-screen background image and enables the user to drag certain widgets (i.e. a basic circle) from pre-defined start positions (given in pixels) to pre-defined target positions (also given in pixels). The following screenshot from the TouchSurgery app shows a very similar setup to what I'm trying to achieve (green circle = start position, white circle = target position):
My biggest concern at this point are different screen sizes. Let's assume we have an iPhone SE (second generation) with a resolution of 750 x 1334
. I can create the following background image with the desired resolution and randomly determine the desired start position to be at coordinates (430, 949)
(for simplicity we can disregard the target position):
With the following widget, I can render a circular Container
on top of the starting point:
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var dpr = MediaQuery.of(context).devicePixelRatio;
return Scaffold(
body: Stack(
children: <Widget>[
Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage("assets/iPhoneSE.png"),
fit: BoxFit.fill,
),
),
),
Positioned(
left: 430 / dpr,
top: 949 / dpr,
child: Container(
width: 77.0 / dpr,
height: 77.0 / dpr,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.red,
),
),
),
],
),
);
}
}
The resulting image looks like this:
Things start to get tricky when I add an AppBar
or a BottomNavigationBar
to my application. Both Widgets have a default height of 56 pixels. Given a devicePixelRatio
of 2
on the iPhone SE, I need to crop the size of my background image to 750 x 1110
for the overlay to still be accurate (1334 - 2 * 56 (AppBar) - 2 * 56 (BottomNavigationBar)
).
Things get even more complicated for other devices such as the iPhone XR, where also the size of the safe area has to be considered. And for Android, there's even more different screen resolutions available.
My question now is the following: instead of creating differently sized background images for 20-30+ different screen sizes - is there a more efficient way in Flutter to draw widgets such as a circular Container
at very specific screen locations that works independently of the actual screen size?
You need to get the size of the image container BEFORE positioning your Positioned Widget.
Because as you said, the screen size could change, independently of the image size (e.g. The screen is taller but has a bigger SafeArea, or has an appBar and BottomAppBar. The image could be the same size even if the screen size increased...)
Since your Positioned widget and your image Container are in the same build method, you have to use a LayoutBuilder widget to track the size of your image Container before moving on to building your Positioned widget.
Here's how:
(I've included 2 fully working examples so that you can see that the red circle keeps the same relative position to the background image, even when the image size changes. Your corrected code is the first example).
Example 1
/*
I used these calculated ratios based on your code.
Feel free to use any other way to get these ratios.
The method will be the same.
- The image takes all the available width and height
- The positioned element is postioned :
58.9% from the left of the image container
72% from the top of the image container
- The inner container's:
width is 7.129629629% of the Image Container's width,
height is 4.292084726% of the Image Container's height,
*/
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) { //This is key
return Scaffold(
body: Stack(
children: <Widget>[
Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage("assets/iPhoneSE.png"),
fit: BoxFit.fill,
),
),
),
Positioned(
left: 0.589 * constraints.maxWidth,
top: 0.72 * constraints.maxHeight,
child: Container(
width: 0.07129629629 * constraints.maxWidth,
height: 04292084726 * constraints.maxHeight,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.red,
),
),
),
],
),
);
});
}
}
Example 1 image:
Example 2 (with an AppBar and BottomAppBar)
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Title of app"),
),
body: LayoutBuilder(builder: (context, constraints) {
return Column(
children: <Widget>[
Flexible(
fit: FlexFit.loose,
child: Stack(
children: <Widget>[
Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage("assets/iPhoneSE.png"),
fit: BoxFit.fill,
),
),
),
Positioned(
left: 0.589 * constraints.maxWidth,
top: 0.72 * constraints.maxHeight,
child: Container(
width: 0.07129629629 * constraints.maxWidth,
height: 0.04292084726 * constraints.maxHeight,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.red,
),
),
),
],
),
),
],
);
}),
bottomNavigationBar: BottomNavigationBar(
items: [
BottomNavigationBarItem(
icon: Icon(
Icons.home,
),
title: Text("Home")),
BottomNavigationBarItem(
icon: Icon(Icons.account_circle), title: Text("Profile")),
],
),
);
}
}
Example 2 image:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With