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';
9library;
10
11import 'dart:math' as math;
12
13import 'package:flutter/foundation.dart';
14import 'package:flutter/rendering.dart';
15
16import 'basic.dart';
17import 'focus_scope.dart';
18import 'framework.dart';
19import '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/// intrinsic 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.
54class 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
98enum _Slot { minExtent, maxExtent, child }
99
100class _SliverResizingHeader extends SlottedMultiChildRenderObjectWidget<_Slot, RenderBox> {
101 const _SliverResizingHeader({
102 this.minExtentPrototype,
103 this.maxExtentPrototype,
104 required this.child,
105 });
106
107 final Widget? minExtentPrototype;
108 final Widget? maxExtentPrototype;
109 final Widget child;
110
111 @override
112 Iterable<_Slot> get slots => _Slot.values;
113
114 @override
115 Widget? childForSlot(_Slot slot) {
116 return switch (slot) {
117 _Slot.minExtent => minExtentPrototype,
118 _Slot.maxExtent => maxExtentPrototype,
119 _Slot.child => child,
120 };
121 }
122
123 @override
124 _RenderSliverResizingHeader createRenderObject(BuildContext context) {
125 return _RenderSliverResizingHeader();
126 }
127}
128
129class _RenderSliverResizingHeader extends RenderSliver
130 with SlottedContainerRenderObjectMixin<_Slot, RenderBox>, RenderSliverHelpers {
131 RenderBox? get minExtentPrototype => childForSlot(_Slot.minExtent);
132 RenderBox? get maxExtentPrototype => childForSlot(_Slot.maxExtent);
133 RenderBox? get child => childForSlot(_Slot.child);
134
135 @override
136 Iterable<RenderBox> get children => <RenderBox>[
137 if (minExtentPrototype != null) minExtentPrototype!,
138 if (maxExtentPrototype != null) maxExtentPrototype!,
139 if (child != null) child!,
140 ];
141
142 double boxExtent(RenderBox box) {
143 assert(box.hasSize);
144 return switch (constraints.axis) {
145 Axis.vertical => box.size.height,
146 Axis.horizontal => box.size.width,
147 };
148 }
149
150 double get childExtent => child == null ? 0 : boxExtent(child!);
151
152 @override
153 void setupParentData(RenderObject child) {
154 if (child.parentData is! SliverPhysicalParentData) {
155 child.parentData = SliverPhysicalParentData();
156 }
157 }
158
159 @protected
160 void setChildParentData(
161 RenderObject child,
162 SliverConstraints constraints,
163 SliverGeometry geometry,
164 ) {
165 final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData;
166 final AxisDirection direction = applyGrowthDirectionToAxisDirection(
167 constraints.axisDirection,
168 constraints.growthDirection,
169 );
170 childParentData.paintOffset = switch (direction) {
171 AxisDirection.up => Offset(
172 0.0,
173 -(geometry.scrollExtent - (geometry.paintExtent + constraints.scrollOffset)),
174 ),
175 AxisDirection.right => Offset(-constraints.scrollOffset, 0.0),
176 AxisDirection.down => Offset(0.0, -constraints.scrollOffset),
177 AxisDirection.left => Offset(
178 -(geometry.scrollExtent - (geometry.paintExtent + constraints.scrollOffset)),
179 0.0,
180 ),
181 };
182 }
183
184 @override
185 double childMainAxisPosition(covariant RenderObject child) => 0;
186
187 @override
188 void performLayout() {
189 final SliverConstraints constraints = this.constraints;
190 final BoxConstraints prototypeBoxConstraints = constraints.asBoxConstraints();
191
192 double minExtent = 0;
193 if (minExtentPrototype != null) {
194 minExtentPrototype!.layout(prototypeBoxConstraints, parentUsesSize: true);
195 minExtent = boxExtent(minExtentPrototype!);
196 }
197
198 late final double maxExtent;
199 if (maxExtentPrototype != null) {
200 maxExtentPrototype!.layout(prototypeBoxConstraints, parentUsesSize: true);
201 maxExtent = boxExtent(maxExtentPrototype!);
202 } else {
203 final Size childSize = child!.getDryLayout(prototypeBoxConstraints);
204 maxExtent = switch (constraints.axis) {
205 Axis.vertical => childSize.height,
206 Axis.horizontal => childSize.width,
207 };
208 }
209
210 final double scrollOffset = constraints.scrollOffset;
211 final double shrinkOffset = math.min(scrollOffset, maxExtent);
212 final BoxConstraints boxConstraints = constraints.asBoxConstraints(
213 minExtent: minExtent,
214 maxExtent: math.max(minExtent, maxExtent - shrinkOffset),
215 );
216 child?.layout(boxConstraints, parentUsesSize: true);
217
218 final double remainingPaintExtent = constraints.remainingPaintExtent;
219 final double layoutExtent = math.min(childExtent, maxExtent - scrollOffset);
220 geometry = SliverGeometry(
221 scrollExtent: maxExtent,
222 paintOrigin: constraints.overlap,
223 paintExtent: math.min(childExtent, remainingPaintExtent),
224 layoutExtent: clampDouble(layoutExtent, 0, remainingPaintExtent),
225 maxPaintExtent: childExtent,
226 maxScrollObstructionExtent: minExtent,
227 cacheExtent: calculateCacheOffset(constraints, from: 0.0, to: childExtent),
228 hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity.
229 );
230 }
231
232 @override
233 void applyPaintTransform(RenderObject child, Matrix4 transform) {
234 final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData;
235 childParentData.applyPaintTransform(transform);
236 }
237
238 @override
239 void paint(PaintingContext context, Offset offset) {
240 if (child != null && geometry!.visible) {
241 final SliverPhysicalParentData childParentData =
242 child!.parentData! as SliverPhysicalParentData;
243 context.paintChild(child!, offset + childParentData.paintOffset);
244 }
245 }
246
247 @override
248 bool hitTestChildren(
249 SliverHitTestResult result, {
250 required double mainAxisPosition,
251 required double crossAxisPosition,
252 }) {
253 assert(geometry!.hitTestExtent > 0.0);
254 if (child != null) {
255 return hitTestBoxChild(
256 BoxHitTestResult.wrap(result),
257 child!,
258 mainAxisPosition: mainAxisPosition,
259 crossAxisPosition: crossAxisPosition,
260 );
261 }
262 return false;
263 }
264}
265