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
5import 'package:flutter/foundation.dart';
6import 'package:flutter/rendering.dart';
7import 'package:flutter/scheduler.dart' show TickerProvider;
8
9import 'framework.dart';
10import 'scroll_position.dart';
11import 'scrollable.dart';
12
13/// Delegate for configuring a [SliverPersistentHeader].
14abstract 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]._
119class 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
191class _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.
204class _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
252class _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
343abstract 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
370mixin _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
391class _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
409class _RenderSliverScrollingPersistentHeaderForWidgets extends RenderSliverScrollingPersistentHeader
410 with _RenderSliverPersistentHeaderForWidgetsMixin {
411 _RenderSliverScrollingPersistentHeaderForWidgets({
412 super.stretchConfiguration,
413 });
414}
415
416class _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
437class _RenderSliverPinnedPersistentHeaderForWidgets extends RenderSliverPinnedPersistentHeader
438 with _RenderSliverPersistentHeaderForWidgetsMixin {
439 _RenderSliverPinnedPersistentHeaderForWidgets({
440 super.stretchConfiguration,
441 super.showOnScreenConfiguration,
442 });
443}
444
445class _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
471class _RenderSliverFloatingPinnedPersistentHeaderForWidgets extends RenderSliverFloatingPinnedPersistentHeader
472 with _RenderSliverPersistentHeaderForWidgetsMixin {
473 _RenderSliverFloatingPinnedPersistentHeaderForWidgets({
474 required super.vsync,
475 super.snapConfiguration,
476 super.stretchConfiguration,
477 super.showOnScreenConfiguration,
478 });
479}
480
481class _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
507class _RenderSliverFloatingPersistentHeaderForWidgets extends RenderSliverFloatingPersistentHeader
508 with _RenderSliverPersistentHeaderForWidgetsMixin {
509 _RenderSliverFloatingPersistentHeaderForWidgets({
510 required super.vsync,
511 super.snapConfiguration,
512 super.stretchConfiguration,
513 super.showOnScreenConfiguration,
514 });
515}
516