I'm trying to create a horizontal scrolling list of items in Flutter, and I want that list to only take up the necessary height based on its children. By design "ListView tries to expand to fit the space available in its cross-direction" (from the Flutter docs), which I also notice in that it takes up the whole height of the viewport, but is there a way to make it not do this? Ideally something similar to this (which obviously doesn't work):
new ListView(
scrollDirection: Axis.horizontal,
crossAxisSize: CrossAxisSize.min,
children: <Widget>[
new ListItem(),
new ListItem(),
// ...
],
);
I realize that one way to do this is by wrapping the ListView in a Container with a fixed height. However, I don't necessarily know the height of the items:
new Container(
height: 97.0,
child: new ListView(
scrollDirection: Axis.horizontal,
children: <Widget>[
new ListItem(),
new ListItem(),
// ...
],
),
);
I was able to hack together a "solution" by nesting a Row in a SingleChildScrollView in a Column with a mainAxisSize: MainAxisSize.min. However, this doesn't feel like a solution, to me:
new Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
new SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: new Row(
children: <Widget>[
new ListItem(),
new ListItem(),
// ...
],
),
),
],
);
12 Answers 12
Just set shrink property of ListView to true and it will fit the space rather than expanding.
Example:
ListView(
shrinkWrap: true, //just set this property
padding: const EdgeInsets.all(8.0),
children: listItems.toList(),
),
5 Comments
shrinkWrap will shrink the ListView along the main-axis. Not along the cross-axis as asked.CustomScrollViewAs far as I understand, you can't have a horizontal ListView inside a vertical ListView and have its height dynamically set. If your requirements allow (no infinite scrolling, small amount of elements, etc), you could use a SingleChildScrollView instead.
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [...],
),
);
4 Comments
Use ConstrainedBox to set minHeight and maxHeight
ConstrainedBox(
constraints: new BoxConstraints(
minHeight: 35.0,
maxHeight: 160.0,
),
child: new ListView(
shrinkWrap: true,
children: <Widget>[
new ListItem(),
new ListItem(),
],
),
)
4 Comments
Use a SingleChildScrollView with scrollDirection: Axis.horizontal and a Row inside.
Big advantages:
It doesn't matter how many of widgets you need.
You don't need to know the heights of the widgets.
Widget _horizontalWrappedRow(List data) { var list = <Widget>[SizedBox(width: 16)]; // 16 is start padding //create a new row widget for each data element data.forEach((element) { list.add(MyRowItemWidget(element)); }); // add the list of widgets to the Row as children return SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: list, ), );}
Disadvantage:
- Items not built lazily, so maybe avoid it for long lists.
2 Comments
I figured out a way to do dynamic sizing for a horizontally scrolling ListView. The list can have infinite items, but each item should have the same height.
(If you only have a few items, or if your items must have different heights, see this answer instead.)
Key point:
The idea is to measure one item, let's call it a "prototype". And based on the assumption that all items are the same height, we then set the height of the ListView to match the prototype item.
This way, we can avoid having to hardcode any values, and the list can automatically resize when, for example, the user sets a larger font in the system and caused the cards to expand.
Demo:
a demo gif of infinite items scrolling
Code:
I made a custom widget, called PrototypeHeight (name inspired from IntrinsicHeight). To use it, simply pass in a Widget as a "prototype", then pass in a horizontally scrolling ListView, for example:
PrototypeHeight(
prototype: MyItem(),
listView: ListView.builder(
scrollDirection: Axis.horizontal,
itemBuilder: (_, index) => MyItem(),
),
)
And the implementation of PrototypeHeight is as follows (you can just paste it somewhere and start using it, or modify it as you need):
class PrototypeHeight extends StatelessWidget {
final Widget prototype;
final ListView listView;
const PrototypeHeight({
Key? key,
required this.prototype,
required this.listView,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Stack(
children: [
IgnorePointer(
child: Opacity(
opacity: 0.0,
child: prototype,
),
),
SizedBox(width: double.infinity),
Positioned.fill(child: listView),
],
);
}
}
Explaination:
Let me briefly explain how this works.
Simply put, the size of a Stack widget, depends on all of its "non-positioned" children. Basically, if a Stack has 3 children, but 2 of which are Positioned, then the Stack will simply match the remaining "non-positioned" child.
In this case, I'm using the "prototype item" as one of the non-positioned children, to help the Stack decide its height (which will be the same height as the prototype). Then I'm using a very wide SizedBox to help the Stack decide its width (which will be the same width as the parent's width, typically the device's screen width, because remember: constraints go down, size go up. ). I'm also adding an Opacity widget to hide the prototype (this also increases performance by skipping parts of the rendering pipeline), and an IgnorePointer widget to prevent user interactions, in case there are buttons on the prototype item.
Next, I'm using a Positioned.fill to make its child (the ListView) match the size of the Stack, which in turn, would match the height of the prototype item, and match the width of the parent. This will be the constraint passed to the ListView, so the ListView will create a "viewport" of that size, which is exactly what we wanted to achieve.
This actually covers quite a few concepts in Flutter. I plan to make a video tutorial in a bit.
6 Comments
prototypeItem that applies to the crossAxis. The one we have only applies to the main axis. This would make everything much easier.PrototypeConstraints widget. I think I am going to make a package that implements something like this so we don't have to rely on Stack.This is very similar to a question asked here: Flutter ListView.builder() widget's cross Axis is taking up the entire Screen height
I believe ListViews require every item to have the same Cross Axis size. That means in this case, we are unable to set a unique height for every object, the cross axis size is fixed.
If you want to have the height of a scrollable uniquely controlled by the child itself, then you can use a SingleChildScrollView paired with a Row (note: make sure to set the scrollDirection: Axis.horizontal) output
import 'package:flutter/material.dart';
final Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
debugShowCheckedModeBanner: false,
home: Scaffold(
body: SafeArea(
child: Container(
// height: 100, // no need to specify here
color: Colors.white,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: <Widget>[
Container(
height: 300,
width: 300,
color: Colors.amber[600],
child: const Center(child: Text('Entry A')),
),
Container(
height: 100,
color: Colors.amber[500],
child: const Center(child: Text('Entry B')),
),
Container(
height: 50,
width: 100,
color: Colors.amber[100],
child: const Center(child: Text('Entry C')),
),
Container(
height: 300,
width: 300,
color: Colors.amber[600],
child: const Center(child: Text('Entry A')),
),
Container(
height: 100,
color: Colors.amber[500],
child: const Center(child: Text('Entry B')),
),
Container(
height: 50,
width: 100,
color: Colors.amber[100],
child: const Center(child: Text('Entry C')),
),
Container(
height: 300,
width: 300,
color: Colors.amber[600],
child: const Center(child: Text('Entry A')),
),
Container(
height: 100,
color: Colors.amber[500],
child: const Center(child: Text('Entry B')),
),
Container(
height: 50,
width: 100,
color: Colors.amber[100],
child: const Center(child: Text('Entry C')),
),
Container(
height: 300,
width: 300,
color: Colors.amber[600],
child: const Center(child: Text('Entry A')),
),
Container(
height: 100,
color: Colors.amber[500],
child: const Center(child: Text('Entry B')),
),
Container(
height: 50,
width: 100,
color: Colors.amber[100],
child: const Center(child: Text('Entry C')),
),
Container(
height: 300,
width: 300,
color: Colors.amber[600],
child: const Center(child: Text('Entry A')),
),
Container(
height: 100,
color: Colors.amber[500],
child: const Center(child: Text('Entry B')),
),
Container(
height: 50,
width: 100,
color: Colors.amber[100],
child: const Center(child: Text('Entry C')),
),
],
),
)),
),
),
);
}
}
1 Comment
(This answer covers a workaround when items have different sizes. If your items have the same size/height, see this answer for a ListView approach.)
If the items have different sizes, it should not be done with a ListView, and for a good reason:
A ListView could potentially have a lot of items (or even unlimited), but figuring out "the biggest one" requires going through every item, which is against the whole point of using a ListView (dynamically load each item using the builder method).
On the other hand, a Row widget always lay out all children at once, so it's easy to find the biggest one. So if you don't have too many items (perhaps less than 100 items or so), you can use Row instead. If you need scrolling, wrap Row with a SingleChildScrollView.
Code used in the example:
Column(
children: [
Container(
color: Colors.green.shade100,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
FlutterLogo(),
FlutterLogo(size: 100),
for (int i = 0; i < 50; i++) FlutterLogo(),
],
),
),
),
const Text('The row has ended.'),
],
)
2 Comments
ListView was designed in a such way that prohibits the way OP wants it to use. So ListView should always go with defined height. And shrinkwrap won't help because the Flutter team itself advocates against it, as it causes performance issues. So the easiest and best solution is to set the height.Wrapping the widget by Column with mainAxisSize: MainAxisSize.min worked for me. You can try to change the height here.
var data = [
{'name': 'Shopping', 'icon': Icons.local_shipping},
{'name': 'Service', 'icon': Icons.room_service},
{'name': 'Hotel', 'icon': Icons.hotel},
{'name': 'More', 'icon': Icons.more}
];
new Container(
constraints: new BoxConstraints(
minHeight: 40.0,
maxHeight: 60.0,
),
color: Colors.transparent,
child: new ListView(
scrollDirection: Axis.horizontal,
children: data
.map<Widget>((e) => Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
new Container(
width: 40,
height: 50, // try to change this.
color: Colors.transparent,
margin: EdgeInsets.only(right: 20, left: 4),
child: ClipOval(
child: Container(
padding: EdgeInsets.all(4),
color: Colors.white,
child: Icon(
e["icon"],
color: Colors.grey,
size: 30,
),
),
),
),
],
))
.toList(),
));
Comments
I'm using a horizontal listview builder of container so to stop the Container for taking the full height i just wrapped it with Center and it worked perfectly.
itemBuilder: (context, i){
return Center(child: word_on_line(titles[i]));
}
1 Comment
By applying all the above solution there is no suitable answer found yet, which help us to set horizontal Listview If we don't know the height. we must have to set height in all the cases. so I applied the below solution which works for me without specifying any height for the list items. which @sindrenm has already mentioned in his question. so I would like to go with it till a feasible solution will found. I applied shrinkWrap: true but it will shrink the ListView along the main-axis. (only for vertical scrollView) and Not along the cross-axis as asked in the question. so in the nearest future, anyone can go with this solution in the production as well, it works great for me for my all horizontal lists.
//below declared somewhere in a class above any method and assume list has filled from some API.
var mylist = List<myModel>();
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
for (int index = 0; index < mylist.length; index++)
listItem(mylist[index]);
],
),
),
),
listItem(myModelObj){
return Column(
children[
widget1(),
widget2().... etc..
]);
}
Comments
Use Expanded
Expanded(
child:ListView.separated(
shrinkWrap: true,
padding: EdgeInsets.all(10),
separatorBuilder: (BuildContext context, int index) {
return Align(
alignment: Alignment.centerRight,
child: Container(
height: 0.5,
width: MediaQuery.of(context).size.width / 1.3,
child: Divider(),
),
);
},
itemCount: dataMasterClass.length,
itemBuilder: (BuildContext context, int index) {
Datum datum = dataMasterClass[index];
return sendItem(datum);
},)
) ,
Comments
If you do not want the listview to force your widget to fit in the cross axis make suure that they are placed in a Column() or Row() widget instead of placing it directly in a listview()
new ListView(
scrollDirection: Axis.horizontal,
crossAxisSize: CrossAxisSize.min,
children: <Widget>[
Column(
children:[
new ListItem(),
new ListItem(),
],
)
],
);
ListViewhave the height based on the height of the children? Is theSingleChildScrollViewbasedListViewthe only option? I would really like to use aListViewas it's more semantically correct.