1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | /// @docImport 'pinned_header_sliver.dart'; |
6 | /// @docImport 'scroll_view.dart'; |
7 | /// @docImport 'sliver_floating_header.dart'; |
8 | /// @docImport 'sliver_persistent_header.dart'; |
9 | library; |
10 | |
11 | import 'dart:math' as math; |
12 | |
13 | import 'package:flutter/foundation.dart'; |
14 | import 'package:flutter/rendering.dart'; |
15 | |
16 | import 'basic.dart'; |
17 | import 'focus_scope.dart'; |
18 | import 'framework.dart'; |
19 | import 'slotted_render_object_widget.dart'; |
20 | |
21 | /// A sliver that is pinned to the start of its [CustomScrollView] and |
22 | /// reacts to scrolling by resizing between the intrinsic sizes of its |
23 | /// min and max extent prototypes. |
24 | /// |
25 | /// The minimum and maximum sizes of this sliver are defined by [minExtentPrototype] |
26 | /// and [maxExtentPrototype], a pair of widgets that are laid out once. You can |
27 | /// use [SizedBox] widgets to define the size limits. |
28 | /// |
29 | /// If the [minExtentPrototype] is null, then the default minimum extent is 0. If |
30 | /// [maxExtentPrototype] is null then the default maximum extent is based on the child's |
31 | /// intrisic size. |
32 | /// |
33 | /// This sliver is preferable to the general purpose [SliverPersistentHeader] |
34 | /// for its relatively narrow use case because there's no need to create a |
35 | /// [SliverPersistentHeaderDelegate] or to predict the header's minimum or |
36 | /// maximum size. |
37 | /// |
38 | /// {@tool dartpad} |
39 | /// This sample shows how this sliver's two extent prototype properties can be used to |
40 | /// create a resizing header whose minimum and maximum sizes match small and large |
41 | /// configurations of the same header widget. |
42 | /// |
43 | /// ** See code in examples/api/lib/widgets/sliver/sliver_resizing_header.0.dart ** |
44 | /// {@end-tool} |
45 | /// |
46 | /// See also: |
47 | /// |
48 | /// * [PinnedHeaderSliver] - which just pins the header at the top |
49 | /// of the [CustomScrollView]. |
50 | /// * [SliverFloatingHeader] - which animates the header in and out of view |
51 | /// in response to downward and upwards scrolls. |
52 | /// * [SliverPersistentHeader] - a general purpose header that can be |
53 | /// configured as a pinned, resizing, or floating header. |
54 | class SliverResizingHeader extends StatelessWidget { |
55 | /// Create a pinned header sliver that reacts to scrolling by resizing between |
56 | /// the intrinsic sizes of the min and max extent prototypes. |
57 | const SliverResizingHeader({ |
58 | super.key, |
59 | this.minExtentPrototype, |
60 | this.maxExtentPrototype, |
61 | this.child, |
62 | }); |
63 | |
64 | /// Laid out once to define the minimum size of this sliver along the |
65 | /// [CustomScrollView.scrollDirection] axis. |
66 | /// |
67 | /// If null, the minimum size of the sliver is 0. |
68 | /// |
69 | /// This widget is never made visible. |
70 | final Widget? minExtentPrototype; |
71 | |
72 | /// Laid out once to define the maximum size of this sliver along the |
73 | /// [CustomScrollView.scrollDirection] axis. |
74 | /// |
75 | /// If null, the maximum extent of the sliver is based on the child's |
76 | /// intrinsic size. |
77 | /// |
78 | /// This widget is never made visible. |
79 | final Widget? maxExtentPrototype; |
80 | |
81 | /// The widget contained by this sliver. |
82 | final Widget? child; |
83 | |
84 | Widget? _excludeFocus(Widget? extentPrototype) { |
85 | return extentPrototype != null ? ExcludeFocus(child: extentPrototype) : null; |
86 | } |
87 | |
88 | @override |
89 | Widget build(BuildContext context) { |
90 | return _SliverResizingHeader( |
91 | minExtentPrototype: _excludeFocus(minExtentPrototype), |
92 | maxExtentPrototype: _excludeFocus(maxExtentPrototype), |
93 | child: child ?? const SizedBox.shrink(), |
94 | ); |
95 | } |
96 | } |
97 | |
98 | enum _Slot { |
99 | minExtent, |
100 | maxExtent, |
101 | child, |
102 | } |
103 | |
104 | class _SliverResizingHeader extends SlottedMultiChildRenderObjectWidget<_Slot, RenderBox> { |
105 | const _SliverResizingHeader({ |
106 | this.minExtentPrototype, |
107 | this.maxExtentPrototype, |
108 | required this.child, |
109 | }); |
110 | |
111 | final Widget? minExtentPrototype; |
112 | final Widget? maxExtentPrototype; |
113 | final Widget child; |
114 | |
115 | @override |
116 | Iterable<_Slot> get slots => _Slot.values; |
117 | |
118 | @override |
119 | Widget? childForSlot(_Slot slot) { |
120 | return switch (slot) { |
121 | _Slot.minExtent => minExtentPrototype, |
122 | _Slot.maxExtent => maxExtentPrototype, |
123 | _Slot.child => child, |
124 | }; |
125 | } |
126 | |
127 | @override |
128 | _RenderSliverResizingHeader createRenderObject(BuildContext context) { |
129 | return _RenderSliverResizingHeader(); |
130 | } |
131 | } |
132 | |
133 | class _RenderSliverResizingHeader extends RenderSliver with SlottedContainerRenderObjectMixin<_Slot, RenderBox>, RenderSliverHelpers { |
134 | RenderBox? get minExtentPrototype => childForSlot(_Slot.minExtent); |
135 | RenderBox? get maxExtentPrototype => childForSlot(_Slot.maxExtent); |
136 | RenderBox? get child => childForSlot(_Slot.child); |
137 | |
138 | @override |
139 | Iterable<RenderBox> get children { |
140 | return <RenderBox>[ |
141 | if (minExtentPrototype != null) minExtentPrototype!, |
142 | if (maxExtentPrototype != null) maxExtentPrototype!, |
143 | if (child != null) child!, |
144 | ]; |
145 | } |
146 | |
147 | double boxExtent(RenderBox box) { |
148 | assert(box.hasSize); |
149 | return switch (constraints.axis) { |
150 | Axis.vertical => box.size.height, |
151 | Axis.horizontal => box.size.width, |
152 | }; |
153 | } |
154 | |
155 | double get childExtent => child == null ? 0 : boxExtent(child!); |
156 | |
157 | @override |
158 | void setupParentData(RenderObject child) { |
159 | if (child.parentData is! SliverPhysicalParentData) { |
160 | child.parentData = SliverPhysicalParentData(); |
161 | } |
162 | } |
163 | |
164 | @protected |
165 | void setChildParentData(RenderObject child, SliverConstraints constraints, SliverGeometry geometry) { |
166 | final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData; |
167 | final AxisDirection direction = applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection); |
168 | childParentData.paintOffset = switch (direction) { |
169 | AxisDirection.up => Offset(0.0, -(geometry.scrollExtent - (geometry.paintExtent + constraints.scrollOffset))), |
170 | AxisDirection.right => Offset(-constraints.scrollOffset, 0.0), |
171 | AxisDirection.down => Offset(0.0, -constraints.scrollOffset), |
172 | AxisDirection.left => Offset(-(geometry.scrollExtent - (geometry.paintExtent + constraints.scrollOffset)), 0.0), |
173 | }; |
174 | } |
175 | |
176 | @override |
177 | double childMainAxisPosition(covariant RenderObject child) => 0; |
178 | |
179 | @override |
180 | void performLayout() { |
181 | final SliverConstraints constraints = this.constraints; |
182 | final BoxConstraints prototypeBoxConstraints = constraints.asBoxConstraints(); |
183 | |
184 | double minExtent = 0; |
185 | if (minExtentPrototype != null) { |
186 | minExtentPrototype!.layout(prototypeBoxConstraints, parentUsesSize: true); |
187 | minExtent = boxExtent(minExtentPrototype!); |
188 | } |
189 | |
190 | late final double maxExtent; |
191 | if (maxExtentPrototype != null) { |
192 | maxExtentPrototype!.layout(prototypeBoxConstraints, parentUsesSize: true); |
193 | maxExtent = boxExtent(maxExtentPrototype!); |
194 | } else { |
195 | final Size childSize = child!.getDryLayout(prototypeBoxConstraints); |
196 | maxExtent = switch (constraints.axis) { |
197 | Axis.vertical => childSize.height, |
198 | Axis.horizontal => childSize.width, |
199 | }; |
200 | } |
201 | |
202 | final double scrollOffset = constraints.scrollOffset; |
203 | final double shrinkOffset = math.min(scrollOffset, maxExtent); |
204 | final BoxConstraints boxConstraints = constraints.asBoxConstraints( |
205 | minExtent: minExtent, |
206 | maxExtent: math.max(minExtent, maxExtent - shrinkOffset), |
207 | ); |
208 | child?.layout(boxConstraints, parentUsesSize: true); |
209 | |
210 | final double remainingPaintExtent = constraints.remainingPaintExtent; |
211 | final double layoutExtent = math.min(childExtent, maxExtent - scrollOffset); |
212 | geometry = SliverGeometry( |
213 | scrollExtent: maxExtent, |
214 | paintOrigin: constraints.overlap, |
215 | paintExtent: math.min(childExtent, remainingPaintExtent), |
216 | layoutExtent: clampDouble(layoutExtent, 0, remainingPaintExtent), |
217 | maxPaintExtent: childExtent, |
218 | maxScrollObstructionExtent: childExtent, |
219 | cacheExtent: calculateCacheOffset(constraints, from: 0.0, to: childExtent), |
220 | hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity. |
221 | ); |
222 | } |
223 | |
224 | @override |
225 | void applyPaintTransform(RenderObject child, Matrix4 transform) { |
226 | final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData; |
227 | childParentData.applyPaintTransform(transform); |
228 | } |
229 | |
230 | @override |
231 | void paint(PaintingContext context, Offset offset) { |
232 | if (child != null && geometry!.visible) { |
233 | final SliverPhysicalParentData childParentData = child!.parentData! as SliverPhysicalParentData; |
234 | context.paintChild(child!, offset + childParentData.paintOffset); |
235 | } |
236 | } |
237 | |
238 | @override |
239 | bool hitTestChildren(SliverHitTestResult result, { required double mainAxisPosition, required double crossAxisPosition }) { |
240 | assert(geometry!.hitTestExtent > 0.0); |
241 | if (child != null) { |
242 | return hitTestBoxChild(BoxHitTestResult.wrap(result), child!, mainAxisPosition: mainAxisPosition, crossAxisPosition: crossAxisPosition); |
243 | } |
244 | return false; |
245 | } |
246 | } |
247 | |