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 | import 'package:flutter/foundation.dart'; |
6 | import 'package:flutter/rendering.dart'; |
7 | import 'package:flutter/scheduler.dart' show TickerProvider; |
8 | |
9 | import 'framework.dart'; |
10 | import 'scroll_position.dart'; |
11 | import 'scrollable.dart'; |
12 | |
13 | /// Delegate for configuring a [SliverPersistentHeader]. |
14 | abstract class SliverPersistentHeaderDelegate { |
15 | /// Abstract const constructor. This constructor enables subclasses to provide |
16 | /// const constructors so that they can be used in const expressions. |
17 | const SliverPersistentHeaderDelegate(); |
18 | |
19 | /// The widget to place inside the [SliverPersistentHeader]. |
20 | /// |
21 | /// The `context` is the [BuildContext] of the sliver. |
22 | /// |
23 | /// The `shrinkOffset` is a distance from [maxExtent] towards [minExtent] |
24 | /// representing the current amount by which the sliver has been shrunk. When |
25 | /// the `shrinkOffset` is zero, the contents will be rendered with a dimension |
26 | /// of [maxExtent] in the main axis. When `shrinkOffset` equals the difference |
27 | /// between [maxExtent] and [minExtent] (a positive number), the contents will |
28 | /// be rendered with a dimension of [minExtent] in the main axis. The |
29 | /// `shrinkOffset` will always be a positive number in that range. |
30 | /// |
31 | /// The `overlapsContent` argument is true if subsequent slivers (if any) will |
32 | /// be rendered beneath this one, and false if the sliver will not have any |
33 | /// contents below it. Typically this is used to decide whether to draw a |
34 | /// shadow to simulate the sliver being above the contents below it. Typically |
35 | /// this is true when `shrinkOffset` is at its greatest value and false |
36 | /// otherwise, but that is not guaranteed. See [NestedScrollView] for an |
37 | /// example of a case where `overlapsContent`'s value can be unrelated to |
38 | /// `shrinkOffset`. |
39 | Widget build(BuildContext context, double shrinkOffset, bool overlapsContent); |
40 | |
41 | /// The smallest size to allow the header to reach, when it shrinks at the |
42 | /// start of the viewport. |
43 | /// |
44 | /// This must return a value equal to or less than [maxExtent]. |
45 | /// |
46 | /// This value should not change over the lifetime of the delegate. It should |
47 | /// be based entirely on the constructor arguments passed to the delegate. See |
48 | /// [shouldRebuild], which must return true if a new delegate would return a |
49 | /// different value. |
50 | double get minExtent; |
51 | |
52 | /// The size of the header when it is not shrinking at the top of the |
53 | /// viewport. |
54 | /// |
55 | /// This must return a value equal to or greater than [minExtent]. |
56 | /// |
57 | /// This value should not change over the lifetime of the delegate. It should |
58 | /// be based entirely on the constructor arguments passed to the delegate. See |
59 | /// [shouldRebuild], which must return true if a new delegate would return a |
60 | /// different value. |
61 | double get maxExtent; |
62 | |
63 | /// A [TickerProvider] to use when animating the header's size changes. |
64 | /// |
65 | /// Must not be null if the persistent header is a floating header, and |
66 | /// [snapConfiguration] or [showOnScreenConfiguration] is not null. |
67 | TickerProvider? get vsync => null; |
68 | |
69 | /// Specifies how floating headers should animate in and out of view. |
70 | /// |
71 | /// If the value of this property is null, then floating headers will |
72 | /// not animate into place. |
73 | /// |
74 | /// This is only used for floating headers (those with |
75 | /// [SliverPersistentHeader.floating] set to true). |
76 | /// |
77 | /// Defaults to null. |
78 | FloatingHeaderSnapConfiguration? get snapConfiguration => null; |
79 | |
80 | /// Specifies an [AsyncCallback] and offset for execution. |
81 | /// |
82 | /// If the value of this property is null, then callback will not be |
83 | /// triggered. |
84 | /// |
85 | /// This is only used for stretching headers (those with |
86 | /// [SliverAppBar.stretch] set to true). |
87 | /// |
88 | /// Defaults to null. |
89 | OverScrollHeaderStretchConfiguration? get stretchConfiguration => null; |
90 | |
91 | /// Specifies how floating headers and pinned headers should behave in |
92 | /// response to [RenderObject.showOnScreen] calls. |
93 | /// |
94 | /// Defaults to null. |
95 | PersistentHeaderShowOnScreenConfiguration? get showOnScreenConfiguration => null; |
96 | |
97 | /// Whether this delegate is meaningfully different from the old delegate. |
98 | /// |
99 | /// If this returns false, then the header might not be rebuilt, even though |
100 | /// the instance of the delegate changed. |
101 | /// |
102 | /// This must return true if `oldDelegate` and this object would return |
103 | /// different values for [minExtent], [maxExtent], [snapConfiguration], or |
104 | /// would return a meaningfully different widget tree from [build] for the |
105 | /// same arguments. |
106 | bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate); |
107 | } |
108 | |
109 | /// A sliver whose size varies when the sliver is scrolled to the edge |
110 | /// of the viewport opposite the sliver's [GrowthDirection]. |
111 | /// |
112 | /// In the normal case of a [CustomScrollView] with no centered sliver, this |
113 | /// sliver will vary its size when scrolled to the leading edge of the viewport. |
114 | /// |
115 | /// This is the layout primitive that [SliverAppBar] uses for its |
116 | /// shrinking/growing effect. |
117 | /// |
118 | /// _To learn more about slivers, see [CustomScrollView.slivers]._ |
119 | class SliverPersistentHeader extends StatelessWidget { |
120 | /// Creates a sliver that varies its size when it is scrolled to the start of |
121 | /// a viewport. |
122 | const SliverPersistentHeader({ |
123 | super.key, |
124 | required this.delegate, |
125 | this.pinned = false, |
126 | this.floating = false, |
127 | }); |
128 | |
129 | /// Configuration for the sliver's layout. |
130 | /// |
131 | /// The delegate provides the following information: |
132 | /// |
133 | /// * The minimum and maximum dimensions of the sliver. |
134 | /// |
135 | /// * The builder for generating the widgets of the sliver. |
136 | /// |
137 | /// * The instructions for snapping the scroll offset, if [floating] is true. |
138 | final SliverPersistentHeaderDelegate delegate; |
139 | |
140 | /// Whether to stick the header to the start of the viewport once it has |
141 | /// reached its minimum size. |
142 | /// |
143 | /// If this is false, the header will continue scrolling off the screen after |
144 | /// it has shrunk to its minimum extent. |
145 | final bool pinned; |
146 | |
147 | /// Whether the header should immediately grow again if the user reverses |
148 | /// scroll direction. |
149 | /// |
150 | /// If this is false, the header only grows again once the user reaches the |
151 | /// part of the viewport that contains the sliver. |
152 | /// |
153 | /// The [delegate]'s [SliverPersistentHeaderDelegate.snapConfiguration] is |
154 | /// ignored unless [floating] is true. |
155 | final bool floating; |
156 | |
157 | @override |
158 | Widget build(BuildContext context) { |
159 | if (floating && pinned) { |
160 | return _SliverFloatingPinnedPersistentHeader(delegate: delegate); |
161 | } |
162 | if (pinned) { |
163 | return _SliverPinnedPersistentHeader(delegate: delegate); |
164 | } |
165 | if (floating) { |
166 | return _SliverFloatingPersistentHeader(delegate: delegate); |
167 | } |
168 | return _SliverScrollingPersistentHeader(delegate: delegate); |
169 | } |
170 | |
171 | @override |
172 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
173 | super.debugFillProperties(properties); |
174 | properties.add( |
175 | DiagnosticsProperty<SliverPersistentHeaderDelegate>( |
176 | 'delegate' , |
177 | delegate, |
178 | ), |
179 | ); |
180 | final List<String> flags = <String>[ |
181 | if (pinned) 'pinned' , |
182 | if (floating) 'floating' , |
183 | ]; |
184 | if (flags.isEmpty) { |
185 | flags.add('normal' ); |
186 | } |
187 | properties.add(IterableProperty<String>('mode' , flags)); |
188 | } |
189 | } |
190 | |
191 | class _FloatingHeader extends StatefulWidget { |
192 | const _FloatingHeader({ required this.child }); |
193 | |
194 | final Widget child; |
195 | |
196 | @override |
197 | _FloatingHeaderState createState() => _FloatingHeaderState(); |
198 | } |
199 | |
200 | // A wrapper for the widget created by _SliverPersistentHeaderElement that |
201 | // starts and stops the floating app bar's snap-into-view or snap-out-of-view |
202 | // animation. It also informs the float when pointer scrolling by updating the |
203 | // last known ScrollDirection when scrolling began. |
204 | class _FloatingHeaderState extends State<_FloatingHeader> { |
205 | ScrollPosition? _position; |
206 | |
207 | @override |
208 | void didChangeDependencies() { |
209 | super.didChangeDependencies(); |
210 | if (_position != null) { |
211 | _position!.isScrollingNotifier.removeListener(_isScrollingListener); |
212 | } |
213 | _position = Scrollable.maybeOf(context)?.position; |
214 | if (_position != null) { |
215 | _position!.isScrollingNotifier.addListener(_isScrollingListener); |
216 | } |
217 | } |
218 | |
219 | @override |
220 | void dispose() { |
221 | if (_position != null) { |
222 | _position!.isScrollingNotifier.removeListener(_isScrollingListener); |
223 | } |
224 | super.dispose(); |
225 | } |
226 | |
227 | RenderSliverFloatingPersistentHeader? _headerRenderer() { |
228 | return context.findAncestorRenderObjectOfType<RenderSliverFloatingPersistentHeader>(); |
229 | } |
230 | |
231 | void _isScrollingListener() { |
232 | assert(_position != null); |
233 | |
234 | // When a scroll stops, then maybe snap the app bar into view. |
235 | // Similarly, when a scroll starts, then maybe stop the snap animation. |
236 | // Update the scrolling direction as well for pointer scrolling updates. |
237 | final RenderSliverFloatingPersistentHeader? header = _headerRenderer(); |
238 | if (_position!.isScrollingNotifier.value) { |
239 | header?.updateScrollStartDirection(_position!.userScrollDirection); |
240 | // Only SliverAppBars support snapping, headers will not snap. |
241 | header?.maybeStopSnapAnimation(_position!.userScrollDirection); |
242 | } else { |
243 | // Only SliverAppBars support snapping, headers will not snap. |
244 | header?.maybeStartSnapAnimation(_position!.userScrollDirection); |
245 | } |
246 | } |
247 | |
248 | @override |
249 | Widget build(BuildContext context) => widget.child; |
250 | } |
251 | |
252 | class _SliverPersistentHeaderElement extends RenderObjectElement { |
253 | _SliverPersistentHeaderElement( |
254 | _SliverPersistentHeaderRenderObjectWidget super.widget, { |
255 | this.floating = false, |
256 | }); |
257 | |
258 | final bool floating; |
259 | |
260 | @override |
261 | _RenderSliverPersistentHeaderForWidgetsMixin get renderObject => super.renderObject as _RenderSliverPersistentHeaderForWidgetsMixin; |
262 | |
263 | @override |
264 | void mount(Element? parent, Object? newSlot) { |
265 | super.mount(parent, newSlot); |
266 | renderObject._element = this; |
267 | } |
268 | |
269 | @override |
270 | void unmount() { |
271 | renderObject._element = null; |
272 | super.unmount(); |
273 | } |
274 | |
275 | @override |
276 | void update(_SliverPersistentHeaderRenderObjectWidget newWidget) { |
277 | final _SliverPersistentHeaderRenderObjectWidget oldWidget = widget as _SliverPersistentHeaderRenderObjectWidget; |
278 | super.update(newWidget); |
279 | final SliverPersistentHeaderDelegate newDelegate = newWidget.delegate; |
280 | final SliverPersistentHeaderDelegate oldDelegate = oldWidget.delegate; |
281 | if (newDelegate != oldDelegate && |
282 | (newDelegate.runtimeType != oldDelegate.runtimeType || newDelegate.shouldRebuild(oldDelegate))) { |
283 | renderObject.triggerRebuild(); |
284 | } |
285 | } |
286 | |
287 | @override |
288 | void performRebuild() { |
289 | super.performRebuild(); |
290 | renderObject.triggerRebuild(); |
291 | } |
292 | |
293 | Element? child; |
294 | |
295 | void _build(double shrinkOffset, bool overlapsContent) { |
296 | owner!.buildScope(this, () { |
297 | final _SliverPersistentHeaderRenderObjectWidget sliverPersistentHeaderRenderObjectWidget = widget as _SliverPersistentHeaderRenderObjectWidget; |
298 | child = updateChild( |
299 | child, |
300 | floating |
301 | ? _FloatingHeader(child: sliverPersistentHeaderRenderObjectWidget.delegate.build( |
302 | this, |
303 | shrinkOffset, |
304 | overlapsContent |
305 | )) |
306 | : sliverPersistentHeaderRenderObjectWidget.delegate.build(this, shrinkOffset, overlapsContent), |
307 | null, |
308 | ); |
309 | }); |
310 | } |
311 | |
312 | @override |
313 | void forgetChild(Element child) { |
314 | assert(child == this.child); |
315 | this.child = null; |
316 | super.forgetChild(child); |
317 | } |
318 | |
319 | @override |
320 | void insertRenderObjectChild(covariant RenderBox child, Object? slot) { |
321 | assert(renderObject.debugValidateChild(child)); |
322 | renderObject.child = child; |
323 | } |
324 | |
325 | @override |
326 | void moveRenderObjectChild(covariant RenderObject child, Object? oldSlot, Object? newSlot) { |
327 | assert(false); |
328 | } |
329 | |
330 | @override |
331 | void removeRenderObjectChild(covariant RenderObject child, Object? slot) { |
332 | renderObject.child = null; |
333 | } |
334 | |
335 | @override |
336 | void visitChildren(ElementVisitor visitor) { |
337 | if (child != null) { |
338 | visitor(child!); |
339 | } |
340 | } |
341 | } |
342 | |
343 | abstract class _SliverPersistentHeaderRenderObjectWidget extends RenderObjectWidget { |
344 | const _SliverPersistentHeaderRenderObjectWidget({ |
345 | required this.delegate, |
346 | this.floating = false, |
347 | }); |
348 | |
349 | final SliverPersistentHeaderDelegate delegate; |
350 | final bool floating; |
351 | |
352 | @override |
353 | _SliverPersistentHeaderElement createElement() => _SliverPersistentHeaderElement(this, floating: floating); |
354 | |
355 | @override |
356 | _RenderSliverPersistentHeaderForWidgetsMixin createRenderObject(BuildContext context); |
357 | |
358 | @override |
359 | void debugFillProperties(DiagnosticPropertiesBuilder description) { |
360 | super.debugFillProperties(description); |
361 | description.add( |
362 | DiagnosticsProperty<SliverPersistentHeaderDelegate>( |
363 | 'delegate' , |
364 | delegate, |
365 | ), |
366 | ); |
367 | } |
368 | } |
369 | |
370 | mixin _RenderSliverPersistentHeaderForWidgetsMixin on RenderSliverPersistentHeader { |
371 | _SliverPersistentHeaderElement? _element; |
372 | |
373 | @override |
374 | double get minExtent => (_element!.widget as _SliverPersistentHeaderRenderObjectWidget).delegate.minExtent; |
375 | |
376 | @override |
377 | double get maxExtent => (_element!.widget as _SliverPersistentHeaderRenderObjectWidget).delegate.maxExtent; |
378 | |
379 | @override |
380 | void updateChild(double shrinkOffset, bool overlapsContent) { |
381 | assert(_element != null); |
382 | _element!._build(shrinkOffset, overlapsContent); |
383 | } |
384 | |
385 | @protected |
386 | void triggerRebuild() { |
387 | markNeedsLayout(); |
388 | } |
389 | } |
390 | |
391 | class _SliverScrollingPersistentHeader extends _SliverPersistentHeaderRenderObjectWidget { |
392 | const _SliverScrollingPersistentHeader({ |
393 | required super.delegate, |
394 | }); |
395 | |
396 | @override |
397 | _RenderSliverPersistentHeaderForWidgetsMixin createRenderObject(BuildContext context) { |
398 | return _RenderSliverScrollingPersistentHeaderForWidgets( |
399 | stretchConfiguration: delegate.stretchConfiguration, |
400 | ); |
401 | } |
402 | |
403 | @override |
404 | void updateRenderObject(BuildContext context, covariant _RenderSliverScrollingPersistentHeaderForWidgets renderObject) { |
405 | renderObject.stretchConfiguration = delegate.stretchConfiguration; |
406 | } |
407 | } |
408 | |
409 | class _RenderSliverScrollingPersistentHeaderForWidgets extends RenderSliverScrollingPersistentHeader |
410 | with _RenderSliverPersistentHeaderForWidgetsMixin { |
411 | _RenderSliverScrollingPersistentHeaderForWidgets({ |
412 | super.stretchConfiguration, |
413 | }); |
414 | } |
415 | |
416 | class _SliverPinnedPersistentHeader extends _SliverPersistentHeaderRenderObjectWidget { |
417 | const _SliverPinnedPersistentHeader({ |
418 | required super.delegate, |
419 | }); |
420 | |
421 | @override |
422 | _RenderSliverPersistentHeaderForWidgetsMixin createRenderObject(BuildContext context) { |
423 | return _RenderSliverPinnedPersistentHeaderForWidgets( |
424 | stretchConfiguration: delegate.stretchConfiguration, |
425 | showOnScreenConfiguration: delegate.showOnScreenConfiguration, |
426 | ); |
427 | } |
428 | |
429 | @override |
430 | void updateRenderObject(BuildContext context, covariant _RenderSliverPinnedPersistentHeaderForWidgets renderObject) { |
431 | renderObject |
432 | ..stretchConfiguration = delegate.stretchConfiguration |
433 | ..showOnScreenConfiguration = delegate.showOnScreenConfiguration; |
434 | } |
435 | } |
436 | |
437 | class _RenderSliverPinnedPersistentHeaderForWidgets extends RenderSliverPinnedPersistentHeader |
438 | with _RenderSliverPersistentHeaderForWidgetsMixin { |
439 | _RenderSliverPinnedPersistentHeaderForWidgets({ |
440 | super.stretchConfiguration, |
441 | super.showOnScreenConfiguration, |
442 | }); |
443 | } |
444 | |
445 | class _SliverFloatingPersistentHeader extends _SliverPersistentHeaderRenderObjectWidget { |
446 | const _SliverFloatingPersistentHeader({ |
447 | required super.delegate, |
448 | }) : super( |
449 | floating: true, |
450 | ); |
451 | |
452 | @override |
453 | _RenderSliverPersistentHeaderForWidgetsMixin createRenderObject(BuildContext context) { |
454 | return _RenderSliverFloatingPersistentHeaderForWidgets( |
455 | vsync: delegate.vsync, |
456 | snapConfiguration: delegate.snapConfiguration, |
457 | stretchConfiguration: delegate.stretchConfiguration, |
458 | showOnScreenConfiguration: delegate.showOnScreenConfiguration, |
459 | ); |
460 | } |
461 | |
462 | @override |
463 | void updateRenderObject(BuildContext context, _RenderSliverFloatingPersistentHeaderForWidgets renderObject) { |
464 | renderObject.vsync = delegate.vsync; |
465 | renderObject.snapConfiguration = delegate.snapConfiguration; |
466 | renderObject.stretchConfiguration = delegate.stretchConfiguration; |
467 | renderObject.showOnScreenConfiguration = delegate.showOnScreenConfiguration; |
468 | } |
469 | } |
470 | |
471 | class _RenderSliverFloatingPinnedPersistentHeaderForWidgets extends RenderSliverFloatingPinnedPersistentHeader |
472 | with _RenderSliverPersistentHeaderForWidgetsMixin { |
473 | _RenderSliverFloatingPinnedPersistentHeaderForWidgets({ |
474 | required super.vsync, |
475 | super.snapConfiguration, |
476 | super.stretchConfiguration, |
477 | super.showOnScreenConfiguration, |
478 | }); |
479 | } |
480 | |
481 | class _SliverFloatingPinnedPersistentHeader extends _SliverPersistentHeaderRenderObjectWidget { |
482 | const _SliverFloatingPinnedPersistentHeader({ |
483 | required super.delegate, |
484 | }) : super( |
485 | floating: true, |
486 | ); |
487 | |
488 | @override |
489 | _RenderSliverPersistentHeaderForWidgetsMixin createRenderObject(BuildContext context) { |
490 | return _RenderSliverFloatingPinnedPersistentHeaderForWidgets( |
491 | vsync: delegate.vsync, |
492 | snapConfiguration: delegate.snapConfiguration, |
493 | stretchConfiguration: delegate.stretchConfiguration, |
494 | showOnScreenConfiguration: delegate.showOnScreenConfiguration, |
495 | ); |
496 | } |
497 | |
498 | @override |
499 | void updateRenderObject(BuildContext context, _RenderSliverFloatingPinnedPersistentHeaderForWidgets renderObject) { |
500 | renderObject.vsync = delegate.vsync; |
501 | renderObject.snapConfiguration = delegate.snapConfiguration; |
502 | renderObject.stretchConfiguration = delegate.stretchConfiguration; |
503 | renderObject.showOnScreenConfiguration = delegate.showOnScreenConfiguration; |
504 | } |
505 | } |
506 | |
507 | class _RenderSliverFloatingPersistentHeaderForWidgets extends RenderSliverFloatingPersistentHeader |
508 | with _RenderSliverPersistentHeaderForWidgetsMixin { |
509 | _RenderSliverFloatingPersistentHeaderForWidgets({ |
510 | required super.vsync, |
511 | super.snapConfiguration, |
512 | super.stretchConfiguration, |
513 | super.showOnScreenConfiguration, |
514 | }); |
515 | } |
516 | |