Below you can find an example of a PageView that adapts its height to currently displayed child. With animation:

The idea is:
- We wrap each child of PageView with a custom widget that can report widget size after each frame. Here I called it
SizeReportingWidget. - Each height is saved in a list, called
_heightson an index that corresponds to the child’s index. - Attach a PageController that will let you determine the currently displayed page’s index. Here we save it to a field called
_currentPage - Wrap
PageViewin aSizedBoxand give it the height of the currently displayed child. I’ve added a convenient getter called_currentHeightto retrieve the height. - (Optional) Wrap the
SizedBoxinTweenAnimationBuilder, 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
State<ExpandablePageView> createState() => _ExpandablePageViewState();
}
class _ExpandablePageViewState extends State<ExpandablePageView>
with TickerProviderStateMixin {
late PageController _pageController;
late 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() ?? 0;
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),
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
State<SizeReportingWidget> createState() => _SizeReportingWidgetState();
}
class _SizeReportingWidgetState extends State<SizeReportingWidget> {
Size? _oldSize;
@override
Widget build(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) => _notifySize());
return widget.child;
}
void _notifySize() {
if (!mounted) {
return;
}
final size = context.size;
if (_oldSize != size && size != null) {
_oldSize = size;
widget.onSizeChange(size);
}
}
}