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/// 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.
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 {
99 minExtent,
100 maxExtent,
101 child,
102}
103
104class _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
133class _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