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 'dart:math' as math;
6
7import 'package:flutter/animation.dart';
8import 'package:flutter/foundation.dart';
9import 'package:flutter/scheduler.dart';
10import 'package:flutter/semantics.dart';
11
12import 'box.dart';
13import 'object.dart';
14import 'sliver.dart';
15import 'viewport.dart';
16import 'viewport_offset.dart';
17
18// Trims the specified edges of the given `Rect` [original], so that they do not
19// exceed the given values.
20Rect? _trim(
21 Rect? original, {
22 double top = -double.infinity,
23 double right = double.infinity,
24 double bottom = double.infinity,
25 double left = -double.infinity,
26}) => original?.intersect(Rect.fromLTRB(left, top, right, bottom));
27
28/// Specifies how a stretched header is to trigger an [AsyncCallback].
29///
30/// See also:
31///
32/// * [SliverAppBar], which creates a header that can be stretched into an
33/// overscroll area and trigger a callback function.
34class OverScrollHeaderStretchConfiguration {
35 /// Creates an object that specifies how a stretched header may activate an
36 /// [AsyncCallback].
37 OverScrollHeaderStretchConfiguration({
38 this.stretchTriggerOffset = 100.0,
39 this.onStretchTrigger,
40 });
41
42 /// The offset of overscroll required to trigger the [onStretchTrigger].
43 final double stretchTriggerOffset;
44
45 /// The callback function to be executed when a user over-scrolls to the
46 /// offset specified by [stretchTriggerOffset].
47 final AsyncCallback? onStretchTrigger;
48}
49
50/// {@template flutter.rendering.PersistentHeaderShowOnScreenConfiguration}
51/// Specifies how a pinned header or a floating header should react to
52/// [RenderObject.showOnScreen] calls.
53/// {@endtemplate}
54@immutable
55class PersistentHeaderShowOnScreenConfiguration {
56 /// Creates an object that specifies how a pinned or floating persistent header
57 /// should behave in response to [RenderObject.showOnScreen] calls.
58 const PersistentHeaderShowOnScreenConfiguration({
59 this.minShowOnScreenExtent = double.negativeInfinity,
60 this.maxShowOnScreenExtent = double.infinity,
61 }) : assert(minShowOnScreenExtent <= maxShowOnScreenExtent);
62
63 /// The smallest the floating header can expand to in the main axis direction,
64 /// in response to a [RenderObject.showOnScreen] call, in addition to its
65 /// [RenderSliverPersistentHeader.minExtent].
66 ///
67 /// When a floating persistent header is told to show a [Rect] on screen, it
68 /// may expand itself to accommodate the [Rect]. The minimum extent that is
69 /// allowed for such expansion is either
70 /// [RenderSliverPersistentHeader.minExtent] or [minShowOnScreenExtent],
71 /// whichever is larger. If the persistent header's current extent is already
72 /// larger than that maximum extent, it will remain unchanged.
73 ///
74 /// This parameter can be set to the persistent header's `maxExtent` (or
75 /// `double.infinity`) so the persistent header will always try to expand when
76 /// [RenderObject.showOnScreen] is called on it.
77 ///
78 /// Defaults to [double.negativeInfinity], must be less than or equal to
79 /// [maxShowOnScreenExtent]. Has no effect unless the persistent header is a
80 /// floating header.
81 final double minShowOnScreenExtent;
82
83 /// The biggest the floating header can expand to in the main axis direction,
84 /// in response to a [RenderObject.showOnScreen] call, in addition to its
85 /// [RenderSliverPersistentHeader.maxExtent].
86 ///
87 /// When a floating persistent header is told to show a [Rect] on screen, it
88 /// may expand itself to accommodate the [Rect]. The maximum extent that is
89 /// allowed for such expansion is either
90 /// [RenderSliverPersistentHeader.maxExtent] or [maxShowOnScreenExtent],
91 /// whichever is smaller. If the persistent header's current extent is already
92 /// larger than that maximum extent, it will remain unchanged.
93 ///
94 /// This parameter can be set to the persistent header's `minExtent` (or
95 /// `double.negativeInfinity`) so the persistent header will never try to
96 /// expand when [RenderObject.showOnScreen] is called on it.
97 ///
98 /// Defaults to [double.infinity], must be greater than or equal to
99 /// [minShowOnScreenExtent]. Has no effect unless the persistent header is a
100 /// floating header.
101 final double maxShowOnScreenExtent;
102}
103
104/// A base class for slivers that have a [RenderBox] child which scrolls
105/// normally, except that when it hits the leading edge (typically the top) of
106/// the viewport, it shrinks to a minimum size ([minExtent]).
107///
108/// This class primarily provides helpers for managing the child, in particular:
109///
110/// * [layoutChild], which applies min and max extents and a scroll offset to
111/// lay out the child. This is normally called from [performLayout].
112///
113/// * [childExtent], to convert the child's box layout dimensions to the sliver
114/// geometry model.
115///
116/// * hit testing, painting, and other details of the sliver protocol.
117///
118/// Subclasses must implement [performLayout], [minExtent], and [maxExtent], and
119/// typically also will implement [updateChild].
120abstract class RenderSliverPersistentHeader extends RenderSliver with RenderObjectWithChildMixin<RenderBox>, RenderSliverHelpers {
121 /// Creates a sliver that changes its size when scrolled to the start of the
122 /// viewport.
123 ///
124 /// This is an abstract class; this constructor only initializes the [child].
125 RenderSliverPersistentHeader({
126 RenderBox? child,
127 this.stretchConfiguration,
128 }) {
129 this.child = child;
130 }
131
132 late double _lastStretchOffset;
133
134 /// The biggest that this render object can become, in the main axis direction.
135 ///
136 /// This value should not be based on the child. If it changes, call
137 /// [markNeedsLayout].
138 double get maxExtent;
139
140 /// The smallest that this render object can become, in the main axis direction.
141 ///
142 /// If this is based on the intrinsic dimensions of the child, the child
143 /// should be measured during [updateChild] and the value cached and returned
144 /// here. The [updateChild] method will automatically be invoked any time the
145 /// child changes its intrinsic dimensions.
146 double get minExtent;
147
148 /// The dimension of the child in the main axis.
149 @protected
150 double get childExtent {
151 if (child == null) {
152 return 0.0;
153 }
154 assert(child!.hasSize);
155 switch (constraints.axis) {
156 case Axis.vertical:
157 return child!.size.height;
158 case Axis.horizontal:
159 return child!.size.width;
160 }
161 }
162
163 bool _needsUpdateChild = true;
164 double _lastShrinkOffset = 0.0;
165 bool _lastOverlapsContent = false;
166
167 /// Defines the parameters used to execute an [AsyncCallback] when a
168 /// stretching header over-scrolls.
169 ///
170 /// If [stretchConfiguration] is null then callback is not triggered.
171 ///
172 /// See also:
173 ///
174 /// * [SliverAppBar], which creates a header that can stretched into an
175 /// overscroll area and trigger a callback function.
176 OverScrollHeaderStretchConfiguration? stretchConfiguration;
177
178 /// Update the child render object if necessary.
179 ///
180 /// Called before the first layout, any time [markNeedsLayout] is called, and
181 /// any time the scroll offset changes. The `shrinkOffset` is the difference
182 /// between the [maxExtent] and the current size. Zero means the header is
183 /// fully expanded, any greater number up to [maxExtent] means that the header
184 /// has been scrolled by that much. The `overlapsContent` argument is true if
185 /// the sliver's leading edge is beyond its normal place in the viewport
186 /// contents, and false otherwise. It may still paint beyond its normal place
187 /// if the [minExtent] after this call is greater than the amount of space that
188 /// would normally be left.
189 ///
190 /// The render object will size itself to the larger of (a) the [maxExtent]
191 /// minus the child's intrinsic height and (b) the [maxExtent] minus the
192 /// shrink offset.
193 ///
194 /// When this method is called by [layoutChild], the [child] can be set,
195 /// mutated, or replaced. (It should not be called outside [layoutChild].)
196 ///
197 /// Any time this method would mutate the child, call [markNeedsLayout].
198 @protected
199 void updateChild(double shrinkOffset, bool overlapsContent) { }
200
201 @override
202 void markNeedsLayout() {
203 // This is automatically called whenever the child's intrinsic dimensions
204 // change, at which point we should remeasure them during the next layout.
205 _needsUpdateChild = true;
206 super.markNeedsLayout();
207 }
208
209 /// Lays out the [child].
210 ///
211 /// This is called by [performLayout]. It applies the given `scrollOffset`
212 /// (which need not match the offset given by the [constraints]) and the
213 /// `maxExtent` (which need not match the value returned by the [maxExtent]
214 /// getter).
215 ///
216 /// The `overlapsContent` argument is passed to [updateChild].
217 @protected
218 void layoutChild(double scrollOffset, double maxExtent, { bool overlapsContent = false }) {
219 final double shrinkOffset = math.min(scrollOffset, maxExtent);
220 if (_needsUpdateChild || _lastShrinkOffset != shrinkOffset || _lastOverlapsContent != overlapsContent) {
221 invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
222 assert(constraints == this.constraints);
223 updateChild(shrinkOffset, overlapsContent);
224 });
225 _lastShrinkOffset = shrinkOffset;
226 _lastOverlapsContent = overlapsContent;
227 _needsUpdateChild = false;
228 }
229 assert(() {
230 if (minExtent <= maxExtent) {
231 return true;
232 }
233 throw FlutterError.fromParts(<DiagnosticsNode>[
234 ErrorSummary('The maxExtent for this $runtimeType is less than its minExtent.'),
235 DoubleProperty('The specified maxExtent was', maxExtent),
236 DoubleProperty('The specified minExtent was', minExtent),
237 ]);
238 }());
239 double stretchOffset = 0.0;
240 if (stretchConfiguration != null && constraints.scrollOffset == 0.0) {
241 stretchOffset += constraints.overlap.abs();
242 }
243
244 child?.layout(
245 constraints.asBoxConstraints(
246 maxExtent: math.max(minExtent, maxExtent - shrinkOffset) + stretchOffset,
247 ),
248 parentUsesSize: true,
249 );
250
251 if (stretchConfiguration != null &&
252 stretchConfiguration!.onStretchTrigger != null &&
253 stretchOffset >= stretchConfiguration!.stretchTriggerOffset &&
254 _lastStretchOffset <= stretchConfiguration!.stretchTriggerOffset) {
255 stretchConfiguration!.onStretchTrigger!();
256 }
257 _lastStretchOffset = stretchOffset;
258 }
259
260 /// Returns the distance from the leading _visible_ edge of the sliver to the
261 /// side of the child closest to that edge, in the scroll axis direction.
262 ///
263 /// For example, if the [constraints] describe this sliver as having an axis
264 /// direction of [AxisDirection.down], then this is the distance from the top
265 /// of the visible portion of the sliver to the top of the child. If the child
266 /// is scrolled partially off the top of the viewport, then this will be
267 /// negative. On the other hand, if the [constraints] describe this sliver as
268 /// having an axis direction of [AxisDirection.up], then this is the distance
269 /// from the bottom of the visible portion of the sliver to the bottom of the
270 /// child. In both cases, this is the direction of increasing
271 /// [SliverConstraints.scrollOffset].
272 ///
273 /// Calling this when the child is not visible is not valid.
274 ///
275 /// The argument must be the value of the [child] property.
276 ///
277 /// This must be implemented by [RenderSliverPersistentHeader] subclasses.
278 ///
279 /// If there is no child, this should return 0.0.
280 @override
281 double childMainAxisPosition(covariant RenderObject child) => super.childMainAxisPosition(child);
282
283 @override
284 bool hitTestChildren(SliverHitTestResult result, { required double mainAxisPosition, required double crossAxisPosition }) {
285 assert(geometry!.hitTestExtent > 0.0);
286 if (child != null) {
287 return hitTestBoxChild(BoxHitTestResult.wrap(result), child!, mainAxisPosition: mainAxisPosition, crossAxisPosition: crossAxisPosition);
288 }
289 return false;
290 }
291
292 @override
293 void applyPaintTransform(RenderObject child, Matrix4 transform) {
294 assert(child == this.child);
295 applyPaintTransformForBoxChild(child as RenderBox, transform);
296 }
297
298 @override
299 void paint(PaintingContext context, Offset offset) {
300 if (child != null && geometry!.visible) {
301 switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
302 case AxisDirection.up:
303 offset += Offset(0.0, geometry!.paintExtent - childMainAxisPosition(child!) - childExtent);
304 case AxisDirection.down:
305 offset += Offset(0.0, childMainAxisPosition(child!));
306 case AxisDirection.left:
307 offset += Offset(geometry!.paintExtent - childMainAxisPosition(child!) - childExtent, 0.0);
308 case AxisDirection.right:
309 offset += Offset(childMainAxisPosition(child!), 0.0);
310 }
311 context.paintChild(child!, offset);
312 }
313 }
314
315 @override
316 void describeSemanticsConfiguration(SemanticsConfiguration config) {
317 super.describeSemanticsConfiguration(config);
318 config.addTagForChildren(RenderViewport.excludeFromScrolling);
319 }
320
321 @override
322 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
323 super.debugFillProperties(properties);
324 properties.add(DoubleProperty.lazy('maxExtent', () => maxExtent));
325 properties.add(DoubleProperty.lazy('child position', () => childMainAxisPosition(child!)));
326 }
327}
328
329/// A sliver with a [RenderBox] child which scrolls normally, except that when
330/// it hits the leading edge (typically the top) of the viewport, it shrinks to
331/// a minimum size before continuing to scroll.
332///
333/// This sliver makes no effort to avoid overlapping other content.
334abstract class RenderSliverScrollingPersistentHeader extends RenderSliverPersistentHeader {
335 /// Creates a sliver that shrinks when it hits the start of the viewport, then
336 /// scrolls off.
337 RenderSliverScrollingPersistentHeader({
338 super.child,
339 super.stretchConfiguration,
340 });
341
342 // Distance from our leading edge to the child's leading edge, in the axis
343 // direction. Negative if we're scrolled off the top.
344 double? _childPosition;
345
346 /// Updates [geometry], and returns the new value for [childMainAxisPosition].
347 ///
348 /// This is used by [performLayout].
349 @protected
350 double updateGeometry() {
351 double stretchOffset = 0.0;
352 if (stretchConfiguration != null) {
353 stretchOffset += constraints.overlap.abs();
354 }
355 final double maxExtent = this.maxExtent;
356 final double paintExtent = maxExtent - constraints.scrollOffset;
357 geometry = SliverGeometry(
358 scrollExtent: maxExtent,
359 paintOrigin: math.min(constraints.overlap, 0.0),
360 paintExtent: clampDouble(paintExtent, 0.0, constraints.remainingPaintExtent),
361 maxPaintExtent: maxExtent + stretchOffset,
362 hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity.
363 );
364 return stretchOffset > 0 ? 0.0 : math.min(0.0, paintExtent - childExtent);
365 }
366
367
368 @override
369 void performLayout() {
370 final SliverConstraints constraints = this.constraints;
371 final double maxExtent = this.maxExtent;
372 layoutChild(constraints.scrollOffset, maxExtent);
373 final double paintExtent = maxExtent - constraints.scrollOffset;
374 geometry = SliverGeometry(
375 scrollExtent: maxExtent,
376 paintOrigin: math.min(constraints.overlap, 0.0),
377 paintExtent: clampDouble(paintExtent, 0.0, constraints.remainingPaintExtent),
378 maxPaintExtent: maxExtent,
379 hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity.
380 );
381 _childPosition = updateGeometry();
382 }
383
384 @override
385 double childMainAxisPosition(RenderBox child) {
386 assert(child == this.child);
387 assert(_childPosition != null);
388 return _childPosition!;
389 }
390}
391
392/// A sliver with a [RenderBox] child which never scrolls off the viewport in
393/// the positive scroll direction, and which first scrolls on at a full size but
394/// then shrinks as the viewport continues to scroll.
395///
396/// This sliver avoids overlapping other earlier slivers where possible.
397abstract class RenderSliverPinnedPersistentHeader extends RenderSliverPersistentHeader {
398 /// Creates a sliver that shrinks when it hits the start of the viewport, then
399 /// stays pinned there.
400 RenderSliverPinnedPersistentHeader({
401 super.child,
402 super.stretchConfiguration,
403 this.showOnScreenConfiguration = const PersistentHeaderShowOnScreenConfiguration(),
404 });
405
406 /// Specifies the persistent header's behavior when `showOnScreen` is called.
407 ///
408 /// If set to null, the persistent header will delegate the `showOnScreen` call
409 /// to it's parent [RenderObject].
410 PersistentHeaderShowOnScreenConfiguration? showOnScreenConfiguration;
411
412 @override
413 void performLayout() {
414 final SliverConstraints constraints = this.constraints;
415 final double maxExtent = this.maxExtent;
416 final bool overlapsContent = constraints.overlap > 0.0;
417 layoutChild(constraints.scrollOffset, maxExtent, overlapsContent: overlapsContent);
418 final double effectiveRemainingPaintExtent = math.max(0, constraints.remainingPaintExtent - constraints.overlap);
419 final double layoutExtent = clampDouble(maxExtent - constraints.scrollOffset, 0.0, effectiveRemainingPaintExtent);
420 final double stretchOffset = stretchConfiguration != null ?
421 constraints.overlap.abs() :
422 0.0;
423 geometry = SliverGeometry(
424 scrollExtent: maxExtent,
425 paintOrigin: constraints.overlap,
426 paintExtent: math.min(childExtent, effectiveRemainingPaintExtent),
427 layoutExtent: layoutExtent,
428 maxPaintExtent: maxExtent + stretchOffset,
429 maxScrollObstructionExtent: minExtent,
430 cacheExtent: layoutExtent > 0.0 ? -constraints.cacheOrigin + layoutExtent : layoutExtent,
431 hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity.
432 );
433 }
434
435 @override
436 double childMainAxisPosition(RenderBox child) => 0.0;
437
438 @override
439 void showOnScreen({
440 RenderObject? descendant,
441 Rect? rect,
442 Duration duration = Duration.zero,
443 Curve curve = Curves.ease,
444 }) {
445 final Rect? localBounds = descendant != null
446 ? MatrixUtils.transformRect(descendant.getTransformTo(this), rect ?? descendant.paintBounds)
447 : rect;
448
449 Rect? newRect;
450 switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
451 case AxisDirection.up:
452 newRect = _trim(localBounds, bottom: childExtent);
453 case AxisDirection.right:
454 newRect = _trim(localBounds, left: 0);
455 case AxisDirection.down:
456 newRect = _trim(localBounds, top: 0);
457 case AxisDirection.left:
458 newRect = _trim(localBounds, right: childExtent);
459 }
460
461 super.showOnScreen(
462 descendant: this,
463 rect: newRect,
464 duration: duration,
465 curve: curve,
466 );
467 }
468}
469
470/// Specifies how a floating header is to be "snapped" (animated) into or out
471/// of view.
472///
473/// See also:
474///
475/// * [RenderSliverFloatingPersistentHeader.maybeStartSnapAnimation] and
476/// [RenderSliverFloatingPersistentHeader.maybeStopSnapAnimation], which
477/// start or stop the floating header's animation.
478/// * [SliverAppBar], which creates a header that can be pinned, floating,
479/// and snapped into view via the corresponding parameters.
480class FloatingHeaderSnapConfiguration {
481 /// Creates an object that specifies how a floating header is to be "snapped"
482 /// (animated) into or out of view.
483 FloatingHeaderSnapConfiguration({
484 this.curve = Curves.ease,
485 this.duration = const Duration(milliseconds: 300),
486 });
487
488 /// The snap animation curve.
489 final Curve curve;
490
491 /// The snap animation's duration.
492 final Duration duration;
493}
494
495/// A sliver with a [RenderBox] child which shrinks and scrolls like a
496/// [RenderSliverScrollingPersistentHeader], but immediately comes back when the
497/// user scrolls in the reverse direction.
498///
499/// See also:
500///
501/// * [RenderSliverFloatingPinnedPersistentHeader], which is similar but sticks
502/// to the start of the viewport rather than scrolling off.
503abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersistentHeader {
504 /// Creates a sliver that shrinks when it hits the start of the viewport, then
505 /// scrolls off, and comes back immediately when the user reverses the scroll
506 /// direction.
507 RenderSliverFloatingPersistentHeader({
508 super.child,
509 TickerProvider? vsync,
510 this.snapConfiguration,
511 super.stretchConfiguration,
512 required this.showOnScreenConfiguration,
513 }) : _vsync = vsync;
514
515 AnimationController? _controller;
516 late Animation<double> _animation;
517 double? _lastActualScrollOffset;
518 double? _effectiveScrollOffset;
519 // Important for pointer scrolling, which does not have the same concept of
520 // a hold and release scroll movement, like dragging.
521 // This keeps track of the last ScrollDirection when scrolling started.
522 ScrollDirection? _lastStartedScrollDirection;
523
524 // Distance from our leading edge to the child's leading edge, in the axis
525 // direction. Negative if we're scrolled off the top.
526 double? _childPosition;
527
528 @override
529 void detach() {
530 _controller?.dispose();
531 _controller = null; // lazily recreated if we're reattached.
532 super.detach();
533 }
534
535
536 /// A [TickerProvider] to use when animating the scroll position.
537 TickerProvider? get vsync => _vsync;
538 TickerProvider? _vsync;
539 set vsync(TickerProvider? value) {
540 if (value == _vsync) {
541 return;
542 }
543 _vsync = value;
544 if (value == null) {
545 _controller?.dispose();
546 _controller = null;
547 } else {
548 _controller?.resync(value);
549 }
550 }
551
552 /// Defines the parameters used to snap (animate) the floating header in and
553 /// out of view.
554 ///
555 /// If [snapConfiguration] is null then the floating header does not snap.
556 ///
557 /// See also:
558 ///
559 /// * [RenderSliverFloatingPersistentHeader.maybeStartSnapAnimation] and
560 /// [RenderSliverFloatingPersistentHeader.maybeStopSnapAnimation], which
561 /// start or stop the floating header's animation.
562 /// * [SliverAppBar], which creates a header that can be pinned, floating,
563 /// and snapped into view via the corresponding parameters.
564 FloatingHeaderSnapConfiguration? snapConfiguration;
565
566 /// {@macro flutter.rendering.PersistentHeaderShowOnScreenConfiguration}
567 ///
568 /// If set to null, the persistent header will delegate the `showOnScreen` call
569 /// to it's parent [RenderObject].
570 PersistentHeaderShowOnScreenConfiguration? showOnScreenConfiguration;
571
572 /// Updates [geometry], and returns the new value for [childMainAxisPosition].
573 ///
574 /// This is used by [performLayout].
575 @protected
576 double updateGeometry() {
577 double stretchOffset = 0.0;
578 if (stretchConfiguration != null) {
579 stretchOffset += constraints.overlap.abs();
580 }
581 final double maxExtent = this.maxExtent;
582 final double paintExtent = maxExtent - _effectiveScrollOffset!;
583 final double layoutExtent = maxExtent - constraints.scrollOffset;
584 geometry = SliverGeometry(
585 scrollExtent: maxExtent,
586 paintOrigin: math.min(constraints.overlap, 0.0),
587 paintExtent: clampDouble(paintExtent, 0.0, constraints.remainingPaintExtent),
588 layoutExtent: clampDouble(layoutExtent, 0.0, constraints.remainingPaintExtent),
589 maxPaintExtent: maxExtent + stretchOffset,
590 hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity.
591 );
592 return stretchOffset > 0 ? 0.0 : math.min(0.0, paintExtent - childExtent);
593 }
594
595 void _updateAnimation(Duration duration, double endValue, Curve curve) {
596 assert(
597 vsync != null,
598 'vsync must not be null if the floating header changes size animatedly.',
599 );
600
601 final AnimationController effectiveController =
602 _controller ??= AnimationController(vsync: vsync!, duration: duration)
603 ..addListener(() {
604 if (_effectiveScrollOffset == _animation.value) {
605 return;
606 }
607 _effectiveScrollOffset = _animation.value;
608 markNeedsLayout();
609 });
610
611 _animation = effectiveController.drive(
612 Tween<double>(
613 begin: _effectiveScrollOffset,
614 end: endValue,
615 ).chain(CurveTween(curve: curve)),
616 );
617 }
618
619 /// Update the last known ScrollDirection when scrolling began.
620 // ignore: use_setters_to_change_properties, (API predates enforcing the lint)
621 void updateScrollStartDirection(ScrollDirection direction) {
622 _lastStartedScrollDirection = direction;
623 }
624
625 /// If the header isn't already fully exposed, then scroll it into view.
626 void maybeStartSnapAnimation(ScrollDirection direction) {
627 final FloatingHeaderSnapConfiguration? snap = snapConfiguration;
628 if (snap == null) {
629 return;
630 }
631 if (direction == ScrollDirection.forward && _effectiveScrollOffset! <= 0.0) {
632 return;
633 }
634 if (direction == ScrollDirection.reverse && _effectiveScrollOffset! >= maxExtent) {
635 return;
636 }
637
638 _updateAnimation(
639 snap.duration,
640 direction == ScrollDirection.forward ? 0.0 : maxExtent,
641 snap.curve,
642 );
643 _controller?.forward(from: 0.0);
644 }
645
646 /// If a header snap animation or a [showOnScreen] expand animation is underway
647 /// then stop it.
648 void maybeStopSnapAnimation(ScrollDirection direction) {
649 _controller?.stop();
650 }
651
652 @override
653 void performLayout() {
654 final SliverConstraints constraints = this.constraints;
655 final double maxExtent = this.maxExtent;
656 if (_lastActualScrollOffset != null && // We've laid out at least once to get an initial position, and either
657 ((constraints.scrollOffset < _lastActualScrollOffset!) || // we are scrolling back, so should reveal, or
658 (_effectiveScrollOffset! < maxExtent))) { // some part of it is visible, so should shrink or reveal as appropriate.
659 double delta = _lastActualScrollOffset! - constraints.scrollOffset;
660
661 final bool allowFloatingExpansion = constraints.userScrollDirection == ScrollDirection.forward
662 || (_lastStartedScrollDirection != null && _lastStartedScrollDirection == ScrollDirection.forward);
663 if (allowFloatingExpansion) {
664 if (_effectiveScrollOffset! > maxExtent) {
665 // We're scrolled off-screen, but should reveal, so pretend we're just at the limit.
666 _effectiveScrollOffset = maxExtent;
667 }
668 } else {
669 if (delta > 0.0) {
670 // Disallow the expansion. (But allow shrinking, i.e. delta < 0.0 is fine.)
671 delta = 0.0;
672 }
673 }
674 _effectiveScrollOffset = clampDouble(_effectiveScrollOffset! - delta, 0.0, constraints.scrollOffset);
675 } else {
676 _effectiveScrollOffset = constraints.scrollOffset;
677 }
678 final bool overlapsContent = _effectiveScrollOffset! < constraints.scrollOffset;
679
680 layoutChild(
681 _effectiveScrollOffset!,
682 maxExtent,
683 overlapsContent: overlapsContent,
684 );
685 _childPosition = updateGeometry();
686 _lastActualScrollOffset = constraints.scrollOffset;
687 }
688
689 @override
690 void showOnScreen({
691 RenderObject? descendant,
692 Rect? rect,
693 Duration duration = Duration.zero,
694 Curve curve = Curves.ease,
695 }) {
696 final PersistentHeaderShowOnScreenConfiguration? showOnScreen = showOnScreenConfiguration;
697 if (showOnScreen == null) {
698 return super.showOnScreen(descendant: descendant, rect: rect, duration: duration, curve: curve);
699 }
700
701 assert(child != null || descendant == null);
702 // We prefer the child's coordinate space (instead of the sliver's) because
703 // it's easier for us to convert the target rect into target extents: when
704 // the sliver is sitting above the leading edge (not possible with pinned
705 // headers), the leading edge of the sliver and the leading edge of the child
706 // will not be aligned. The only exception is when child is null (and thus
707 // descendant == null).
708 final Rect? childBounds = descendant != null
709 ? MatrixUtils.transformRect(descendant.getTransformTo(child), rect ?? descendant.paintBounds)
710 : rect;
711
712 double targetExtent;
713 Rect? targetRect;
714 switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
715 case AxisDirection.up:
716 targetExtent = childExtent - (childBounds?.top ?? 0);
717 targetRect = _trim(childBounds, bottom: childExtent);
718 case AxisDirection.right:
719 targetExtent = childBounds?.right ?? childExtent;
720 targetRect = _trim(childBounds, left: 0);
721 case AxisDirection.down:
722 targetExtent = childBounds?.bottom ?? childExtent;
723 targetRect = _trim(childBounds, top: 0);
724 case AxisDirection.left:
725 targetExtent = childExtent - (childBounds?.left ?? 0);
726 targetRect = _trim(childBounds, right: childExtent);
727 }
728
729 // A stretch header can have a bigger childExtent than maxExtent.
730 final double effectiveMaxExtent = math.max(childExtent, maxExtent);
731
732 targetExtent = clampDouble(
733 clampDouble(
734 targetExtent,
735 showOnScreen.minShowOnScreenExtent,
736 showOnScreen.maxShowOnScreenExtent,
737 ),
738 // Clamp the value back to the valid range after applying additional
739 // constraints. Contracting is not allowed.
740 childExtent,
741 effectiveMaxExtent);
742
743 // Expands the header if needed, with animation.
744 if (targetExtent > childExtent && _controller?.status != AnimationStatus.forward) {
745 final double targetScrollOffset = maxExtent - targetExtent;
746 assert(
747 vsync != null,
748 'vsync must not be null if the floating header changes size animatedly.',
749 );
750 _updateAnimation(duration, targetScrollOffset, curve);
751 _controller?.forward(from: 0.0);
752 }
753
754 super.showOnScreen(
755 descendant: descendant == null ? this : child,
756 rect: targetRect,
757 duration: duration,
758 curve: curve,
759 );
760 }
761
762 @override
763 double childMainAxisPosition(RenderBox child) {
764 assert(child == this.child);
765 return _childPosition ?? 0.0;
766 }
767
768 @override
769 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
770 super.debugFillProperties(properties);
771 properties.add(DoubleProperty('effective scroll offset', _effectiveScrollOffset));
772 }
773}
774
775/// A sliver with a [RenderBox] child which shrinks and then remains pinned to
776/// the start of the viewport like a [RenderSliverPinnedPersistentHeader], but
777/// immediately grows when the user scrolls in the reverse direction.
778///
779/// See also:
780///
781/// * [RenderSliverFloatingPersistentHeader], which is similar but scrolls off
782/// the top rather than sticking to it.
783abstract class RenderSliverFloatingPinnedPersistentHeader extends RenderSliverFloatingPersistentHeader {
784 /// Creates a sliver that shrinks when it hits the start of the viewport, then
785 /// stays pinned there, and grows immediately when the user reverses the
786 /// scroll direction.
787 RenderSliverFloatingPinnedPersistentHeader({
788 super.child,
789 super.vsync,
790 super.snapConfiguration,
791 super.stretchConfiguration,
792 super.showOnScreenConfiguration,
793 });
794
795 @override
796 double updateGeometry() {
797 final double minExtent = this.minExtent;
798 final double minAllowedExtent = constraints.remainingPaintExtent > minExtent ?
799 minExtent :
800 constraints.remainingPaintExtent;
801 final double maxExtent = this.maxExtent;
802 final double paintExtent = maxExtent - _effectiveScrollOffset!;
803 final double clampedPaintExtent = clampDouble(paintExtent,
804 minAllowedExtent,
805 constraints.remainingPaintExtent,
806 );
807 final double layoutExtent = maxExtent - constraints.scrollOffset;
808 final double stretchOffset = stretchConfiguration != null ?
809 constraints.overlap.abs() :
810 0.0;
811 geometry = SliverGeometry(
812 scrollExtent: maxExtent,
813 paintOrigin: math.min(constraints.overlap, 0.0),
814 paintExtent: clampedPaintExtent,
815 layoutExtent: clampDouble(layoutExtent, 0.0, clampedPaintExtent),
816 maxPaintExtent: maxExtent + stretchOffset,
817 maxScrollObstructionExtent: minExtent,
818 hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity.
819 );
820 return 0.0;
821 }
822}
823