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