sliver_persistent_header.dart 32.1 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
Ian Hickson's avatar
Ian Hickson committed
2 3 4 5 6
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:math' as math;

7
import 'package:flutter/animation.dart';
Ian Hickson's avatar
Ian Hickson committed
8
import 'package:flutter/foundation.dart';
9
import 'package:flutter/scheduler.dart';
10
import 'package:flutter/semantics.dart';
Ian Hickson's avatar
Ian Hickson committed
11

12
import 'box.dart';
Ian Hickson's avatar
Ian Hickson committed
13 14
import 'object.dart';
import 'sliver.dart';
15
import 'viewport.dart';
16
import 'viewport_offset.dart';
Ian Hickson's avatar
Ian Hickson committed
17

18 19
// Trims the specified edges of the given `Rect` [original], so that they do not
// exceed the given values.
20 21
Rect? _trim(
  Rect? original, {
22 23 24 25 26 27
  double top = -double.infinity,
  double right = double.infinity,
  double bottom = double.infinity,
  double left = -double.infinity,
}) => original?.intersect(Rect.fromLTRB(left, top, right, bottom));

28 29 30 31 32
/// Specifies how a stretched header is to trigger an [AsyncCallback].
///
/// See also:
///
///  * [SliverAppBar], which creates a header that can be stretched into an
33
///    overscroll area and trigger a callback function.
34 35 36 37 38 39 40 41 42 43 44 45 46
class OverScrollHeaderStretchConfiguration {
  /// Creates an object that specifies how a stretched header may activate an
  /// [AsyncCallback].
  OverScrollHeaderStretchConfiguration({
    this.stretchTriggerOffset = 100.0,
    this.onStretchTrigger,
  }) : assert(stretchTriggerOffset != null);

  /// The offset of overscroll required to trigger the [onStretchTrigger].
  final double stretchTriggerOffset;

  /// The callback function to be executed when a user over-scrolls to the
  /// offset specified by [stretchTriggerOffset].
47
  final AsyncCallback? onStretchTrigger;
48 49
}

50
/// {@template flutter.rendering.PersistentHeaderShowOnScreenConfiguration}
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
/// Specifies how a pinned header or a floating header should react to
/// [RenderObject.showOnScreen] calls.
/// {@endtemplate}
@immutable
class PersistentHeaderShowOnScreenConfiguration {
  /// Creates an object that specifies how a pinned or floating persistent header
  /// should behave in response to [RenderObject.showOnScreen] calls.
  const PersistentHeaderShowOnScreenConfiguration({
    this.minShowOnScreenExtent = double.negativeInfinity,
    this.maxShowOnScreenExtent = double.infinity,
  }) : assert(minShowOnScreenExtent <= maxShowOnScreenExtent);

  /// The smallest the floating header can expand to in the main axis direction,
  /// in response to a [RenderObject.showOnScreen] call, in addition to its
  /// [RenderSliverPersistentHeader.minExtent].
  ///
  /// 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 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
  /// allowed for such expansion is either
  /// [RenderSliverPersistentHeader.minExtent] or [minShowOnScreenExtent],
  /// whichever is larger. If the persistent header's current extent is already
  /// larger than that maximum extent, it will remain unchanged.
  ///
  /// This parameter can be set to the persistent header's `maxExtent` (or
  /// `double.infinity`) so the persistent header will always try to expand when
  /// [RenderObject.showOnScreen] is called on it.
  ///
  /// Defaults to [double.negativeInfinity], must be less than or equal to
  /// [maxShowOnScreenExtent]. Has no effect unless the persistent header is a
  /// floating header.
  final double minShowOnScreenExtent;

  /// The biggest the floating header can expand to in the main axis direction,
  /// in response to a [RenderObject.showOnScreen] call, in addition to its
  /// [RenderSliverPersistentHeader.maxExtent].
  ///
  /// 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 90 91 92 93 94 95 96 97 98 99 100 101 102 103
  /// allowed for such expansion is either
  /// [RenderSliverPersistentHeader.maxExtent] or [maxShowOnScreenExtent],
  /// whichever is smaller. If the persistent header's current extent is already
  /// larger than that maximum extent, it will remain unchanged.
  ///
  /// This parameter can be set to the persistent header's `minExtent` (or
  /// `double.negativeInfinity`) so the persistent header will never try to
  /// expand when [RenderObject.showOnScreen] is called on it.
  ///
  /// Defaults to [double.infinity], must be greater than or equal to
  /// [minShowOnScreenExtent]. Has no effect unless the persistent header is a
  /// floating header.
  final double maxShowOnScreenExtent;
}

104 105 106 107 108 109 110 111 112 113 114 115 116 117
/// A base class for slivers that have a [RenderBox] child which scrolls
/// normally, except that when it hits the leading edge (typically the top) of
/// the viewport, it shrinks to a minimum size ([minExtent]).
///
/// This class primarily provides helpers for managing the child, in particular:
///
///  * [layoutChild], which applies min and max extents and a scroll offset to
///    lay out the child. This is normally called from [performLayout].
///
///  * [childExtent], to convert the child's box layout dimensions to the sliver
///    geometry model.
///
///  * hit testing, painting, and other details of the sliver protocol.
///
118 119
/// Subclasses must implement [performLayout], [minExtent], and [maxExtent], and
/// typically also will implement [updateChild].
120
abstract class RenderSliverPersistentHeader extends RenderSliver with RenderObjectWithChildMixin<RenderBox>, RenderSliverHelpers {
121 122 123 124
  /// Creates a sliver that changes its size when scrolled to the start of the
  /// viewport.
  ///
  /// This is an abstract class; this constructor only initializes the [child].
125
  RenderSliverPersistentHeader({
126
    RenderBox? child,
127 128
    this.stretchConfiguration,
  }) {
Ian Hickson's avatar
Ian Hickson committed
129 130 131
    this.child = child;
  }

132
  late double _lastStretchOffset;
133

134 135 136 137
  /// The biggest that this render object can become, in the main axis direction.
  ///
  /// This value should not be based on the child. If it changes, call
  /// [markNeedsLayout].
Ian Hickson's avatar
Ian Hickson committed
138 139
  double get maxExtent;

140
  /// The smallest that this render object can become, in the main axis direction.
Ian Hickson's avatar
Ian Hickson committed
141
  ///
142 143 144 145 146
  /// If this is based on the intrinsic dimensions of the child, the child
  /// should be measured during [updateChild] and the value cached and returned
  /// here. The [updateChild] method will automatically be invoked any time the
  /// child changes its intrinsic dimensions.
  double get minExtent;
Ian Hickson's avatar
Ian Hickson committed
147

148
  /// The dimension of the child in the main axis.
Ian Hickson's avatar
Ian Hickson committed
149 150
  @protected
  double get childExtent {
151
    if (child == null) {
Ian Hickson's avatar
Ian Hickson committed
152
      return 0.0;
153
    }
154
    assert(child!.hasSize);
Ian Hickson's avatar
Ian Hickson committed
155 156 157
    assert(constraints.axis != null);
    switch (constraints.axis) {
      case Axis.vertical:
158
        return child!.size.height;
Ian Hickson's avatar
Ian Hickson committed
159
      case Axis.horizontal:
160
        return child!.size.width;
Ian Hickson's avatar
Ian Hickson committed
161 162 163
    }
  }

164 165 166
  bool _needsUpdateChild = true;
  double _lastShrinkOffset = 0.0;
  bool _lastOverlapsContent = false;
Ian Hickson's avatar
Ian Hickson committed
167

168 169 170 171 172 173 174 175
  /// Defines the parameters used to execute an [AsyncCallback] when a
  /// stretching header over-scrolls.
  ///
  /// If [stretchConfiguration] is null then callback is not triggered.
  ///
  /// See also:
  ///
  ///  * [SliverAppBar], which creates a header that can stretched into an
176
  ///    overscroll area and trigger a callback function.
177
  OverScrollHeaderStretchConfiguration? stretchConfiguration;
178

179
  /// Update the child render object if necessary.
Ian Hickson's avatar
Ian Hickson committed
180
  ///
181 182 183 184 185 186 187 188 189 190 191 192 193
  /// Called before the first layout, any time [markNeedsLayout] is called, and
  /// any time the scroll offset changes. The `shrinkOffset` is the difference
  /// between the [maxExtent] and the current size. Zero means the header is
  /// fully expanded, any greater number up to [maxExtent] means that the header
  /// has been scrolled by that much. The `overlapsContent` argument is true if
  /// the sliver's leading edge is beyond its normal place in the viewport
  /// contents, and false otherwise. It may still paint beyond its normal place
  /// if the [minExtent] after this call is greater than the amount of space that
  /// would normally be left.
  ///
  /// The render object will size itself to the larger of (a) the [maxExtent]
  /// minus the child's intrinsic height and (b) the [maxExtent] minus the
  /// shrink offset.
Ian Hickson's avatar
Ian Hickson committed
194
  ///
195 196
  /// When this method is called by [layoutChild], the [child] can be set,
  /// mutated, or replaced. (It should not be called outside [layoutChild].)
Ian Hickson's avatar
Ian Hickson committed
197
  ///
198
  /// Any time this method would mutate the child, call [markNeedsLayout].
Ian Hickson's avatar
Ian Hickson committed
199
  @protected
200 201 202 203 204 205 206 207
  void updateChild(double shrinkOffset, bool overlapsContent) { }

  @override
  void markNeedsLayout() {
    // This is automatically called whenever the child's intrinsic dimensions
    // change, at which point we should remeasure them during the next layout.
    _needsUpdateChild = true;
    super.markNeedsLayout();
Ian Hickson's avatar
Ian Hickson committed
208 209
  }

210 211 212 213 214 215 216 217 218
  /// Lays out the [child].
  ///
  /// This is called by [performLayout]. It applies the given `scrollOffset`
  /// (which need not match the offset given by the [constraints]) and the
  /// `maxExtent` (which need not match the value returned by the [maxExtent]
  /// getter).
  ///
  /// The `overlapsContent` argument is passed to [updateChild].
  @protected
219
  void layoutChild(double scrollOffset, double maxExtent, { bool overlapsContent = false }) {
Ian Hickson's avatar
Ian Hickson committed
220 221
    assert(maxExtent != null);
    final double shrinkOffset = math.min(scrollOffset, maxExtent);
222
    if (_needsUpdateChild || _lastShrinkOffset != shrinkOffset || _lastOverlapsContent != overlapsContent) {
Ian Hickson's avatar
Ian Hickson committed
223 224
      invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
        assert(constraints == this.constraints);
225
        updateChild(shrinkOffset, overlapsContent);
Ian Hickson's avatar
Ian Hickson committed
226 227
      });
      _lastShrinkOffset = shrinkOffset;
228 229
      _lastOverlapsContent = overlapsContent;
      _needsUpdateChild = false;
Ian Hickson's avatar
Ian Hickson committed
230
    }
231
    assert(minExtent != null);
Ian Hickson's avatar
Ian Hickson committed
232
    assert(() {
233
      if (minExtent <= maxExtent) {
Ian Hickson's avatar
Ian Hickson committed
234
        return true;
235
      }
236 237 238 239 240
      throw FlutterError.fromParts(<DiagnosticsNode>[
        ErrorSummary('The maxExtent for this $runtimeType is less than its minExtent.'),
        DoubleProperty('The specified maxExtent was', maxExtent),
        DoubleProperty('The specified minExtent was', minExtent),
      ]);
241
    }());
242
    double stretchOffset = 0.0;
243
    if (stretchConfiguration != null && constraints.scrollOffset == 0.0) {
244
      stretchOffset += constraints.overlap.abs();
245
    }
246

Ian Hickson's avatar
Ian Hickson committed
247
    child?.layout(
248 249 250
      constraints.asBoxConstraints(
        maxExtent: math.max(minExtent, maxExtent - shrinkOffset) + stretchOffset,
      ),
Ian Hickson's avatar
Ian Hickson committed
251 252
      parentUsesSize: true,
    );
253 254

    if (stretchConfiguration != null &&
255 256 257 258
      stretchConfiguration!.onStretchTrigger != null &&
      stretchOffset >= stretchConfiguration!.stretchTriggerOffset &&
      _lastStretchOffset <= stretchConfiguration!.stretchTriggerOffset) {
      stretchConfiguration!.onStretchTrigger!();
259 260
    }
    _lastStretchOffset = stretchOffset;
Ian Hickson's avatar
Ian Hickson committed
261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279
  }

  /// Returns the distance from the leading _visible_ edge of the sliver to the
  /// side of the child closest to that edge, in the scroll axis direction.
  ///
  /// For example, if the [constraints] describe this sliver as having an axis
  /// direction of [AxisDirection.down], then this is the distance from the top
  /// of the visible portion of the sliver to the top of the child. If the child
  /// is scrolled partially off the top of the viewport, then this will be
  /// negative. On the other hand, if the [constraints] describe this sliver as
  /// having an axis direction of [AxisDirection.up], then this is the distance
  /// from the bottom of the visible portion of the sliver to the bottom of the
  /// child. In both cases, this is the direction of increasing
  /// [SliverConstraints.scrollOffset].
  ///
  /// Calling this when the child is not visible is not valid.
  ///
  /// The argument must be the value of the [child] property.
  ///
280
  /// This must be implemented by [RenderSliverPersistentHeader] subclasses.
Ian Hickson's avatar
Ian Hickson committed
281 282 283
  ///
  /// If there is no child, this should return 0.0.
  @override
284
  double childMainAxisPosition(covariant RenderObject child) => super.childMainAxisPosition(child);
Ian Hickson's avatar
Ian Hickson committed
285 286

  @override
287 288
  bool hitTestChildren(SliverHitTestResult result, { required double mainAxisPosition, required double crossAxisPosition }) {
    assert(geometry!.hitTestExtent > 0.0);
289
    if (child != null) {
290
      return hitTestBoxChild(BoxHitTestResult.wrap(result), child!, mainAxisPosition: mainAxisPosition, crossAxisPosition: crossAxisPosition);
291
    }
Ian Hickson's avatar
Ian Hickson committed
292 293 294 295 296 297 298
    return false;
  }

  @override
  void applyPaintTransform(RenderObject child, Matrix4 transform) {
    assert(child != null);
    assert(child == this.child);
299
    applyPaintTransformForBoxChild(child as RenderBox, transform);
Ian Hickson's avatar
Ian Hickson committed
300 301 302 303
  }

  @override
  void paint(PaintingContext context, Offset offset) {
304
    if (child != null && geometry!.visible) {
Ian Hickson's avatar
Ian Hickson committed
305 306 307
      assert(constraints.axisDirection != null);
      switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
        case AxisDirection.up:
308
          offset += Offset(0.0, geometry!.paintExtent - childMainAxisPosition(child!) - childExtent);
Ian Hickson's avatar
Ian Hickson committed
309 310
          break;
        case AxisDirection.down:
311
          offset += Offset(0.0, childMainAxisPosition(child!));
Ian Hickson's avatar
Ian Hickson committed
312 313
          break;
        case AxisDirection.left:
314
          offset += Offset(geometry!.paintExtent - childMainAxisPosition(child!) - childExtent, 0.0);
Ian Hickson's avatar
Ian Hickson committed
315 316
          break;
        case AxisDirection.right:
317
          offset += Offset(childMainAxisPosition(child!), 0.0);
Ian Hickson's avatar
Ian Hickson committed
318 319
          break;
      }
320
      context.paintChild(child!, offset);
Ian Hickson's avatar
Ian Hickson committed
321 322 323
    }
  }

324
  @override
325 326
  void describeSemanticsConfiguration(SemanticsConfiguration config) {
    super.describeSemanticsConfiguration(config);
327
    config.addTagForChildren(RenderViewport.excludeFromScrolling);
328 329
  }

Ian Hickson's avatar
Ian Hickson committed
330
  @override
331 332
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
333
    properties.add(DoubleProperty.lazy('maxExtent', () => maxExtent));
334
    properties.add(DoubleProperty.lazy('child position', () => childMainAxisPosition(child!)));
Ian Hickson's avatar
Ian Hickson committed
335 336 337 338 339 340 341 342
  }
}

/// A sliver with a [RenderBox] child which scrolls normally, except that when
/// it hits the leading edge (typically the top) of the viewport, it shrinks to
/// a minimum size before continuing to scroll.
///
/// This sliver makes no effort to avoid overlapping other content.
343
abstract class RenderSliverScrollingPersistentHeader extends RenderSliverPersistentHeader {
344 345
  /// Creates a sliver that shrinks when it hits the start of the viewport, then
  /// scrolls off.
346
  RenderSliverScrollingPersistentHeader({
347 348 349
    super.child,
    super.stretchConfiguration,
  });
Ian Hickson's avatar
Ian Hickson committed
350 351 352

  // Distance from our leading edge to the child's leading edge, in the axis
  // direction. Negative if we're scrolled off the top.
353
  double? _childPosition;
Ian Hickson's avatar
Ian Hickson committed
354

355 356 357 358 359 360
  /// Updates [geometry], and returns the new value for [childMainAxisPosition].
  ///
  /// This is used by [performLayout].
  @protected
  double updateGeometry() {
    double stretchOffset = 0.0;
361
    if (stretchConfiguration != null) {
362 363 364 365 366 367 368
      stretchOffset += constraints.overlap.abs();
    }
    final double maxExtent = this.maxExtent;
    final double paintExtent = maxExtent - constraints.scrollOffset;
    geometry = SliverGeometry(
      scrollExtent: maxExtent,
      paintOrigin: math.min(constraints.overlap, 0.0),
369
      paintExtent: clampDouble(paintExtent, 0.0, constraints.remainingPaintExtent),
370 371 372 373 374 375 376
      maxPaintExtent: maxExtent + stretchOffset,
      hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity.
    );
    return stretchOffset > 0 ? 0.0 : math.min(0.0, paintExtent - childExtent);
  }


Ian Hickson's avatar
Ian Hickson committed
377 378
  @override
  void performLayout() {
379
    final SliverConstraints constraints = this.constraints;
Ian Hickson's avatar
Ian Hickson committed
380 381 382
    final double maxExtent = this.maxExtent;
    layoutChild(constraints.scrollOffset, maxExtent);
    final double paintExtent = maxExtent - constraints.scrollOffset;
383
    geometry = SliverGeometry(
Ian Hickson's avatar
Ian Hickson committed
384
      scrollExtent: maxExtent,
385
      paintOrigin: math.min(constraints.overlap, 0.0),
386
      paintExtent: clampDouble(paintExtent, 0.0, constraints.remainingPaintExtent),
Ian Hickson's avatar
Ian Hickson committed
387
      maxPaintExtent: maxExtent,
388
      hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity.
Ian Hickson's avatar
Ian Hickson committed
389
    );
390
    _childPosition = updateGeometry();
Ian Hickson's avatar
Ian Hickson committed
391 392 393
  }

  @override
394
  double childMainAxisPosition(RenderBox child) {
Ian Hickson's avatar
Ian Hickson committed
395
    assert(child == this.child);
396 397
    assert(_childPosition != null);
    return _childPosition!;
Ian Hickson's avatar
Ian Hickson committed
398 399 400 401 402 403 404 405
  }
}

/// A sliver with a [RenderBox] child which never scrolls off the viewport in
/// the positive scroll direction, and which first scrolls on at a full size but
/// then shrinks as the viewport continues to scroll.
///
/// This sliver avoids overlapping other earlier slivers where possible.
406
abstract class RenderSliverPinnedPersistentHeader extends RenderSliverPersistentHeader {
407 408
  /// Creates a sliver that shrinks when it hits the start of the viewport, then
  /// stays pinned there.
409
  RenderSliverPinnedPersistentHeader({
410 411
    super.child,
    super.stretchConfiguration,
412
    this.showOnScreenConfiguration = const PersistentHeaderShowOnScreenConfiguration(),
413
  });
Ian Hickson's avatar
Ian Hickson committed
414

415 416 417 418 419 420
  /// Specifies the persistent header's behavior when `showOnScreen` is called.
  ///
  /// If set to null, the persistent header will delegate the `showOnScreen` call
  /// to it's parent [RenderObject].
  PersistentHeaderShowOnScreenConfiguration? showOnScreenConfiguration;

Ian Hickson's avatar
Ian Hickson committed
421 422
  @override
  void performLayout() {
423
    final SliverConstraints constraints = this.constraints;
Ian Hickson's avatar
Ian Hickson committed
424
    final double maxExtent = this.maxExtent;
425 426
    final bool overlapsContent = constraints.overlap > 0.0;
    layoutChild(constraints.scrollOffset, maxExtent, overlapsContent: overlapsContent);
427
    final double effectiveRemainingPaintExtent = math.max(0, constraints.remainingPaintExtent - constraints.overlap);
428
    final double layoutExtent = clampDouble(maxExtent - constraints.scrollOffset, 0.0, effectiveRemainingPaintExtent);
429 430 431
    final double stretchOffset = stretchConfiguration != null ?
      constraints.overlap.abs() :
      0.0;
432
    geometry = SliverGeometry(
Ian Hickson's avatar
Ian Hickson committed
433
      scrollExtent: maxExtent,
434
      paintOrigin: constraints.overlap,
435
      paintExtent: math.min(childExtent, effectiveRemainingPaintExtent),
436
      layoutExtent: layoutExtent,
437
      maxPaintExtent: maxExtent + stretchOffset,
438
      maxScrollObstructionExtent: minExtent,
439
      cacheExtent: layoutExtent > 0.0 ? -constraints.cacheOrigin + layoutExtent : layoutExtent,
440
      hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity.
Ian Hickson's avatar
Ian Hickson committed
441 442 443 444
    );
  }

  @override
445
  double childMainAxisPosition(RenderBox child) => 0.0;
446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480

  @override
  void showOnScreen({
    RenderObject? descendant,
    Rect? rect,
    Duration duration = Duration.zero,
    Curve curve = Curves.ease,
  }) {
    final Rect? localBounds = descendant != null
      ? MatrixUtils.transformRect(descendant.getTransformTo(this), rect ?? descendant.paintBounds)
      : rect;

    Rect? newRect;
    switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
      case AxisDirection.up:
        newRect = _trim(localBounds, bottom: childExtent);
        break;
      case AxisDirection.right:
        newRect = _trim(localBounds, left: 0);
        break;
      case AxisDirection.down:
        newRect = _trim(localBounds, top: 0);
        break;
      case AxisDirection.left:
        newRect = _trim(localBounds, right: childExtent);
        break;
    }

    super.showOnScreen(
      descendant: this,
      rect: newRect,
      duration: duration,
      curve: curve,
    );
  }
Ian Hickson's avatar
Ian Hickson committed
481 482
}

483 484 485 486 487 488 489 490 491 492 493 494 495 496
/// Specifies how a floating header is to be "snapped" (animated) into or out
/// of view.
///
/// See also:
///
///  * [RenderSliverFloatingPersistentHeader.maybeStartSnapAnimation] and
///    [RenderSliverFloatingPersistentHeader.maybeStopSnapAnimation], which
///    start or stop the floating header's animation.
///  * [SliverAppBar], which creates a header that can be pinned, floating,
///    and snapped into view via the corresponding parameters.
class FloatingHeaderSnapConfiguration {
  /// Creates an object that specifies how a floating header is to be "snapped"
  /// (animated) into or out of view.
  FloatingHeaderSnapConfiguration({
497 498
    this.curve = Curves.ease,
    this.duration = const Duration(milliseconds: 300),
499
  }) : assert(curve != null),
500
       assert(duration != null);
501 502 503 504 505 506 507 508

  /// The snap animation curve.
  final Curve curve;

  /// The snap animation's duration.
  final Duration duration;
}

509 510 511
/// A sliver with a [RenderBox] child which shrinks and scrolls like a
/// [RenderSliverScrollingPersistentHeader], but immediately comes back when the
/// user scrolls in the reverse direction.
512 513 514 515 516
///
/// See also:
///
///  * [RenderSliverFloatingPinnedPersistentHeader], which is similar but sticks
///    to the start of the viewport rather than scrolling off.
517
abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersistentHeader {
518 519 520
  /// Creates a sliver that shrinks when it hits the start of the viewport, then
  /// scrolls off, and comes back immediately when the user reverses the scroll
  /// direction.
521
  RenderSliverFloatingPersistentHeader({
522
    super.child,
523 524
    TickerProvider? vsync,
    this.snapConfiguration,
525
    super.stretchConfiguration,
526
    required this.showOnScreenConfiguration,
527
  }) : _vsync = vsync;
Ian Hickson's avatar
Ian Hickson committed
528

529 530 531 532
  AnimationController? _controller;
  late Animation<double> _animation;
  double? _lastActualScrollOffset;
  double? _effectiveScrollOffset;
533 534 535 536
  // Important for pointer scrolling, which does not have the same concept of
  // a hold and release scroll movement, like dragging.
  // This keeps track of the last ScrollDirection when scrolling started.
  ScrollDirection? _lastStartedScrollDirection;
Ian Hickson's avatar
Ian Hickson committed
537 538 539

  // Distance from our leading edge to the child's leading edge, in the axis
  // direction. Negative if we're scrolled off the top.
540
  double? _childPosition;
Ian Hickson's avatar
Ian Hickson committed
541

542 543 544 545 546 547 548
  @override
  void detach() {
    _controller?.dispose();
    _controller = null; // lazily recreated if we're reattached.
    super.detach();
  }

549 550 551 552 553

  /// A [TickerProvider] to use when animating the scroll position.
  TickerProvider? get vsync => _vsync;
  TickerProvider? _vsync;
  set vsync(TickerProvider? value) {
554
    if (value == _vsync) {
555
      return;
556
    }
557 558 559 560 561 562 563 564 565
    _vsync = value;
    if (value == null) {
      _controller?.dispose();
      _controller = null;
    } else {
      _controller?.resync(value);
    }
  }

566 567 568 569 570 571 572 573 574 575 576 577
  /// Defines the parameters used to snap (animate) the floating header in and
  /// out of view.
  ///
  /// If [snapConfiguration] is null then the floating header does not snap.
  ///
  /// See also:
  ///
  ///  * [RenderSliverFloatingPersistentHeader.maybeStartSnapAnimation] and
  ///    [RenderSliverFloatingPersistentHeader.maybeStopSnapAnimation], which
  ///    start or stop the floating header's animation.
  ///  * [SliverAppBar], which creates a header that can be pinned, floating,
  ///    and snapped into view via the corresponding parameters.
578 579
  FloatingHeaderSnapConfiguration? snapConfiguration;

580
  /// {@macro flutter.rendering.PersistentHeaderShowOnScreenConfiguration}
581 582 583 584
  ///
  /// If set to null, the persistent header will delegate the `showOnScreen` call
  /// to it's parent [RenderObject].
  PersistentHeaderShowOnScreenConfiguration? showOnScreenConfiguration;
585

586 587 588
  /// Updates [geometry], and returns the new value for [childMainAxisPosition].
  ///
  /// This is used by [performLayout].
589 590
  @protected
  double updateGeometry() {
591
    double stretchOffset = 0.0;
592
    if (stretchConfiguration != null) {
593 594
      stretchOffset += constraints.overlap.abs();
    }
595
    final double maxExtent = this.maxExtent;
596
    final double paintExtent = maxExtent - _effectiveScrollOffset!;
597
    final double layoutExtent = maxExtent - constraints.scrollOffset;
598
    geometry = SliverGeometry(
599
      scrollExtent: maxExtent,
600
      paintOrigin: math.min(constraints.overlap, 0.0),
601 602
      paintExtent: clampDouble(paintExtent, 0.0, constraints.remainingPaintExtent),
      layoutExtent: clampDouble(layoutExtent, 0.0, constraints.remainingPaintExtent),
603
      maxPaintExtent: maxExtent + stretchOffset,
604 605
      hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity.
    );
606
    return stretchOffset > 0 ? 0.0 : math.min(0.0, paintExtent - childExtent);
607 608
  }

609 610 611 612 613 614 615 616 617 618 619
  void _updateAnimation(Duration duration, double endValue, Curve curve) {
    assert(duration != null);
    assert(endValue != null);
    assert(curve != null);
    assert(
      vsync != null,
      'vsync must not be null if the floating header changes size animatedly.',
    );

    final AnimationController effectiveController =
      _controller ??= AnimationController(vsync: vsync!, duration: duration)
620
        ..addListener(() {
621
            if (_effectiveScrollOffset == _animation.value) {
622
              return;
623
            }
624 625 626
            _effectiveScrollOffset = _animation.value;
            markNeedsLayout();
          });
627 628 629 630 631 632 633 634 635

    _animation = effectiveController.drive(
      Tween<double>(
        begin: _effectiveScrollOffset,
        end: endValue,
      ).chain(CurveTween(curve: curve)),
    );
  }

636
  /// Update the last known ScrollDirection when scrolling began.
637
  // ignore: use_setters_to_change_properties, (API predates enforcing the lint)
638 639 640 641
  void updateScrollStartDirection(ScrollDirection direction) {
    _lastStartedScrollDirection = direction;
  }

642 643
  /// If the header isn't already fully exposed, then scroll it into view.
  void maybeStartSnapAnimation(ScrollDirection direction) {
644
    final FloatingHeaderSnapConfiguration? snap = snapConfiguration;
645
    if (snap == null) {
646
      return;
647 648
    }
    if (direction == ScrollDirection.forward && _effectiveScrollOffset! <= 0.0) {
649
      return;
650 651
    }
    if (direction == ScrollDirection.reverse && _effectiveScrollOffset! >= maxExtent) {
652
      return;
653
    }
654

655 656 657 658
    _updateAnimation(
      snap.duration,
      direction == ScrollDirection.forward ? 0.0 : maxExtent,
      snap.curve,
659
    );
660
    _controller?.forward(from: 0.0);
661 662
  }

663 664
  /// If a header snap animation or a [showOnScreen] expand animation is underway
  /// then stop it.
665 666 667
  void maybeStopSnapAnimation(ScrollDirection direction) {
    _controller?.stop();
  }
668

Ian Hickson's avatar
Ian Hickson committed
669 670
  @override
  void performLayout() {
671
    final SliverConstraints constraints = this.constraints;
Ian Hickson's avatar
Ian Hickson committed
672 673
    final double maxExtent = this.maxExtent;
    if (_lastActualScrollOffset != null && // We've laid out at least once to get an initial position, and either
674 675 676
        ((constraints.scrollOffset < _lastActualScrollOffset!) || // we are scrolling back, so should reveal, or
         (_effectiveScrollOffset! < maxExtent))) { // some part of it is visible, so should shrink or reveal as appropriate.
      double delta = _lastActualScrollOffset! - constraints.scrollOffset;
677

678 679
      final bool allowFloatingExpansion = constraints.userScrollDirection == ScrollDirection.forward
        || (_lastStartedScrollDirection != null && _lastStartedScrollDirection == ScrollDirection.forward);
Ian Hickson's avatar
Ian Hickson committed
680
      if (allowFloatingExpansion) {
681 682 683 684
        if (_effectiveScrollOffset! > maxExtent) {
          // We're scrolled off-screen, but should reveal, so pretend we're just at the limit.
          _effectiveScrollOffset = maxExtent;
        }
Ian Hickson's avatar
Ian Hickson committed
685
      } else {
686 687 688 689
        if (delta > 0.0) {
          // Disallow the expansion. (But allow shrinking, i.e. delta < 0.0 is fine.)
          delta = 0.0;
        }
Ian Hickson's avatar
Ian Hickson committed
690
      }
691
      _effectiveScrollOffset = clampDouble(_effectiveScrollOffset! - delta, 0.0, constraints.scrollOffset);
Ian Hickson's avatar
Ian Hickson committed
692 693 694
    } else {
      _effectiveScrollOffset = constraints.scrollOffset;
    }
695
    final bool overlapsContent = _effectiveScrollOffset! < constraints.scrollOffset;
696 697

    layoutChild(
698
      _effectiveScrollOffset!,
699 700 701
      maxExtent,
      overlapsContent: overlapsContent,
    );
702
    _childPosition = updateGeometry();
Ian Hickson's avatar
Ian Hickson committed
703 704 705
    _lastActualScrollOffset = constraints.scrollOffset;
  }

706 707 708 709 710 711 712 713
  @override
  void showOnScreen({
    RenderObject? descendant,
    Rect? rect,
    Duration duration = Duration.zero,
    Curve curve = Curves.ease,
  }) {
    final PersistentHeaderShowOnScreenConfiguration? showOnScreen = showOnScreenConfiguration;
714
    if (showOnScreen == null) {
715
      return super.showOnScreen(descendant: descendant, rect: rect, duration: duration, curve: curve);
716
    }
717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752

    assert(child != null || descendant == null);
    // We prefer the child's coordinate space (instead of the sliver's) because
    // it's easier for us to convert the target rect into target extents: when
    // the sliver is sitting above the leading edge (not possible with pinned
    // headers), the leading edge of the sliver and the leading edge of the child
    // will not be aligned. The only exception is when child is null (and thus
    // descendant == null).
    final Rect? childBounds = descendant != null
      ? MatrixUtils.transformRect(descendant.getTransformTo(child), rect ?? descendant.paintBounds)
      : rect;

    double targetExtent;
    Rect? targetRect;
    switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
      case AxisDirection.up:
        targetExtent = childExtent - (childBounds?.top ?? 0);
        targetRect = _trim(childBounds, bottom: childExtent);
        break;
      case AxisDirection.right:
        targetExtent = childBounds?.right ?? childExtent;
        targetRect = _trim(childBounds, left: 0);
        break;
      case AxisDirection.down:
        targetExtent = childBounds?.bottom ?? childExtent;
        targetRect = _trim(childBounds, top: 0);
        break;
      case AxisDirection.left:
        targetExtent = childExtent - (childBounds?.left ?? 0);
        targetRect = _trim(childBounds, right: childExtent);
        break;
    }

    // A stretch header can have a bigger childExtent than maxExtent.
    final double effectiveMaxExtent = math.max(childExtent, maxExtent);

753 754 755 756 757 758 759 760 761 762
    targetExtent = clampDouble(
        clampDouble(
          targetExtent,
          showOnScreen.minShowOnScreenExtent,
          showOnScreen.maxShowOnScreenExtent,
        ),
        // Clamp the value back to the valid range after applying additional
        // constraints. Contracting is not allowed.
        childExtent,
        effectiveMaxExtent);
763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782

    // Expands the header if needed, with animation.
    if (targetExtent > childExtent) {
      final double targetScrollOffset = maxExtent - targetExtent;
      assert(
        vsync != null,
        'vsync must not be null if the floating header changes size animatedly.',
      );
      _updateAnimation(duration, targetScrollOffset, curve);
      _controller?.forward(from: 0.0);
    }

    super.showOnScreen(
      descendant: descendant == null ? this : child,
      rect: targetRect,
      duration: duration,
      curve: curve,
    );
  }

Ian Hickson's avatar
Ian Hickson committed
783
  @override
784
  double childMainAxisPosition(RenderBox child) {
Ian Hickson's avatar
Ian Hickson committed
785
    assert(child == this.child);
786
    return _childPosition ?? 0.0;
Ian Hickson's avatar
Ian Hickson committed
787 788 789
  }

  @override
790 791
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
792
    properties.add(DoubleProperty('effective scroll offset', _effectiveScrollOffset));
Ian Hickson's avatar
Ian Hickson committed
793 794
  }
}
795

796 797 798 799 800 801 802 803
/// A sliver with a [RenderBox] child which shrinks and then remains pinned to
/// the start of the viewport like a [RenderSliverPinnedPersistentHeader], but
/// immediately grows when the user scrolls in the reverse direction.
///
/// See also:
///
///  * [RenderSliverFloatingPersistentHeader], which is similar but scrolls off
///    the top rather than sticking to it.
804
abstract class RenderSliverFloatingPinnedPersistentHeader extends RenderSliverFloatingPersistentHeader {
805 806 807
  /// Creates a sliver that shrinks when it hits the start of the viewport, then
  /// stays pinned there, and grows immediately when the user reverses the
  /// scroll direction.
808
  RenderSliverFloatingPinnedPersistentHeader({
809 810 811 812 813 814
    super.child,
    super.vsync,
    super.snapConfiguration,
    super.stretchConfiguration,
    super.showOnScreenConfiguration,
  });
815 816 817

  @override
  double updateGeometry() {
818
    final double minExtent = this.minExtent;
819 820 821
    final double minAllowedExtent = constraints.remainingPaintExtent > minExtent ?
      minExtent :
      constraints.remainingPaintExtent;
822
    final double maxExtent = this.maxExtent;
823
    final double paintExtent = maxExtent - _effectiveScrollOffset!;
824
    final double clampedPaintExtent = clampDouble(paintExtent,
825 826
      minAllowedExtent,
      constraints.remainingPaintExtent,
827
    );
828
    final double layoutExtent = maxExtent - constraints.scrollOffset;
829 830 831
    final double stretchOffset = stretchConfiguration != null ?
      constraints.overlap.abs() :
      0.0;
832
    geometry = SliverGeometry(
833
      scrollExtent: maxExtent,
834
      paintOrigin: math.min(constraints.overlap, 0.0),
835
      paintExtent: clampedPaintExtent,
836
      layoutExtent: clampDouble(layoutExtent, 0.0, clampedPaintExtent),
837
      maxPaintExtent: maxExtent + stretchOffset,
838
      maxScrollObstructionExtent: minExtent,
839 840 841 842 843
      hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity.
    );
    return 0.0;
  }
}