sliver_persistent_header.dart 32.5 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 9
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
10
import 'package:flutter/scheduler.dart';
11
import 'package:flutter/semantics.dart';
Ian Hickson's avatar
Ian Hickson committed
12 13
import 'package:vector_math/vector_math_64.dart';

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

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

29 30 31 32 33
/// Specifies how a stretched header is to trigger an [AsyncCallback].
///
/// See also:
///
///  * [SliverAppBar], which creates a header that can be stretched into an
34
///    overscroll area and trigger a callback function.
35 36 37 38 39 40 41 42 43 44 45 46 47
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].
48
  final AsyncCallback? onStretchTrigger;
49 50
}

51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
/// {@template flutter.rendering.persistentHeader.showOnScreenConfiguration}
/// 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
  /// may expand itself to accomodate the [Rect]. The minimum extent that is
  /// 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
  /// may expand itself to accomodate the [Rect]. The maximum extent that is
  /// 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;
}

105 106 107 108 109 110 111 112 113 114 115 116 117 118
/// 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.
///
119 120
/// Subclasses must implement [performLayout], [minExtent], and [maxExtent], and
/// typically also will implement [updateChild].
121
abstract class RenderSliverPersistentHeader extends RenderSliver with RenderObjectWithChildMixin<RenderBox>, RenderSliverHelpers {
122 123 124 125
  /// 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].
126
  RenderSliverPersistentHeader({
127
    RenderBox? child,
128 129
    this.stretchConfiguration,
  }) {
Ian Hickson's avatar
Ian Hickson committed
130 131 132
    this.child = child;
  }

133
  late double _lastStretchOffset;
134

135 136 137 138
  /// 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
139 140
  double get maxExtent;

141
  /// The smallest that this render object can become, in the main axis direction.
Ian Hickson's avatar
Ian Hickson committed
142
  ///
143 144 145 146 147
  /// 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
148

149
  /// The dimension of the child in the main axis.
Ian Hickson's avatar
Ian Hickson committed
150 151 152 153
  @protected
  double get childExtent {
    if (child == null)
      return 0.0;
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
      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),
      ]);
240
    }());
241
    double stretchOffset = 0.0;
242
    if (stretchConfiguration != null && constraints.scrollOffset == 0.0) {
243
      stretchOffset += constraints.overlap.abs();
244
    }
245

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

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

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

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

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

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

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

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

/// 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.
341
abstract class RenderSliverScrollingPersistentHeader extends RenderSliverPersistentHeader {
342 343
  /// Creates a sliver that shrinks when it hits the start of the viewport, then
  /// scrolls off.
344
  RenderSliverScrollingPersistentHeader({
345 346
    RenderBox? child,
    OverScrollHeaderStretchConfiguration? stretchConfiguration,
347 348 349 350
  }) : super(
    child: child,
    stretchConfiguration: stretchConfiguration,
  );
Ian Hickson's avatar
Ian Hickson committed
351 352 353

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

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

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

/// 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.
407
abstract class RenderSliverPinnedPersistentHeader extends RenderSliverPersistentHeader {
408 409
  /// Creates a sliver that shrinks when it hits the start of the viewport, then
  /// stays pinned there.
410
  RenderSliverPinnedPersistentHeader({
411 412
    RenderBox? child,
    OverScrollHeaderStretchConfiguration? stretchConfiguration,
413
    this.showOnScreenConfiguration = const PersistentHeaderShowOnScreenConfiguration(),
414 415 416 417
  }) : super(
    child: child,
    stretchConfiguration: stretchConfiguration,
  );
Ian Hickson's avatar
Ian Hickson committed
418

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

  @override
449
  double childMainAxisPosition(RenderBox child) => 0.0;
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 481 482 483 484

  @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
485 486
}

487 488 489 490 491 492 493 494 495 496 497 498 499 500
/// 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({
501 502 503 504 505
    @Deprecated(
      'Specify SliverPersistentHeaderDelegate.vsync instead. '
      'This feature was deprecated after v1.19.0.'
    )
    this.vsync,
506 507
    this.curve = Curves.ease,
    this.duration = const Duration(milliseconds: 300),
508
  }) : assert(curve != null),
509
       assert(duration != null);
510

511 512 513 514 515 516 517
  /// The [TickerProvider] for the [AnimationController] that causes a floating
  /// header to snap in or out of view.
  @Deprecated(
    'Specify SliverPersistentHeaderDelegate.vsync instead. '
    'This feature was deprecated after v1.19.0.'
  )
  final TickerProvider? vsync;
518 519 520 521 522 523 524 525

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

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

526 527 528
/// 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.
529 530 531 532 533
///
/// See also:
///
///  * [RenderSliverFloatingPinnedPersistentHeader], which is similar but sticks
///    to the start of the viewport rather than scrolling off.
534
abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersistentHeader {
535 536 537
  /// 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.
538
  RenderSliverFloatingPersistentHeader({
539
    RenderBox? child,
540 541
    TickerProvider? vsync,
    this.snapConfiguration,
542
    OverScrollHeaderStretchConfiguration? stretchConfiguration,
543 544
    required this.showOnScreenConfiguration,
  }) : _vsync = vsync,
545
       super(
546 547 548
    child: child,
    stretchConfiguration: stretchConfiguration,
  );
Ian Hickson's avatar
Ian Hickson committed
549

550 551 552 553
  AnimationController? _controller;
  late Animation<double> _animation;
  double? _lastActualScrollOffset;
  double? _effectiveScrollOffset;
Ian Hickson's avatar
Ian Hickson committed
554 555 556

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

559 560 561 562 563 564 565
  @override
  void detach() {
    _controller?.dispose();
    _controller = null; // lazily recreated if we're reattached.
    super.detach();
  }

566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581

  /// A [TickerProvider] to use when animating the scroll position.
  TickerProvider? get vsync => _vsync;
  TickerProvider? _vsync;
  set vsync(TickerProvider? value) {
    if (value == _vsync)
      return;
    _vsync = value;
    if (value == null) {
      _controller?.dispose();
      _controller = null;
    } else {
      _controller?.resync(value);
    }
  }

582 583 584 585 586 587 588 589 590 591 592 593
  /// 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.
594 595 596 597 598 599 600
  FloatingHeaderSnapConfiguration? snapConfiguration;

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

602 603 604
  /// Updates [geometry], and returns the new value for [childMainAxisPosition].
  ///
  /// This is used by [performLayout].
605 606
  @protected
  double updateGeometry() {
607 608 609 610
    double stretchOffset = 0.0;
    if (stretchConfiguration != null && _childPosition == 0.0) {
      stretchOffset += constraints.overlap.abs();
    }
611
    final double maxExtent = this.maxExtent;
612
    final double paintExtent = maxExtent - _effectiveScrollOffset!;
613
    final double layoutExtent = maxExtent - constraints.scrollOffset;
614
    geometry = SliverGeometry(
615
      scrollExtent: maxExtent,
616
      paintOrigin: math.min(constraints.overlap, 0.0),
617 618
      paintExtent: paintExtent.clamp(0.0, constraints.remainingPaintExtent),
      layoutExtent: layoutExtent.clamp(0.0, constraints.remainingPaintExtent),
619
      maxPaintExtent: maxExtent + stretchOffset,
620 621
      hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity.
    );
622
    return stretchOffset > 0 ? 0.0 : math.min(0.0, paintExtent - childExtent);
623 624
  }

625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650
  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)
                        ..addListener(() {
      if (_effectiveScrollOffset == _animation.value)
        return;
      _effectiveScrollOffset = _animation.value;
      markNeedsLayout();
    });

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

651 652
  /// If the header isn't already fully exposed, then scroll it into view.
  void maybeStartSnapAnimation(ScrollDirection direction) {
653 654
    final FloatingHeaderSnapConfiguration? snap = snapConfiguration;
    if (snap == null)
655
      return;
656
    if (direction == ScrollDirection.forward && _effectiveScrollOffset! <= 0.0)
657
      return;
658
    if (direction == ScrollDirection.reverse && _effectiveScrollOffset! >= maxExtent)
659 660
      return;

661 662 663 664
    _updateAnimation(
      snap.duration,
      direction == ScrollDirection.forward ? 0.0 : maxExtent,
      snap.curve,
665
    );
666
    _controller?.forward(from: 0.0);
667 668
  }

669 670
  /// If a header snap animation or a [showOnScreen] expand animation is underway
  /// then stop it.
671 672 673
  void maybeStopSnapAnimation(ScrollDirection direction) {
    _controller?.stop();
  }
674

Ian Hickson's avatar
Ian Hickson committed
675 676
  @override
  void performLayout() {
677
    final SliverConstraints constraints = this.constraints;
Ian Hickson's avatar
Ian Hickson committed
678 679
    final double maxExtent = this.maxExtent;
    if (_lastActualScrollOffset != null && // We've laid out at least once to get an initial position, and either
680 681 682
        ((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;
683

Ian Hickson's avatar
Ian Hickson committed
684 685
      final bool allowFloatingExpansion = constraints.userScrollDirection == ScrollDirection.forward;
      if (allowFloatingExpansion) {
686
        if (_effectiveScrollOffset! > maxExtent) // We're scrolled off-screen, but should reveal, so
Ian Hickson's avatar
Ian Hickson committed
687 688 689 690 691
          _effectiveScrollOffset = maxExtent; // pretend we're just at the limit.
      } else {
        if (delta > 0.0) // If we are trying to expand when allowFloatingExpansion is false,
          delta = 0.0; // disallow the expansion. (But allow shrinking, i.e. delta < 0.0 is fine.)
      }
692
      _effectiveScrollOffset = (_effectiveScrollOffset! - delta).clamp(0.0, constraints.scrollOffset);
Ian Hickson's avatar
Ian Hickson committed
693 694 695
    } else {
      _effectiveScrollOffset = constraints.scrollOffset;
    }
696
    final bool overlapsContent = _effectiveScrollOffset! < constraints.scrollOffset;
697 698

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

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 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779
  @override
  void showOnScreen({
    RenderObject? descendant,
    Rect? rect,
    Duration duration = Duration.zero,
    Curve curve = Curves.ease,
  }) {
    final PersistentHeaderShowOnScreenConfiguration? showOnScreen = showOnScreenConfiguration;
    if (showOnScreen == null)
      return super.showOnScreen(descendant: descendant, rect: rect, duration: duration, curve: curve);

    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);

    targetExtent = targetExtent.clamp(
        showOnScreen.minShowOnScreenExtent,
        showOnScreen.maxShowOnScreenExtent,
      )
      // Clamp the value back to the valid range after applying additional
      // constriants. Contracting is not allowed.
      .clamp(childExtent, effectiveMaxExtent);

    // 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
780
  @override
781
  double childMainAxisPosition(RenderBox child) {
Ian Hickson's avatar
Ian Hickson committed
782
    assert(child == this.child);
783
    return _childPosition ?? 0.0;
Ian Hickson's avatar
Ian Hickson committed
784 785 786
  }

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

793 794 795 796 797 798 799 800
/// 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.
801
abstract class RenderSliverFloatingPinnedPersistentHeader extends RenderSliverFloatingPersistentHeader {
802 803 804
  /// 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.
805
  RenderSliverFloatingPinnedPersistentHeader({
806
    RenderBox? child,
807
    TickerProvider? vsync,
808 809
    FloatingHeaderSnapConfiguration? snapConfiguration,
    OverScrollHeaderStretchConfiguration? stretchConfiguration,
810
    PersistentHeaderShowOnScreenConfiguration? showOnScreenConfiguration,
811 812
  }) : super(
    child: child,
813
    vsync: vsync,
814 815
    snapConfiguration: snapConfiguration,
    stretchConfiguration: stretchConfiguration,
816
    showOnScreenConfiguration: showOnScreenConfiguration,
817
  );
818 819 820

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