sliver_persistent_header.dart 31.6 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
class OverScrollHeaderStretchConfiguration {
  /// Creates an object that specifies how a stretched header may activate an
  /// [AsyncCallback].
  OverScrollHeaderStretchConfiguration({
    this.stretchTriggerOffset = 100.0,
    this.onStretchTrigger,
40
  });
41 42 43 44 45 46

  /// 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
    switch (constraints.axis) {
      case Axis.vertical:
157
        return child!.size.height;
Ian Hickson's avatar
Ian Hickson committed
158
      case Axis.horizontal:
159
        return child!.size.width;
Ian Hickson's avatar
Ian Hickson committed
160 161 162
    }
  }

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

167 168 169 170 171 172 173 174
  /// 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
175
  ///    overscroll area and trigger a callback function.
176
  OverScrollHeaderStretchConfiguration? stretchConfiguration;
177

178
  /// Update the child render object if necessary.
Ian Hickson's avatar
Ian Hickson committed
179
  ///
180 181 182 183 184 185 186 187 188 189 190 191 192
  /// 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
193
  ///
194 195
  /// 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
196
  ///
197
  /// Any time this method would mutate the child, call [markNeedsLayout].
Ian Hickson's avatar
Ian Hickson committed
198
  @protected
199 200 201 202 203 204 205 206
  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
207 208
  }

209 210 211 212 213 214 215 216 217
  /// 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
218
  void layoutChild(double scrollOffset, double maxExtent, { bool overlapsContent = false }) {
Ian Hickson's avatar
Ian Hickson committed
219
    final double shrinkOffset = math.min(scrollOffset, maxExtent);
220
    if (_needsUpdateChild || _lastShrinkOffset != shrinkOffset || _lastOverlapsContent != overlapsContent) {
Ian Hickson's avatar
Ian Hickson committed
221 222
      invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
        assert(constraints == this.constraints);
223
        updateChild(shrinkOffset, overlapsContent);
Ian Hickson's avatar
Ian Hickson committed
224 225
      });
      _lastShrinkOffset = shrinkOffset;
226 227
      _lastOverlapsContent = overlapsContent;
      _needsUpdateChild = false;
Ian Hickson's avatar
Ian Hickson committed
228 229
    }
    assert(() {
230
      if (minExtent <= maxExtent) {
Ian Hickson's avatar
Ian Hickson committed
231
        return true;
232
      }
233 234 235 236 237
      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),
      ]);
238
    }());
239
    double stretchOffset = 0.0;
240
    if (stretchConfiguration != null && constraints.scrollOffset == 0.0) {
241
      stretchOffset += constraints.overlap.abs();
242
    }
243

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

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

  /// 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.
  ///
277
  /// This must be implemented by [RenderSliverPersistentHeader] subclasses.
Ian Hickson's avatar
Ian Hickson committed
278 279 280
  ///
  /// If there is no child, this should return 0.0.
  @override
281
  double childMainAxisPosition(covariant RenderObject child) => super.childMainAxisPosition(child);
Ian Hickson's avatar
Ian Hickson committed
282 283

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

  @override
  void applyPaintTransform(RenderObject child, Matrix4 transform) {
    assert(child == this.child);
295
    applyPaintTransformForBoxChild(child as RenderBox, transform);
Ian Hickson's avatar
Ian Hickson committed
296 297 298 299
  }

  @override
  void paint(PaintingContext context, Offset offset) {
300
    if (child != null && geometry!.visible) {
Ian Hickson's avatar
Ian Hickson committed
301 302
      switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
        case AxisDirection.up:
303
          offset += Offset(0.0, geometry!.paintExtent - childMainAxisPosition(child!) - childExtent);
Ian Hickson's avatar
Ian Hickson committed
304
        case AxisDirection.down:
305
          offset += Offset(0.0, childMainAxisPosition(child!));
Ian Hickson's avatar
Ian Hickson committed
306
        case AxisDirection.left:
307
          offset += Offset(geometry!.paintExtent - childMainAxisPosition(child!) - childExtent, 0.0);
Ian Hickson's avatar
Ian Hickson committed
308
        case AxisDirection.right:
309
          offset += Offset(childMainAxisPosition(child!), 0.0);
Ian Hickson's avatar
Ian Hickson committed
310
      }
311
      context.paintChild(child!, offset);
Ian Hickson's avatar
Ian Hickson committed
312 313 314
    }
  }

315
  @override
316 317
  void describeSemanticsConfiguration(SemanticsConfiguration config) {
    super.describeSemanticsConfiguration(config);
318
    config.addTagForChildren(RenderViewport.excludeFromScrolling);
319 320
  }

Ian Hickson's avatar
Ian Hickson committed
321
  @override
322 323
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
324
    properties.add(DoubleProperty.lazy('maxExtent', () => maxExtent));
325
    properties.add(DoubleProperty.lazy('child position', () => childMainAxisPosition(child!)));
Ian Hickson's avatar
Ian Hickson committed
326 327 328 329 330 331 332 333
  }
}

/// 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.
334
abstract class RenderSliverScrollingPersistentHeader extends RenderSliverPersistentHeader {
335 336
  /// Creates a sliver that shrinks when it hits the start of the viewport, then
  /// scrolls off.
337
  RenderSliverScrollingPersistentHeader({
338 339 340
    super.child,
    super.stretchConfiguration,
  });
Ian Hickson's avatar
Ian Hickson committed
341 342 343

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

346 347 348 349 350 351
  /// Updates [geometry], and returns the new value for [childMainAxisPosition].
  ///
  /// This is used by [performLayout].
  @protected
  double updateGeometry() {
    double stretchOffset = 0.0;
352
    if (stretchConfiguration != null) {
353 354 355 356 357 358 359
      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),
360
      paintExtent: clampDouble(paintExtent, 0.0, constraints.remainingPaintExtent),
361 362 363 364 365 366 367
      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
368 369
  @override
  void performLayout() {
370
    final SliverConstraints constraints = this.constraints;
Ian Hickson's avatar
Ian Hickson committed
371 372 373
    final double maxExtent = this.maxExtent;
    layoutChild(constraints.scrollOffset, maxExtent);
    final double paintExtent = maxExtent - constraints.scrollOffset;
374
    geometry = SliverGeometry(
Ian Hickson's avatar
Ian Hickson committed
375
      scrollExtent: maxExtent,
376
      paintOrigin: math.min(constraints.overlap, 0.0),
377
      paintExtent: clampDouble(paintExtent, 0.0, constraints.remainingPaintExtent),
Ian Hickson's avatar
Ian Hickson committed
378
      maxPaintExtent: maxExtent,
379
      hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity.
Ian Hickson's avatar
Ian Hickson committed
380
    );
381
    _childPosition = updateGeometry();
Ian Hickson's avatar
Ian Hickson committed
382 383 384
  }

  @override
385
  double childMainAxisPosition(RenderBox child) {
Ian Hickson's avatar
Ian Hickson committed
386
    assert(child == this.child);
387 388
    assert(_childPosition != null);
    return _childPosition!;
Ian Hickson's avatar
Ian Hickson committed
389 390 391 392 393 394 395 396
  }
}

/// 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.
397
abstract class RenderSliverPinnedPersistentHeader extends RenderSliverPersistentHeader {
398 399
  /// Creates a sliver that shrinks when it hits the start of the viewport, then
  /// stays pinned there.
400
  RenderSliverPinnedPersistentHeader({
401 402
    super.child,
    super.stretchConfiguration,
403
    this.showOnScreenConfiguration = const PersistentHeaderShowOnScreenConfiguration(),
404
  });
Ian Hickson's avatar
Ian Hickson committed
405

406 407 408 409 410 411
  /// 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
412 413
  @override
  void performLayout() {
414
    final SliverConstraints constraints = this.constraints;
Ian Hickson's avatar
Ian Hickson committed
415
    final double maxExtent = this.maxExtent;
416 417
    final bool overlapsContent = constraints.overlap > 0.0;
    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 421 422
    final double stretchOffset = stretchConfiguration != null ?
      constraints.overlap.abs() :
      0.0;
423
    geometry = SliverGeometry(
Ian Hickson's avatar
Ian Hickson committed
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.
Ian Hickson's avatar
Ian Hickson committed
432 433 434 435
    );
  }

  @override
436
  double childMainAxisPosition(RenderBox child) => 0.0;
437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467

  @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);
      case AxisDirection.right:
        newRect = _trim(localBounds, left: 0);
      case AxisDirection.down:
        newRect = _trim(localBounds, top: 0);
      case AxisDirection.left:
        newRect = _trim(localBounds, right: childExtent);
    }

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

470 471 472 473 474 475 476 477 478 479 480 481 482 483
/// 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({
484 485
    this.curve = Curves.ease,
    this.duration = const Duration(milliseconds: 300),
486
  });
487 488 489 490 491 492 493 494

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

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

495 496 497
/// 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.
498 499 500 501 502
///
/// See also:
///
///  * [RenderSliverFloatingPinnedPersistentHeader], which is similar but sticks
///    to the start of the viewport rather than scrolling off.
503
abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersistentHeader {
504 505 506
  /// 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.
507
  RenderSliverFloatingPersistentHeader({
508
    super.child,
509 510
    TickerProvider? vsync,
    this.snapConfiguration,
511
    super.stretchConfiguration,
512
    required this.showOnScreenConfiguration,
513
  }) : _vsync = vsync;
Ian Hickson's avatar
Ian Hickson committed
514

515 516 517 518
  AnimationController? _controller;
  late Animation<double> _animation;
  double? _lastActualScrollOffset;
  double? _effectiveScrollOffset;
519 520 521 522
  // 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
523 524 525

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

528 529 530 531 532 533 534
  @override
  void detach() {
    _controller?.dispose();
    _controller = null; // lazily recreated if we're reattached.
    super.detach();
  }

535 536 537 538 539

  /// A [TickerProvider] to use when animating the scroll position.
  TickerProvider? get vsync => _vsync;
  TickerProvider? _vsync;
  set vsync(TickerProvider? value) {
540
    if (value == _vsync) {
541
      return;
542
    }
543 544 545 546 547 548 549 550 551
    _vsync = value;
    if (value == null) {
      _controller?.dispose();
      _controller = null;
    } else {
      _controller?.resync(value);
    }
  }

552 553 554 555 556 557 558 559 560 561 562 563
  /// 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.
564 565
  FloatingHeaderSnapConfiguration? snapConfiguration;

566
  /// {@macro flutter.rendering.PersistentHeaderShowOnScreenConfiguration}
567 568 569 570
  ///
  /// If set to null, the persistent header will delegate the `showOnScreen` call
  /// to it's parent [RenderObject].
  PersistentHeaderShowOnScreenConfiguration? showOnScreenConfiguration;
571

572 573 574
  /// Updates [geometry], and returns the new value for [childMainAxisPosition].
  ///
  /// This is used by [performLayout].
575 576
  @protected
  double updateGeometry() {
577
    double stretchOffset = 0.0;
578
    if (stretchConfiguration != null) {
579 580
      stretchOffset += constraints.overlap.abs();
    }
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 588
      paintExtent: clampDouble(paintExtent, 0.0, constraints.remainingPaintExtent),
      layoutExtent: clampDouble(layoutExtent, 0.0, constraints.remainingPaintExtent),
589
      maxPaintExtent: maxExtent + stretchOffset,
590 591
      hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity.
    );
592
    return stretchOffset > 0 ? 0.0 : math.min(0.0, paintExtent - childExtent);
593 594
  }

595 596 597 598 599 600 601 602
  void _updateAnimation(Duration duration, double endValue, Curve curve) {
    assert(
      vsync != null,
      'vsync must not be null if the floating header changes size animatedly.',
    );

    final AnimationController effectiveController =
      _controller ??= AnimationController(vsync: vsync!, duration: duration)
603
        ..addListener(() {
604
            if (_effectiveScrollOffset == _animation.value) {
605
              return;
606
            }
607 608 609
            _effectiveScrollOffset = _animation.value;
            markNeedsLayout();
          });
610 611 612 613 614 615 616 617 618

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

619
  /// Update the last known ScrollDirection when scrolling began.
620
  // ignore: use_setters_to_change_properties, (API predates enforcing the lint)
621 622 623 624
  void updateScrollStartDirection(ScrollDirection direction) {
    _lastStartedScrollDirection = direction;
  }

625 626
  /// If the header isn't already fully exposed, then scroll it into view.
  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 639 640 641
    _updateAnimation(
      snap.duration,
      direction == ScrollDirection.forward ? 0.0 : maxExtent,
      snap.curve,
642
    );
643
    _controller?.forward(from: 0.0);
644 645
  }

646 647
  /// If a header snap animation or a [showOnScreen] expand animation is underway
  /// then stop it.
648 649 650
  void maybeStopSnapAnimation(ScrollDirection direction) {
    _controller?.stop();
  }
651

Ian Hickson's avatar
Ian Hickson committed
652 653
  @override
  void performLayout() {
654
    final SliverConstraints constraints = this.constraints;
Ian Hickson's avatar
Ian Hickson committed
655 656
    final double maxExtent = this.maxExtent;
    if (_lastActualScrollOffset != null && // We've laid out at least once to get an initial position, and either
657 658 659
        ((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;
660

661 662
      final bool allowFloatingExpansion = constraints.userScrollDirection == ScrollDirection.forward
        || (_lastStartedScrollDirection != null && _lastStartedScrollDirection == ScrollDirection.forward);
Ian Hickson's avatar
Ian Hickson committed
663
      if (allowFloatingExpansion) {
664 665 666 667
        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
668
      } else {
669 670 671 672
        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
673
      }
674
      _effectiveScrollOffset = clampDouble(_effectiveScrollOffset! - delta, 0.0, constraints.scrollOffset);
Ian Hickson's avatar
Ian Hickson committed
675 676 677
    } else {
      _effectiveScrollOffset = constraints.scrollOffset;
    }
678
    final bool overlapsContent = _effectiveScrollOffset! < constraints.scrollOffset;
679 680

    layoutChild(
681
      _effectiveScrollOffset!,
682 683 684
      maxExtent,
      overlapsContent: overlapsContent,
    );
685
    _childPosition = updateGeometry();
Ian Hickson's avatar
Ian Hickson committed
686 687 688
    _lastActualScrollOffset = constraints.scrollOffset;
  }

689 690 691 692 693 694 695 696
  @override
  void showOnScreen({
    RenderObject? descendant,
    Rect? rect,
    Duration duration = Duration.zero,
    Curve curve = Curves.ease,
  }) {
    final PersistentHeaderShowOnScreenConfiguration? showOnScreen = showOnScreenConfiguration;
697
    if (showOnScreen == null) {
698
      return super.showOnScreen(descendant: descendant, rect: rect, duration: duration, curve: curve);
699
    }
700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731

    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);
      case AxisDirection.right:
        targetExtent = childBounds?.right ?? childExtent;
        targetRect = _trim(childBounds, left: 0);
      case AxisDirection.down:
        targetExtent = childBounds?.bottom ?? childExtent;
        targetRect = _trim(childBounds, top: 0);
      case AxisDirection.left:
        targetExtent = childExtent - (childBounds?.left ?? 0);
        targetRect = _trim(childBounds, right: childExtent);
    }

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

732 733 734 735 736 737 738 739 740 741
    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);
742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761

    // 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
762
  @override
763
  double childMainAxisPosition(RenderBox child) {
Ian Hickson's avatar
Ian Hickson committed
764
    assert(child == this.child);
765
    return _childPosition ?? 0.0;
Ian Hickson's avatar
Ian Hickson committed
766 767 768
  }

  @override
769 770
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
771
    properties.add(DoubleProperty('effective scroll offset', _effectiveScrollOffset));
Ian Hickson's avatar
Ian Hickson committed
772 773
  }
}
774

775 776 777 778 779 780 781 782
/// 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.
783
abstract class RenderSliverFloatingPinnedPersistentHeader extends RenderSliverFloatingPersistentHeader {
784 785 786
  /// 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.
787
  RenderSliverFloatingPinnedPersistentHeader({
788 789 790 791 792 793
    super.child,
    super.vsync,
    super.snapConfiguration,
    super.stretchConfiguration,
    super.showOnScreenConfiguration,
  });
794 795 796

  @override
  double updateGeometry() {
797
    final double minExtent = this.minExtent;
798 799 800
    final double minAllowedExtent = constraints.remainingPaintExtent > minExtent ?
      minExtent :
      constraints.remainingPaintExtent;
801
    final double maxExtent = this.maxExtent;
802
    final double paintExtent = maxExtent - _effectiveScrollOffset!;
803
    final double clampedPaintExtent = clampDouble(paintExtent,
804 805
      minAllowedExtent,
      constraints.remainingPaintExtent,
806
    );
807
    final double layoutExtent = maxExtent - constraints.scrollOffset;
808 809 810
    final double stretchOffset = stretchConfiguration != null ?
      constraints.overlap.abs() :
      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 819 820 821 822
      hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity.
    );
    return 0.0;
  }
}