I stumbled upon a layout issue that would be rather obvious in native Android, but I cannot make it work in Flutter.
Let's say I have a layout like this:
Scaffold with AppBar + BottomNavBar + Column + Text
And now I want to fill the remaining white space with 2 Widgets (Containers in that case, but could be anything), to make it look like this (stretch and take all the remaining space): Column with remaining space filled
I tried placing Flex inside the Column and wrapping Containers in Flexible/Expanded with flex. But even after wrapping the Column in SizedBox I still get the constraints error:
The following assertion was thrown during performLayout():
RenderFlex children have non-zero flex but incoming height constraints are unbounded.
Any idea how to achieve that? My code for this screen looks like this currently:
class MyHomePage extends StatelessWidget {
MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Playground")),
bottomNavigationBar: Container(
width: double.infinity,
height: 70,
color: Colors.blueGrey,
),
body: SizedBox(
width: double.infinity,
height: MediaQuery.of(context).size.height,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.only(top: 20, left: 20),
child: Text("I am some text"),
),
Padding(
padding: const EdgeInsets.only(top: 20, left: 20),
child: Text("I am some text too"),
),
FlexibleContent(),
],
),
),
);
}
}
class FlexibleContent extends StatelessWidget {
#override
Widget build(BuildContext context) {
return SizedBox(
child: Flex(
direction: Axis.vertical,
children: [
Expanded(flex: 7, child: Container(width: double.infinity, color: Colors.lightGreen)),
Expanded(flex: 3, child: Container(width: double.infinity, color: Colors.amberAccent)),
],
),
);
}
}
your column has an ubounded amount of height, so you have wrap it's child in a expanded/flexible widget. You can have a column with 3 children, 2 of the children that need to be the exact same size could be in a row, and it's children could be wrapped in a expanded/flexible widget.
Here's what I did:
import 'package:flutter/material.dart';
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Playground")),
bottomNavigationBar: Container(
width: double.infinity,
height: 70,
color: Colors.blueGrey,
),
body: Container(
color: Colors.red,
width: double.infinity,
height: MediaQuery.of(context).size.height,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Padding(
padding: EdgeInsets.only(top: 20, left: 20),
child: Text("I am some text"),
),
Padding(
padding: EdgeInsets.only(top: 20, left: 20),
child: Text("I am some text too"),
),
Flexible( // sorcery
child: FlexibleContent(),
),
],
),
),
);
}
}
class FlexibleContent extends StatelessWidget {
const FlexibleContent({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(
child: Container(
width: double.infinity,
color: Colors.lightGreen,
),
),
Expanded(
child: Container(
width: double.infinity,
color: Colors.amberAccent,
),
),
],
);
}
}
If you add anything in the Container it should automatically expand to accommodate the contents. The Flexible widget well, makes the other Column of widgets, flexible.
Related
Please look at this image Home Page
Now when I scroll the ListView it becomes like this -
Home Page
Now I know the reason why this is happening, it is because I used ListView as a parent to this entire view and added ListView.builder() and other widgets as its child.
What I want is to scroll the ListView.builder() without scrolling the entire page.
For this I first tried to use Column as parent but that ended up giving the overflow pixels error.
And then I set the physics: const NeverScrollableScrollPhysics() inside the parent ListView but after that it made my ListView.builder() to show all of its list items.
Here is my Code for Home Screen
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Expanded(
child: ListView(
padding: const EdgeInsets.only(top: 45, bottom: 24),
children: [
header(),
const SizedBox(height: 36),
const BalanceCard(),
const SizedBox(height: 36),
Recent()
],
),
),
Align(
alignment: Alignment.bottomCenter,
child: bottomNavigationBar(),
),
],
),
); }
Recent List Code
class RecentItems extends StatefulWidget {
final List<Transaction> transactions;
RecentItems({required this.transactions});
#override
State<RecentItems> createState() => _RecentItemsState();
}
class _RecentItemsState extends State<RecentItems> {
#override
Widget build(BuildContext context) {
return SizedBox(
height: 450,
child: Expanded(child: ListView.builder(
itemBuilder: (context, index) {
final item = widget.transactions[index].toString();
return Dismissible(
direction: DismissDirection.endToStart,
key: UniqueKey(),
onDismissed: (direction) {
setState(() {
widget.transactions.removeAt(index);
});
// Then show a snackbar.
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(content: Text('Transaction Deleted')));
},
background: Container(
color: Colors.red,
alignment: AlignmentDirectional.centerEnd,
child: const Padding(
padding: EdgeInsets.fromLTRB(0.0, 0.0, 15.0, 0.0),
child: Icon(
EvaIcons.trash2,
color: Colors.white,
),
),
),
child: Card(
elevation: 5,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20.0),
),
child: ListTile(
leading: CircleAvatar(
radius: 30,
foregroundImage: widget.transactions[index].Image,
backgroundColor: primaryColor,
),
title: Text(
widget.transactions[index].title,
style: const TextStyle(color: secondaryColor),
),
subtitle: Text(
DateFormat.yMMMd().format(widget.transactions[index].date),
),
trailing: Text(
'\$${widget.transactions[index].amount}',
style: const TextStyle(color: secondaryColor),
),
),
),
);
},
itemCount: widget.transactions.length,
),)
);
}
}
Recent Widget -
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Recent Transactions',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: secondaryColor),
),
const SizedBox(height: 5),
RecentItems(transactions: _userTransactions),
],
),
)
The entire screen scrolls because Recent() is included in the same ListView as header() and balanceCard().
Try something like this:
Scaffold(
body: Column(
children: [
Expanded(
child: ListView(
padding: const EdgeInsets.only(top: 45, bottom: 24),
children: [
header(),
const SizedBox(height: 36),
const BalanceCard(),
const SizedBox(height: 36),
],
),
),
// Recent items removed from ListView
Recent(),
Align(
alignment: Alignment.bottomCenter,
child: bottomNavigationBar(),
),
],
),
)
I hope this helps.
By simplifying your code, this is an example of a layout where you have a single Column in the Scaffold. The Column contains some sized, unsized and aligned children.
One child, Recent is a ListView, without explicit height, but wrapped into an Expanded widget. This way it will occupy all the remaining area left by the other children, and it will be scrollable.
(You will run into trouble with this if the children without Recent occupy all the available area.)
Please have a look at this code, you can copy-paste it into a DartPad fiddle:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: MyWidget(),
),
),
);
}
}
class MyWidget extends StatelessWidget {
#override
Widget build(BuildContext context) => Scaffold(
body: Column(
children: [
const Text('header()'),
const SizedBox(height: 36),
const Text('BalanceCard()'),
const SizedBox(height: 36),
Expanded(child: Recent()),
const Align(
alignment: Alignment.bottomCenter,
child: Text('bottomNavigationBar()'),
),
],
),
);
}
class Recent extends StatelessWidget {
#override
Widget build(BuildContext context) => ListView.builder(
itemCount: 100, itemBuilder: (context, index) => Text('Item $index'));
}
I want to make my custom-widget respond to touch, hence I used GestureDetector's onTap method. It's working but it is also changing the dimensions of my custom widget, which I don't want. I am posting some code and screen-shots to make the situation more clear.
What I want is this, just a MALE card (my custom-widget called ReusableCard).
After wrapping my ReusableCard widget in a GestureDetector, what I get is this.
I am sharing my code here. In this code I haven't used GestureDetector yet.
class _InputPageState extends State<InputPage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Center(
child: Text('BMI Calculator')
),
),
body: Column(
children: [
Expanded(
child: Row(
children: [
ReusableCard(activeCardColor,
IconContents(FontAwesomeIcons.mars, 'MALE')),
ReusableCard(activeCardColor,
IconContents(FontAwesomeIcons.venus, 'FEMALE')),
],
),
),
Expanded(
child: Row(
children: [
ReusableCard(activeCardColor),
],
),
),
Expanded(
child: Row(
children: [
ReusableCard(activeCardColor),
ReusableCard(activeCardColor),
],
),
),
Container(
color: bottomContainerColor,
width: double.infinity,
margin: EdgeInsets.only(top: 10),
height: bottomContainerHeight,
),
],
),
);
}
}
The real meaty part of the code is in Blockquotes. Meaty Part after adding GestureDetector to MALE ReusableCard.
GestureDetector(
child: ReusableCard(activeCardColor,
IconContents(FontAwesomeIcons.mars, 'MALE')),
),
ReusableCard(activeCardColor,
IconContents(FontAwesomeIcons.venus, 'FEMALE')),
I don't think if it's helpful but I am also sharing my ReusableCard code with you guys.
class ReusableCard extends StatelessWidget {
ReusableCard(this.colour, [this.cardChild]);
final Color colour;
final Widget cardChild;
#override
Widget build(BuildContext context) {
return Expanded(
child: Container(
child: cardChild,
margin: EdgeInsets.all(15),
decoration: BoxDecoration(
color: colour,
borderRadius: BorderRadius.circular(10),
),
),
);
}
}
I have checked the documentation and I seem to be doing everything correctly. What I can infer from this is that I am messing up somewhere with the Expanded widget, but cannot correct it. Any help appreciated folks.
I would suggest adding the GestureDetector inside your ReusableCard, as a child of your Expanded.
class ReusableCard extends StatelessWidget {
ReusableCard(
this.colour, {
required this.cardChild,
required this.onTap,
});
final Color colour;
final Widget cardChild;
final VoidCallback onTap;
#override
Widget build(BuildContext context) {
return Expanded(
child: GestureDetector(
onTap: onTap,
child: Container(
child: cardChild,
margin: EdgeInsets.all(15),
decoration: BoxDecoration(
color: colour,
borderRadius: BorderRadius.circular(10),
),
)),
);
}
}
This way you won't need to wrap your ReusableCard with a GestureDetector breaking the Expanded widget.
I have a text element which should be aligned to the left but for some reason it appears centered and I can't figure out why.
What code should be changed / added to make the result string aligned to the right?
return Positioned(
bottom: 0,
right: 0,
left: 0,
child: Align(
alignment: Alignment.bottomCenter,
child: Container(
margin: EdgeInsets.fromLTRB(10, 0, 10, 30),
padding: EdgeInsets.all(10),
child: Column(
children: <Widget>[
//First row here. The one below is the second row:
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Container(
padding: EdgeInsets.only(left: 5.0),
child: Icon(Icons.comment),
),
Expanded(
child: Column(children: [
Padding(
padding: EdgeInsets.only(
top: 10.0, left: 5.0, right: 0),
child: Text(
result, //This text is centered. Why?
maxLines: 7,
overflow: TextOverflow.ellipsis,
)),
]),
)
],
),
],
)
)
)
);
It's because row or column alignment.
By default column is centering children, to change that update:
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
...
If this does not solve your problem check:
Under which circumstances textAlign property works in Flutter?
More info on column/row alignment:
https://medium.com/jlouage/flutter-row-column-cheat-sheet-78c38d242041
You can use direction for this purpose.
For example:
MaterialApp(
builder: (context, child) {
return Directionality(
textDirection: TextDirection.rtl,
child: child,
);
},
);
In accordance to https://api.flutter.dev/flutter/widgets/Column-class.html on Column-Class, the Column widget does not scroll (and in general it is considered an error to have more children in a Column than will fit in the available room). If you have a line of widgets and want them to be able to scroll if there is insufficient room, consider using a ListView. For a horizontal variant, see Row.
Also, if you only have one child, then consider using Align or Center to position the child.
For your case, you could add the property crossAxisAlignment to your column as following
... Column(crossAxisAlignment: CrossAxisAlignment.end, ...
as mentioned by #nuts so the text will be align right. Here is a full example also from the above reference under https://api.flutter.dev/flutter/widgets/Expanded-class.html, that i just modified to illustrate how to do it:
/// Flutter code sample for Expanded
// This example shows how to use an [Expanded] widget in a [Column] so that
// its middle child, a [Container] here, expands to fill the space.
//
// ![This results in two thin blue boxes with a larger amber box in between.](https://flutter.github.io/assets-for-api-docs/assets/widgets/expanded_column.png)
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
/// This is the main application widget.
class MyApp extends StatelessWidget {
const MyApp({
Key ? key
}): super(key: key);
static
const String _title = 'Flutter Code Sample';
#override
Widget build(BuildContext context) {
return const MaterialApp(
title: _title,
home: MyStatelessWidget(),
);
}
}
/// This is the stateless widget that the main application instantiates.
class MyStatelessWidget extends StatelessWidget {
const MyStatelessWidget({
Key ? key
}): super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Expanded Column Sample'),
),
body: Center(
child: Column(
children: < Widget > [
Container(
color: Colors.blue,
height: 100,
width: 100,
),
Expanded(
child: Container(
color: Colors.amber,
width: 100,
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: < Widget > [
Text('We moved this text to the right')
],
),
Container(
color: Colors.blue,
height: 100,
width: 100,
),
],
),
),
);
}
}
Hope, this will help!
I've been trying so hard to build page view system for quotes app. I want the page to flow full screen from top to bottom and bottom to top scrollable/swipe able to navigate between different quotes each time
like this. The scroll will bring new page each time its not casual scroll. I haven't found any guide regarding this on internet so far.
I don't know about how to build it, nothing is popping in my mind for days now. I've tried pageview with gesture detector for swiping up and down, it doesn't works as desired and appbar is static too and the bottom containers as well I don't want this. What I want is the page/screen to flow under the appbar or even a button on top right corner and under 2 buttons on the bottom.
Create a column of Containers where each Container's height and width equals the height and width of the screen. You can do that by using:
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
Also, make sure to wrap your code with a SingleChildScrollView widget to scroll vertically.
Here's the code:
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: SingleChildScrollView(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Center(child: Text('Quote number one')),
Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(icon: Icon(Icons.share), onPressed: () {}),
IconButton(
icon: Icon(Icons.favorite), onPressed: () {})
],
),
)
],
),
),
Container(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: Column(
children: [
Text('Quote number two'),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(icon: Icon(Icons.share), onPressed: () {}),
IconButton(icon: Icon(Icons.favorite), onPressed: () {})
],
)
],
),
)
],
),
),
),
);
}
}
You can use PageView to get this type of view
Drive Video link
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
PageController pageController;
PageView pageView;
List<String> quotes = [
'#Quote 1\n\nāI\'m selfish, impatient and a little insecure. I make mistakes, I am out of control and at times hard to handle. But if you can\'t handle me at my worst, then you sure as hell don\'t deserve me at my best.ā ',
'#Qoute 2\n\nSo many books, so little time.',
'#Quote 3\n\n A room without books is like a body without a soul'
];
#override
void initState() {
super.initState();
pageController = PageController();
final _quotes = List.generate(quotes.length, (index) => quoteWidget(index));
pageView = PageView(
children: _quotes,
controller: pageController,
scrollDirection: Axis.vertical,
);
}
Widget quoteWidget(int index) {
return Scaffold(
backgroundColor: Colors.black,
body: Column(
children: [
Expanded(
flex: 3,
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Center(
child: Text(
quotes[index],
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w300,
color: Colors.white,
),
),
),
),
),
Expanded(
flex: 1,
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
FloatingActionButton(
onPressed: () {},
child: Icon(Icons.share),
),
const SizedBox(width: 20),
FloatingActionButton(
onPressed: () {},
child: Icon(Icons.favorite_border),
),
],
),
),
Expanded(child: Container())
],
),
);
}
#override
void dispose() {
pageController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
brightness: Brightness.dark,
title: Text(widget.title),
),
body: SafeArea(
child: Stack(
alignment: Alignment.bottomCenter,
children: [
pageView,
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
child: Row(
children: [
RaisedButton.icon(
icon: Icon(Icons.menu_book),
label: Text('General'),
onPressed: () {},
),
const Spacer(),
Container(
width: 50,
height: 50,
margin: const EdgeInsets.symmetric(horizontal: 10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(5),
),
child: Icon(Icons.color_lens),
),
Container(
width: 50,
height: 50,
margin: const EdgeInsets.symmetric(horizontal: 10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(5),
),
child: Icon(Icons.person),
),
],
),
)
],
),
),
);
}
}
My base widget is a Column. The first element is a Container which has a BoxShadow. The second element is a ListView which builds several Card, depending on the context. If the scroll is 0 the shadow gets displayed. However when start scrolling, the card is "over" the shadow (higher z-index) and hides it.
unscrolled
scrolled
The shadow should stay always on top, over the Cards. How is this done?
EDIT
if you don't want container shadow to disappear on scroll remove the ScrollNotification and NotificationListener
There is a widget for that š
You can use ScrollNotification with NotificationListener. Try this;
Happy coding! š¤
class TestPage extends StatefulWidget {
const TestPage({Key key}) : super(key: key);
#override
_TestPageState createState() => _TestPageState();
}
class _TestPageState extends State<TestPage> {
double blurRadius = 10.0;
double spreadRadius = 1.0;
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.blueGrey,
title: Text('Title'),
),
body: Container(
width: Get.width,
height: Get.height,
child: Stack(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0).copyWith(
top: 62.0,
),
child: NotificationListener<ScrollNotification>(
// ignore: missing_return
onNotification: (scrollNotification) {
if (scrollNotification is ScrollStartNotification) {
setState(() {
blurRadius = 0.0;
spreadRadius = 0.0;
});
} else if (scrollNotification is ScrollEndNotification) {
setState(() {
blurRadius = 10.0;
spreadRadius = 1.0;
});
}
},
child: ListView.builder(
// controller: _controller,
itemCount: 10,
itemBuilder: (context, index) {
return Card(
child: Container(
width: Get.width * .8,
height: 100.0,
child: Center(
child: Text('child # index : $index'),
)),
);
},
),
),
),
Positioned(
top: 0,
left: 0,
right: 0,
child: Container(
width: Get.width,
height: 60.0,
margin: EdgeInsets.only(),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(20.0),
bottomRight: Radius.circular(20.0),
),
boxShadow: [
BoxShadow(
color: Colors.grey,
blurRadius: blurRadius,
spreadRadius: spreadRadius,
),
],
),
child: Center(
child: Text('TOP CONTAINER'),
),
),
),
],
),
),
);
}
}
According to your question, here is the concept of what is going wrong here.
The 1st child of Column is a Container with shadow. Shadow renders outside of the defined size. If you don't provide any spaces after this Container we won't be able to see the shadow. This space can be done by Container margin , SizedBox or wrapping our list with Padding. But now our main question is how we get shadow while index=0. I believe it is coming from ListChildren`. They contain upper spaces, that's why we can see only the 1st time.
On Ui render priority starts from bottom to Top.
How to solve this problem:
We can provide space at the bottom of our container or assign margin(not padding), or use a SizedBox after the container providing the same amount of height as shadow.
providing bottom margin on the container.
adding a SizedBox with height of shadow.
wrapping our list with Padding and providing top:.
In this image,
our shadow: white, background:amber.
Demo code:
import 'package:flutter/material.dart';
class ColumnWithContainerShadow extends StatelessWidget {
const ColumnWithContainerShadow({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.amber,
body: Column(
children: [
Container(
height: 50,
width: double.infinity,
////* we dont need margin if we have padding on ListView
// margin: EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: Colors.green,
boxShadow: [
BoxShadow(
offset: Offset(0, 12),
color: Colors.white,
)
],
),
child: Center(child: Text("Container A")),
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(top: 12),
child: ListView(
children: [
...List.generate(
333,
(index) => Container(
/// enable this margine and remove other spaces to see 1st child shadow.(shadow depend on children position)
// margin: EdgeInsets.only(top: 12),
height: 60,
color: Colors.deepPurple,
child: Text("$index"),
),
)
],
),
),
),
Container(
height: 50,
width: double.infinity,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.green,
boxShadow: [
BoxShadow(
offset: Offset(0, 12),
color: Colors.white,
)
],
),
child: Text("Bottom Container"),
),
// Comment to close shadow
SizedBox(
height: 20,
)
],
),
);
}
}
Wrap this upper box in Material widget and provide an elevation.