| 1 | import 'dart:ui'; |
| 2 | |
| 3 | import 'package:flutter/material.dart'; |
| 4 | import 'package:flutter/rendering.dart'; |
| 5 | |
| 6 | const _firstLayoutMaxHeight = 10000.0; |
| 7 | |
| 8 | class PageViewHeightAdaptable extends StatefulWidget { |
| 9 | const PageViewHeightAdaptable({ |
| 10 | super.key, |
| 11 | required this.controller, |
| 12 | required this.children, |
| 13 | }) : assert(children.length > 0, 'children must not be empty'); |
| 14 | |
| 15 | final PageController controller; |
| 16 | final List<Widget> children; |
| 17 | |
| 18 | @override |
| 19 | State<PageViewHeightAdaptable> createState() => _PageViewHeightAdaptableState(); |
| 20 | } |
| 21 | |
| 22 | class _PageViewHeightAdaptableState extends State<PageViewHeightAdaptable> { |
| 23 | final _sizes = <int, Size>{}; |
| 24 | |
| 25 | @override |
| 26 | void didUpdateWidget(PageViewHeightAdaptable oldWidget) { |
| 27 | super.didUpdateWidget(oldWidget); |
| 28 | |
| 29 | _sizes.clear(); |
| 30 | } |
| 31 | |
| 32 | @override |
| 33 | Widget build(BuildContext context) { |
| 34 | return ListenableBuilder( |
| 35 | listenable: widget.controller, |
| 36 | builder: (context, child) => _SizingContainer( |
| 37 | sizes: _sizes, |
| 38 | page: widget.controller.hasClients ? widget.controller.page ?? 0 : 0, |
| 39 | child: child!, |
| 40 | ), |
| 41 | child: LayoutBuilder( |
| 42 | builder: (context, constraints) => PageView( |
| 43 | controller: widget.controller, |
| 44 | children: [ |
| 45 | for (final (i, child) in widget.children.indexed) |
| 46 | Stack( |
| 47 | alignment: Alignment.topCenter, |
| 48 | clipBehavior: Clip.hardEdge, |
| 49 | children: [ |
| 50 | SizedBox.fromSize(size: _sizes[i]), |
| 51 | Positioned( |
| 52 | left: 0, |
| 53 | top: 0, |
| 54 | right: 0, |
| 55 | child: _SizeAware( |
| 56 | child: child, |
| 57 | // don't setState, we'll use it in the layout phase |
| 58 | onSizeLaidOut: (size) { |
| 59 | _sizes[i] = size; |
| 60 | }, |
| 61 | ), |
| 62 | ), |
| 63 | ], |
| 64 | ), |
| 65 | ], |
| 66 | ), |
| 67 | ), |
| 68 | ); |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | typedef _OnSizeLaidOutCallback = void Function(Size); |
| 73 | |
| 74 | class _SizingContainer extends SingleChildRenderObjectWidget { |
| 75 | const _SizingContainer({ |
| 76 | super.child, |
| 77 | required this.sizes, |
| 78 | required this.page, |
| 79 | }); |
| 80 | |
| 81 | final Map<int, Size> sizes; |
| 82 | final double page; |
| 83 | |
| 84 | @override |
| 85 | _RenderSizingContainer createRenderObject(BuildContext context) { |
| 86 | return _RenderSizingContainer( |
| 87 | sizes: sizes, |
| 88 | page: page, |
| 89 | ); |
| 90 | } |
| 91 | |
| 92 | @override |
| 93 | void updateRenderObject( |
| 94 | BuildContext context, |
| 95 | _RenderSizingContainer renderObject, |
| 96 | ) { |
| 97 | renderObject |
| 98 | ..sizes = sizes |
| 99 | ..page = page; |
| 100 | } |
| 101 | } |
| 102 | |
| 103 | class _RenderSizingContainer extends RenderProxyBox { |
| 104 | _RenderSizingContainer({ |
| 105 | RenderBox? child, |
| 106 | required Map<int, Size> sizes, |
| 107 | required double page, |
| 108 | }) : _sizes = sizes, |
| 109 | _page = page, |
| 110 | super(child); |
| 111 | |
| 112 | Map<int, Size> _sizes; |
| 113 | Map<int, Size> get sizes => _sizes; |
| 114 | set sizes(Map<int, Size> value) { |
| 115 | if (_sizes == value) return; |
| 116 | _sizes = value; |
| 117 | markNeedsLayout(); |
| 118 | } |
| 119 | |
| 120 | double _page; |
| 121 | double get page => _page; |
| 122 | set page(double value) { |
| 123 | if (_page == value) return; |
| 124 | _page = value; |
| 125 | markNeedsLayout(); |
| 126 | } |
| 127 | |
| 128 | @override |
| 129 | void performLayout() { |
| 130 | if (child case final child?) { |
| 131 | child.layout( |
| 132 | constraints.copyWith( |
| 133 | minWidth: constraints.maxWidth, |
| 134 | minHeight: 0, |
| 135 | maxHeight: constraints.hasBoundedHeight ? null : _firstLayoutMaxHeight, |
| 136 | ), |
| 137 | parentUsesSize: true, |
| 138 | ); |
| 139 | |
| 140 | final int floorPage = page.floor(); |
| 141 | final int ceilPage = page.ceil(); |
| 142 | |
| 143 | Size? a = sizes[floorPage]; |
| 144 | Size? b = sizes[ceilPage]; |
| 145 | |
| 146 | if (a == null && sizes.isNotEmpty) { |
| 147 | a = sizes.values.first; |
| 148 | } |
| 149 | if (b == null && sizes.isNotEmpty) { |
| 150 | b = sizes.values.first; |
| 151 | } |
| 152 | |
| 153 | a ??= child.getDryLayout(constraints); |
| 154 | b ??= a; |
| 155 | |
| 156 | final double t = (page - floorPage).clamp(0.0, 1.0); |
| 157 | final double height = lerpDouble(a.height, b.height, t) ?? a.height; |
| 158 | |
| 159 | child.layout( |
| 160 | constraints.copyWith(minHeight: height, maxHeight: height), |
| 161 | parentUsesSize: true, |
| 162 | ); |
| 163 | size = child.size; |
| 164 | } else { |
| 165 | size = computeSizeForNoChild(constraints); |
| 166 | } |
| 167 | } |
| 168 | } |
| 169 | |
| 170 | class _SizeAware extends SingleChildRenderObjectWidget { |
| 171 | const _SizeAware({ |
| 172 | required Widget child, |
| 173 | required this.onSizeLaidOut, |
| 174 | }) : super(child: child); |
| 175 | |
| 176 | final _OnSizeLaidOutCallback onSizeLaidOut; |
| 177 | |
| 178 | @override |
| 179 | _RenderSizeAware createRenderObject(BuildContext context) { |
| 180 | return _RenderSizeAware( |
| 181 | onSizeLaidOut: onSizeLaidOut, |
| 182 | ); |
| 183 | } |
| 184 | |
| 185 | @override |
| 186 | void updateRenderObject(BuildContext context, _RenderSizeAware renderObject) { |
| 187 | renderObject.onSizeLaidOut = onSizeLaidOut; |
| 188 | } |
| 189 | } |
| 190 | |
| 191 | class _RenderSizeAware extends RenderProxyBox { |
| 192 | _RenderSizeAware({ |
| 193 | RenderBox? child, |
| 194 | required _OnSizeLaidOutCallback onSizeLaidOut, |
| 195 | }) : _onSizeLaidOut = onSizeLaidOut, |
| 196 | super(child); |
| 197 | |
| 198 | _OnSizeLaidOutCallback? _onSizeLaidOut; |
| 199 | _OnSizeLaidOutCallback get onSizeLaidOut => _onSizeLaidOut!; |
| 200 | set onSizeLaidOut(_OnSizeLaidOutCallback value) { |
| 201 | if (_onSizeLaidOut == value) return; |
| 202 | _onSizeLaidOut = value; |
| 203 | markNeedsLayout(); |
| 204 | } |
| 205 | |
| 206 | @override |
| 207 | void performLayout() { |
| 208 | super.performLayout(); |
| 209 | |
| 210 | onSizeLaidOut( |
| 211 | getDryLayout( |
| 212 | constraints.copyWith(maxHeight: double.infinity), |
| 213 | ), |
| 214 | ); |
| 215 | } |
| 216 | } |