I'm trying to create a carousel with items of variable heights. When using PageView or ListView with horizontal scrolling, I need to give it a constant height, like this:
class CarouselVariableHightState extends State<CarouselVariableHight> {
double height = 200;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Carousel')),
body: ListView(
children: <Widget>[
Text('My Carousel'),
Container(
height: height,
child: PageView(
children: <Widget>[
_buildCarouselItem(),
_buildCarouselItem(),
_buildCarouselItem(),
_buildCarouselItem(),
],
),
),
Text('end of List')
],
),
);
}
Widget _buildCarouselItem() {
return Column(
children: [
Container(
color: Colors.red,
height: Random().nextInt(200).toDouble(),
child: Text('Text with random length')
)
]
);
}
}
But my items contain text and I don't know the length of it. I don't wan't to limit the height of a CarouselItem because it might cut off the text. How could I have something like a PageView, that adjust its own size based on the currently shown item?
I thought a solution could be to get the height of the CarouselItem and then set it to the height of the PageView. However I couldn't figure out a way to get the height my current Carousel item.
Any Ideas?
You cannot change the height of the elements when PageView is build. So if you need to have for example a server request make it before you render the carusel.
PageView. builder creates a scrollable list that works page by page using widgets that are created on-demand. This constructor is appropriate for page views with a large (or infinite) number of children because the builder is called only for those children that are actually visible.
Below you can find an example of a PageView that adapts its height to currently displayed child. With animation:
The idea is:
SizeReportingWidget
._heights
on an index that corresponds to the child's index._currentPage
PageView
in a SizedBox
and give it the height of the currently displayed child. I've added a convenient getter called _currentHeight
to retrieve the height.SizedBox
in TweenAnimationBuilder
, so that the height changes are smooth and animated rather than clanky jumps.ExpandablePageView
class ExpandablePageView extends StatefulWidget {
final List<Widget> children;
const ExpandablePageView({
Key key,
@required this.children,
}) : super(key: key);
@override
_ExpandablePageViewState createState() => _ExpandablePageViewState();
}
class _ExpandablePageViewState extends State<ExpandablePageView> with TickerProviderStateMixin {
PageController _pageController;
List<double> _heights;
int _currentPage = 0;
double get _currentHeight => _heights[_currentPage];
@override
void initState() {
_heights = widget.children.map((e) => 0.0).toList();
super.initState();
_pageController = PageController() //
..addListener(() {
final _newPage = _pageController.page.round();
if (_currentPage != _newPage) {
setState(() => _currentPage = _newPage);
}
});
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return TweenAnimationBuilder<double>(
curve: Curves.easeInOutCubic,
duration: const Duration(milliseconds: 100),
tween: Tween<double>(begin: _heights[0], end: _currentHeight),
builder: (context, value, child) => SizedBox(height: value, child: child),
child: PageView(
controller: _pageController,
children: _sizeReportingChildren
.asMap() //
.map((index, child) => MapEntry(index, child))
.values
.toList(),
),
);
}
List<Widget> get _sizeReportingChildren => widget.children
.asMap() //
.map(
(index, child) => MapEntry(
index,
OverflowBox(
//needed, so that parent won't impose its constraints on the children, thus skewing the measurement results.
minHeight: 0,
maxHeight: double.infinity,
alignment: Alignment.topCenter,
child: SizeReportingWidget(
onSizeChange: (size) => setState(() => _heights[index] = size?.height ?? 0),
child: Align(child: child),
),
),
),
)
.values
.toList();
}
SizeReportingWidget
class SizeReportingWidget extends StatefulWidget {
final Widget child;
final ValueChanged<Size> onSizeChange;
const SizeReportingWidget({
Key key,
@required this.child,
@required this.onSizeChange,
}) : super(key: key);
@override
_SizeReportingWidgetState createState() => _SizeReportingWidgetState();
}
class _SizeReportingWidgetState extends State<SizeReportingWidget> {
Size _oldSize;
@override
Widget build(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) => _notifySize());
return widget.child;
}
void _notifySize() {
if (!this.mounted) {
return;
}
final size = context?.size;
if (_oldSize != size) {
_oldSize = size;
widget.onSizeChange(size);
}
}
}
There are 2 possible solutions: right one and hacky one.
The Right solution: Because PageView is trying to get all posible height, we can't use it inside of any widget, that dont' have height. So if we don't have height we need to use another widget, for example StackWidget and re-create PageView behavior: swipes, animations etc. It won't be a big library, if you want to make just simple things, less than 200-300 lines I guess.
The hacky solution is to check the height of all elements before you build the PageView widget. You cannot change the height of the elements when PageView is build. So if you need to have for example a server request make it before you render the carusel.
To get the height, it easier to use IndexedStack, becouse it has the height of the highest element, but show only the first one (by default).
class CarouselVariableHight extends StatefulWidget {
@override
CarouselVariableHightState createState() => CarouselVariableHightState();
}
final widgetHeights = <double>[
Random().nextInt(300).toDouble(),
Random().nextInt(300).toDouble(),
Random().nextInt(300).toDouble(),
Random().nextInt(300).toDouble(),
];
class CarouselVariableHightState extends State<CarouselVariableHight> {
double height;
GlobalKey stackKey = GlobalKey();
bool widgetHasHeigh;
@override
void initState() {
WidgetsBinding.instance.addPostFrameCallback(_afterLayout);
super.initState();
}
_afterLayout(_) {
final RenderBox renderBoxRed = stackKey.currentContext.findRenderObject();
final sizeRed = renderBoxRed.size;
setState(() {
height = sizeRed.height;
});
}
final caruselItems = widgetHeights
.map((height) => Column(children: [
Container(
color: Colors.red,
height: Random().nextInt(300).toDouble(),
child: Text('Text with random length'))
]))
.toList();
_buildStack() {
Widget firstElement;
if (height == null) {
firstElement = Container();
} else {
firstElement = Container(
height: height,
child: PageView(
children: caruselItems,
),
);
}
return IndexedStack(
key: stackKey,
children: <Widget>[
firstElement,
...caruselItems,
],
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Carousel')),
body: ListView(
children: <Widget>[
Text('My Carousel'),
_buildStack(),
Text('end of List'),
],
),
);
}
}
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