Flutter: layout question regarding column child height - flutter

I have a layout where I want to expand a widget (container) to the right, it's the red area in the examples. To the left of the expanded container there is a column with an image at the topleft (blue in examples) and a button in the bottom of the column. I do know the button width, it's a bit over 100 pixels but we can assume that it's 100 pixels if that helps.
The thing is that the blue area (it's a user uploaded image) can vary in size. From e.g. 100x100 to 800x800. It will be square and smaller than 100x100 is not supported. Large images are resized to 800x800.
I want to achieve this:
The column should adapt it's size depending on the image size.
The column should be as wide as needed by the button but otherwise 100 <= width <= 400 depending on image size.
The column should not overflow in any direction.
The red area should be maximized.
The button should always be at the bottom left of the layout.
I cannot know the column height in advance without using a LayoutBuilder and I want to know if this is achievable without calculating the exact height in pixels using a LayoutBuilder.
Here's an example of how I want it too look with a small image (100x100) and that works with the code below the image:
Code that works well for 100x100 image:
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.title)),
body: Center(
child: Container(
height: 400,
width: 800,
child: Row(
children: <Widget>[
_buildColumn(),
Expanded(child: Container(color: Colors.red)),
],
),
),
),
);
}
Widget _buildColumn() {
return Container(
constraints: BoxConstraints(maxWidth: 400),
child: Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_buildImage(),
MaterialButton(
color: Colors.green,
child: Text('Fixed height button'),
onPressed: () {},
)
],
),
),
);
}
Widget _buildImage() {
// This can vary from 100x100 to 800x800 (always square)
var size = 100.0;
return Container(color: Colors.blue, width: size, height: size);
}
If I bump it to an 800x800 image by changing the size variable to 800.0 in _buildImage() I get this layout:
I understand that the column have unconstrained height on it's children so it will render the blue container with 800px height which creates the overflow. I do know that the column height is 400px in this case and that the button is 48px high so I can calculate that the max image size is 352px in height. By wrapping the blue container in a Container with constraints (maxWidth: 352, maxHeight: 352) I achieve the layout that I want:
Widget _buildImage() {
// This can vary from 100x100 to 800x800 (always square)
var size = 800.0;
return Container(
constraints: BoxConstraints(maxWidth: 352, maxHeight: 352),
child: Container(color: Colors.blue, width: size, height: size),
);
}
I can also achieve this using expanded, like this:
Widget _buildImage() {
// This can vary from 100x100 to 800x800 (always square)
var size = 800.0;
return Expanded(
child: Container(color: Colors.blue, width: size, height: size),
);
}
I want to avoid using calculated pixels for height/width so lets continue with the Expanded widget. When I have that expanded widget with a small image, i.e. 100x100 I get this result (which is not what I want):
I need to align my blue square within the expanded widget to prevent it to getting stretched but when I do that (align top left), like this:
Widget _buildImage() {
// This can vary from 100x100 to 800x800 (always square)
var size = 100.0;
return Expanded(
child: Container(
alignment: Alignment.topLeft,
child: Container(color: Colors.blue, width: size, height: size),
),
);
}
Then I get this result:
The container outside of the blue container expands and makes the column unnecessary wide. I want to have the same look as the first image when I have a small image.
How can I achieve this adaptive layout without using a LayoutBuilder and calculating exact image constraints?

Wrap the column with a container and set the height:
MediaQuery.of(context).size.height * 0.8;
it will contain 80% height in screen

Related

Why does a SizedBox in another SizedBox ignore its width and hight?

When I nest two SizedBoxes, the width and height of the inner box are ignored. Why is this, how can I work around it?
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Center(
child: SizedBox(
width: 300,
height: 500,
child: SizedBox(
width: 200, height: 200, child: Container(color: Colors.green)),
));
}
}
In this example, I have a 300x500 sized box and an inner 200x200 SizedBox. In the picture you can see that the green box is the size of the outer SizedBox but should actually be a 200x200 square.
According to flutter documentation: If given a child, this widget forces it to have a specific width and/or height. These values will be ignored if this widget's parent does not permit them. For example, this happens if the parent is the screen (forces the child to be the same size as the parent), or another SizedBox (forces its child to have a specific width and/or height). This can be remedied by wrapping the child SizedBox in a widget that does permit it to be any size up to the size of the parent, such as Center or Align.
So wrapping the child with center would solve the problem:
Center(
child: SizedBox(
width: 300,
height: 500,
child: Center(
child: SizedBox(
width: 200, height: 200, child: Container(color: Colors.green)),
),
)),
The problem is, SizedBox can set widget size only within the constrains set by the parent. Many widgets, like Padding, want their child to occupy 100% of the space available to them. This makes sense, because if the child is smaller they wouldn't know where to put it.
If you want the child to be smaller than the parent you could use Center or Align, e.g. replace
I had the same issue and I solved my problem using the FractionallySizedBox class.
You can specify the suitable size using fractions of the above SizedBox as widthFactor and heightFactor:
Widget build(BuildContext context) {
return SizedBox.expand(
child: FractionallySizedBox(
widthFactor: 0.5,
heightFactor: 0.5,
alignment: FractionalOffset.center,
child: DecoratedBox(
decoration: BoxDecoration(
border: Border.all(
color: Colors.blue,
width: 4),
),
),
),
);
}

Keep Flutter widget at fixed size upon resizing window

I have the following in my widget's build method:
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Title bar'),
),
body: Center(
child: SizedBox(
width: 250,
child: ListView(
children: [
const Text('Text goes here'),
Container(
width: 250,
height: 250,
color: Colors.green,
child: Texture(textureId: textureId)), // ID of a texture that displays pixels
],
),
),
),
floatingActionButton: FloatingActionButton(
child: const Text('Button'),
onPressed: () {
noisy();
},
),
));
What I want to have take place is that the Texture widget is always 250x250 pixels, regardless of what I resize the window to. When I resize the window vertically, this is what happens, and the overflow of the widget that goes past the bottom of the screen is just clipped and ignored. However, when I resize horizontally making the window less wide than the widget, the widget is scaled horizontally, getting compressed in that direction instead of being clipped to the right. What arrangement if widgets would I need in order to keep the Container/Texture at 250x250 pixels and have it clipped past the end of the window instead of being compressed?
I have tried surrounding the inner Container in another horizontal ListView, as the outer ListView appears to clip the window vertically correctly, but that causes RenderBox was not laid out: RenderRepaintBoundary#51231 relayoutBoundary=up5 NEEDS-PAINT.... I also attempted placing that inner ListView in another Container/SizedBox, though this did not fix the shrinking issue and cause it to clip instead.
Try using unconstrainedBox
UnconstrainedBox(
child: SizedBox(
)
)
This allows a child to render at the size it would render if it were alone on an infinite canvas with no constraints. This container will then attempt to adopt the same size, within the limits of its own constraints. If it ends up with a different size, it will align the child based on alignment. If the box cannot expand enough to accommodate the entire child, the child will be clipped.
In debug mode, if the child overflows the container, a warning will be printed on the console, and black and yellow striped areas will appear where the overflow occurs.
simple, use ConstraintBox & set it minHeight and minWidth
ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 100,
minHeight: 100,
maxWidth: 100,
maxHeight: 100,
),
)

Auto-scaling image in Row to match other Widgets

Within my layout, I have a Row. On the left side of that Row, I have a Column containing two Text objects of differing font sizes. On the right side of the Row, I have an Image. I want the Image to scale itself uniformly to have an equal height to the intrinsic height of the Column on the left side of the Row. I want this to work whether the Image's pixel height is lesser or greater than the intrinsic height of the Column, regardless of the font sizes used in the Text widgets and the text scaling factor used by the operating system. (That is, I don't want to hard-code the Image size.)
How can I make this work? I suspect I may need to cloak the Image in some widget to make its parent think it has an intrinsic height of 0 because Images have intrinsic heights equal to their pixel heights.
Here is some broken code I tried in DartPad:
(Note: the network image is the same image used in the documentation # https://flutter.dev/docs/cookbook/images/network-image . I don't have any rights to it.)
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: IntrinsicHeight(
child: Container(
color: Colors.green,
width: 600,
height: 600,
child: Row(crossAxisAlignment: CrossAxisAlignment.end, children: [
Column(children: [
Text("Larger", style: TextStyle(fontSize: 36)),
Text("Smaller", style: TextStyle(fontSize: 18))
]),
Expanded(
child: Image.network(
"https://picsum.photos/250?image=9",
alignment: Alignment.bottomRight,
fit: BoxFit.scaleDown,
))
]),
),
)));
}
}

In Flutter, how can I have an adjusting vertical layout, a mix between column and listview behavior?

As far as I see, Column and ListView both have a very distinct usage when used for a base root layouting.
Column is used when the screen has few components (such as login screen). We can add some Expanded components to adjust white spaces in between, so when the keyboard is visible, the screen shrink to keep everything visible.
ListView is used when the screen has many components that potentially need scrolling. We can't use Expanded component in ListView. When using ListView, appearing keyboard does not change the white spaces, only change the size of outer ListView, while the inner content is wrapped in scroll view.
Now the problem is, how if I want to have screen like this:
When all the contents' combined vertical size is not longer than available height quota given from parent (in this case, screen's height), then the components behave like inside Column: expanding or shrinking to fill available white spaces according to rules set by Expanded.
When all the content's combined vertical size is longer than available height quota, then the components behave like inside ListView: all the possible expanding components will shrink into their minimum size (ignoring Expanded), and the screen is scrollable so user can see the rest of the screen below.
Is this possible to be done in Flutter? How?
EDIT: based on Reign's comment, I have isolated some code from SingleChildScrollView manual, but it looks like it still can't handle if its children contains Expanded.
Widget columnRoot({
MainAxisAlignment mainAxisAlignment = MainAxisAlignment.spaceBetween,
AssetImage backgroundImage,
List<Widget> children
}) =>
LayoutBuilder(builder: (BuildContext context, BoxConstraints viewportConstraints) =>
SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: viewportConstraints.maxHeight,
),
child: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: backgroundImage,
fit: BoxFit.cover),
color: Colors.white
),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: mainAxisAlignment,
children: children
),
)
)
)
);
Widget content(BuildContext context) => columnRoot(children: [
Container(color: Colors.red, height: 100.0),
Expanded(Container(color: Colors.green)), // without this line, there's no layout error
Container(color: Colors.blue, height: 100.0),
]);
Error:
RenderFlex children have non-zero flex but incoming height constraints are unbounded.
I added some code you can test with also with some explanation.
Copy paste and run the code
class MyWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: SingleChildScrollView( //Since setting it to scrollable, your widget Column with expanded children wont work as it supposed to be because it wont know its parent height
//Since its already scrollable `Expanded` will expand or shrink now based on it child widget (Expanded(child: SomeHeight widget)) refer: #10 example
child: IntrinsicHeight( //This will fix the expanded widget error
child: Container(
//Test remove this height
// height: 400, //But when you set its height before its parent scroll widget, `Expanded` will expand based on its available space
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.max,
children: [
Container(color: Colors.red, height: 100.0),
//#10
//Experiment with this
Expanded(
child: Container(
color: Colors.purple,
// height: 100.0, //initialized height, remove parent container height: 400
// child: Text("This is also considered as min height"),
),
),
Container(color: Colors.blue, height: 100.0),
],
),
),
),
),
),
);
}
}

BoxConstraints forces an infinite height

child:Column(
children: <Widget>[
Container(
height: double.infinity,
width: 100.0,
color: Colors.red,
child: Text('hello'),
),)
in this,when i make height:double.infinity,it gives error in run saying **BoxConstraints forces an infinite height.**but when i give height manually it work fine.
can anyone explain me why this happening.
How about this one.
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
children: <Widget>[
Expanded(
child: Container(
// height: double.infinity,
width: 100.0,
color: Colors.red,
child: Text('hello'),
),
),
],
),
),
);
}
}
This means that you can't offer inifite height to the container. It's obvious behaviour if you don't provide the contraints to height.
You have to specify limited height to the container so that flutter can render it, if you offer it infinite it how can flutter render that and up to which constraints it would do that !
Rather you can set double.infinity to width and flutter will successfully render that because by default flutter has constraints for width it will set width to width of screen.
Considering that you have to provide height as that of screen you can use MediaQuery for that
Widget yourMethod(or build)(BuildContext context){
final screenHeight = MediaQuery.of(context).size.height;
return Column(
children:<Widget>[
Container(
height:screenHeight,//but this will be height of whole screen. You need to substract screen default paddings and height of appbar if you have one
width:100.0,
....
)
]);
}
Hope this helps !
Happy coding..
BoxConstraints forces an infinite height
Why This Happens
You're asking to render an infinite height object without a height constraint... Flutter can't do that.
Column lays out children in two phases:
Phase 1: non-Flex items (anything not Expanded, Flexible or Spacer)
done in unconstrained space
Phase 2: Flex items (Expanded,Flexible, Spacer only)
done with remaining space
Phase 1
Column's phase 1 vertical layout is done in unbounded space. That means:
no vertical constraint → no height limit
any widget with infinite height will throw the above error
you can't render an infinite height object in an infinite height constraint... that's goes on forever
Phase 2
after Phase 1 widgets have taken as much space as they intrinsically need, phase 2 Flex items share the remaining/leftover space
the remaining space is calculated from incoming constraints minus Phase 1 widgets dimensions
double.infinity height will expand to use up the remaining space
Infinite Height is OK
Here's an example of using infinite height on a Container inside a Column, which is fine:
class ColumnInfiniteChildPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
children: [
Flexible(
child: Container(
height: double.infinity, // ← perfectly fine
child: Text('Column > Container > Text')),
),
Text('Column > Text')
],
),
),
);
}
}
Remove the Flexible and the error will be thrown.