sliver_persistent_header.dart 21.1 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1 2 3 4 5 6
// Copyright 2016 The Chromium Authors. All rights reserved.
// 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';
Ian Hickson's avatar
Ian Hickson committed
11 12 13
import 'package:vector_math/vector_math_64.dart';

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

21 22 23 24 25 26 27 28 29 30 31 32 33 34
/// 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.
///
35 36
/// Subclasses must implement [performLayout], [minExtent], and [maxExtent], and
/// typically also will implement [updateChild].
37
abstract class RenderSliverPersistentHeader extends RenderSliver with RenderObjectWithChildMixin<RenderBox>, RenderSliverHelpers {
38 39 40 41
  /// 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].
42
  RenderSliverPersistentHeader({ RenderBox child }) {
Ian Hickson's avatar
Ian Hickson committed
43 44 45
    this.child = child;
  }

46 47 48 49
  /// 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
50 51
  double get maxExtent;

52
  /// The smallest that this render object can become, in the main axis direction.
Ian Hickson's avatar
Ian Hickson committed
53
  ///
54 55 56 57 58
  /// 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
59

60
  /// The dimension of the child in the main axis.
Ian Hickson's avatar
Ian Hickson committed
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
  @protected
  double get childExtent {
    if (child == null)
      return 0.0;
    assert(child.hasSize);
    assert(constraints.axis != null);
    switch (constraints.axis) {
      case Axis.vertical:
        return child.size.height;
      case Axis.horizontal:
        return child.size.width;
    }
    return null;
  }

76 77 78
  bool _needsUpdateChild = true;
  double _lastShrinkOffset = 0.0;
  bool _lastOverlapsContent = false;
Ian Hickson's avatar
Ian Hickson committed
79

80
  /// Update the child render object if necessary.
Ian Hickson's avatar
Ian Hickson committed
81
  ///
82 83 84 85 86 87 88 89 90 91 92 93 94
  /// 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
95
  ///
96 97
  /// 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
98
  ///
99
  /// Any time this method would mutate the child, call [markNeedsLayout].
Ian Hickson's avatar
Ian Hickson committed
100
  @protected
101 102 103 104 105 106 107 108
  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
109 110
  }

111 112 113 114 115 116 117 118 119
  /// 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
120
  void layoutChild(double scrollOffset, double maxExtent, { bool overlapsContent: false }) {
Ian Hickson's avatar
Ian Hickson committed
121 122
    assert(maxExtent != null);
    final double shrinkOffset = math.min(scrollOffset, maxExtent);
123
    if (_needsUpdateChild || _lastShrinkOffset != shrinkOffset || _lastOverlapsContent != overlapsContent) {
Ian Hickson's avatar
Ian Hickson committed
124 125
      invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
        assert(constraints == this.constraints);
126
        updateChild(shrinkOffset, overlapsContent);
Ian Hickson's avatar
Ian Hickson committed
127 128
      });
      _lastShrinkOffset = shrinkOffset;
129 130
      _lastOverlapsContent = overlapsContent;
      _needsUpdateChild = false;
Ian Hickson's avatar
Ian Hickson committed
131
    }
132
    assert(minExtent != null);
Ian Hickson's avatar
Ian Hickson committed
133
    assert(() {
134
      if (minExtent <= maxExtent)
Ian Hickson's avatar
Ian Hickson committed
135 136
        return true;
      throw new FlutterError(
137
        'The maxExtent for this $runtimeType is less than its minExtent.\n'
Ian Hickson's avatar
Ian Hickson committed
138
        'The specified maxExtent was: ${maxExtent.toStringAsFixed(1)}\n'
139
        'The specified minExtent was: ${minExtent.toStringAsFixed(1)}\n'
Ian Hickson's avatar
Ian Hickson committed
140
      );
141
    }());
Ian Hickson's avatar
Ian Hickson committed
142
    child?.layout(
143
      constraints.asBoxConstraints(maxExtent: math.max(minExtent, maxExtent - shrinkOffset)),
Ian Hickson's avatar
Ian Hickson committed
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
      parentUsesSize: true,
    );
  }

  /// 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.
  ///
165
  /// This must be implemented by [RenderSliverPersistentHeader] subclasses.
Ian Hickson's avatar
Ian Hickson committed
166 167 168
  ///
  /// If there is no child, this should return 0.0.
  @override
169
  double childMainAxisPosition(covariant RenderObject child) => super.childMainAxisPosition(child);
Ian Hickson's avatar
Ian Hickson committed
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191

  @override
  bool hitTestChildren(HitTestResult result, { @required double mainAxisPosition, @required double crossAxisPosition }) {
    assert(geometry.hitTestExtent > 0.0);
    if (child != null)
      return hitTestBoxChild(result, child, mainAxisPosition: mainAxisPosition, crossAxisPosition: crossAxisPosition);
    return false;
  }

  @override
  void applyPaintTransform(RenderObject child, Matrix4 transform) {
    assert(child != null);
    assert(child == this.child);
    applyPaintTransformForBoxChild(child, transform);
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    if (child != null && geometry.visible) {
      assert(constraints.axisDirection != null);
      switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
        case AxisDirection.up:
192
          offset += new Offset(0.0, geometry.paintExtent - childMainAxisPosition(child) - childExtent);
Ian Hickson's avatar
Ian Hickson committed
193 194
          break;
        case AxisDirection.down:
195
          offset += new Offset(0.0, childMainAxisPosition(child));
Ian Hickson's avatar
Ian Hickson committed
196 197
          break;
        case AxisDirection.left:
198
          offset += new Offset(geometry.paintExtent - childMainAxisPosition(child) - childExtent, 0.0);
Ian Hickson's avatar
Ian Hickson committed
199 200
          break;
        case AxisDirection.right:
201
          offset += new Offset(childMainAxisPosition(child), 0.0);
Ian Hickson's avatar
Ian Hickson committed
202 203 204 205 206 207
          break;
      }
      context.paintChild(child, offset);
    }
  }

208 209 210 211
  /// Whether the [SemanticsNode]s associated with this [RenderSliver] should
  /// be excluded from the semantic scrolling area.
  ///
  /// [RenderSliver]s that stay on the screen even though the user has scrolled
212
  /// past them (e.g. a pinned app bar) should set this to true.
213 214 215 216 217 218 219 220 221 222 223 224 225 226
  @protected
  bool get excludeFromSemanticsScrolling => _excludeFromSemanticsScrolling;
  bool _excludeFromSemanticsScrolling = false;
  set excludeFromSemanticsScrolling(bool value) {
    if (_excludeFromSemanticsScrolling == value)
      return;
    _excludeFromSemanticsScrolling = value;
    markNeedsSemanticsUpdate();
  }

  @override
  SemanticsAnnotator get semanticsAnnotator => _excludeFromSemanticsScrolling ? _annotate : null;

  void _annotate(SemanticsNode node) {
227
    node.addTag(RenderSemanticsGestureHandler.excludeFromScrolling);
228 229
  }

Ian Hickson's avatar
Ian Hickson committed
230
  @override
231
  void debugFillProperties(DiagnosticPropertiesBuilder description) {
232 233 234
    super.debugFillProperties(description);
    description.add(new DoubleProperty.lazy('maxExtent', () => maxExtent));
    description.add(new DoubleProperty.lazy('child position', () => childMainAxisPosition(child)));
Ian Hickson's avatar
Ian Hickson committed
235 236 237 238 239 240 241 242
  }
}

/// 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.
243
abstract class RenderSliverScrollingPersistentHeader extends RenderSliverPersistentHeader {
244 245
  /// Creates a sliver that shrinks when it hits the start of the viewport, then
  /// scrolls off.
246
  RenderSliverScrollingPersistentHeader({
Ian Hickson's avatar
Ian Hickson committed
247 248 249 250 251 252 253 254 255 256 257 258 259 260
    RenderBox child,
  }) : super(child: child);

  // Distance from our leading edge to the child's leading edge, in the axis
  // direction. Negative if we're scrolled off the top.
  double _childPosition;

  @override
  void performLayout() {
    final double maxExtent = this.maxExtent;
    layoutChild(constraints.scrollOffset, maxExtent);
    final double paintExtent = maxExtent - constraints.scrollOffset;
    geometry = new SliverGeometry(
      scrollExtent: maxExtent,
261
      paintOrigin: math.min(constraints.overlap, 0.0),
Ian Hickson's avatar
Ian Hickson committed
262 263
      paintExtent: paintExtent.clamp(0.0, constraints.remainingPaintExtent),
      maxPaintExtent: maxExtent,
264
      hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity.
Ian Hickson's avatar
Ian Hickson committed
265 266 267 268 269
    );
    _childPosition = math.min(0.0, paintExtent - childExtent);
  }

  @override
270
  double childMainAxisPosition(RenderBox child) {
Ian Hickson's avatar
Ian Hickson committed
271 272 273 274 275 276 277 278 279 280
    assert(child == this.child);
    return _childPosition;
  }
}

/// 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.
281
abstract class RenderSliverPinnedPersistentHeader extends RenderSliverPersistentHeader {
282 283
  /// Creates a sliver that shrinks when it hits the start of the viewport, then
  /// stays pinned there.
284
  RenderSliverPinnedPersistentHeader({
Ian Hickson's avatar
Ian Hickson committed
285 286 287 288 289 290
    RenderBox child,
  }) : super(child: child);

  @override
  void performLayout() {
    final double maxExtent = this.maxExtent;
291 292 293
    final bool overlapsContent = constraints.overlap > 0.0;
    excludeFromSemanticsScrolling = overlapsContent || (constraints.scrollOffset > maxExtent - minExtent);
    layoutChild(constraints.scrollOffset, maxExtent, overlapsContent: overlapsContent);
Ian Hickson's avatar
Ian Hickson committed
294 295
    geometry = new SliverGeometry(
      scrollExtent: maxExtent,
296 297
      paintOrigin: constraints.overlap,
      paintExtent: math.min(childExtent, constraints.remainingPaintExtent),
Ian Hickson's avatar
Ian Hickson committed
298
      layoutExtent: (maxExtent - constraints.scrollOffset).clamp(0.0, constraints.remainingPaintExtent),
299
      maxPaintExtent: maxExtent,
300
      maxScrollObstructionExtent: minExtent,
301
      hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity.
Ian Hickson's avatar
Ian Hickson committed
302 303 304 305
    );
  }

  @override
306
  double childMainAxisPosition(RenderBox child) => 0.0;
Ian Hickson's avatar
Ian Hickson committed
307 308
}

309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325
/// 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({
    @required this.vsync,
    this.curve: Curves.ease,
    this.duration: const Duration(milliseconds: 300),
326 327 328
  }) : assert(vsync != null),
       assert(curve != null),
       assert(duration != null);
329 330 331 332 333 334 335 336 337 338 339 340

  /// The [TickerProvider] for the [AnimationController] that causes a
  /// floating header to snap in or out of view.
  final TickerProvider vsync;

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

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

341 342 343
/// 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.
344 345 346 347 348
///
/// See also:
///
///  * [RenderSliverFloatingPinnedPersistentHeader], which is similar but sticks
///    to the start of the viewport rather than scrolling off.
349
abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersistentHeader {
350 351 352
  /// 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.
353
  RenderSliverFloatingPersistentHeader({
Ian Hickson's avatar
Ian Hickson committed
354
    RenderBox child,
355 356
    FloatingHeaderSnapConfiguration snapConfiguration,
  }) : _snapConfiguration = snapConfiguration, super(child: child);
Ian Hickson's avatar
Ian Hickson committed
357

358 359
  AnimationController _controller;
  Animation<double> _animation;
Ian Hickson's avatar
Ian Hickson committed
360 361 362 363 364 365 366
  double _lastActualScrollOffset;
  double _effectiveScrollOffset;

  // Distance from our leading edge to the child's leading edge, in the axis
  // direction. Negative if we're scrolled off the top.
  double _childPosition;

367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399
  @override
  void detach() {
    _controller?.dispose();
    _controller = null; // lazily recreated if we're reattached.
    super.detach();
  }

  /// 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.
  FloatingHeaderSnapConfiguration get snapConfiguration => _snapConfiguration;
  FloatingHeaderSnapConfiguration _snapConfiguration;
  set snapConfiguration(FloatingHeaderSnapConfiguration value) {
    if (value == _snapConfiguration)
      return;
    if (value == null) {
      _controller?.dispose();
    } else {
      if (_snapConfiguration != null && value.vsync != _snapConfiguration.vsync)
        _controller?.resync(value.vsync);
    }
    _snapConfiguration = value;
  }

400 401 402
  /// Updates [geometry], and returns the new value for [childMainAxisPosition].
  ///
  /// This is used by [performLayout].
403 404 405 406 407 408 409
  @protected
  double updateGeometry() {
    final double maxExtent = this.maxExtent;
    final double paintExtent = maxExtent - _effectiveScrollOffset;
    final double layoutExtent = maxExtent - constraints.scrollOffset;
    geometry = new SliverGeometry(
      scrollExtent: maxExtent,
410
      paintOrigin: math.min(constraints.overlap, 0.0),
411 412 413
      paintExtent: paintExtent.clamp(0.0, constraints.remainingPaintExtent),
      layoutExtent: layoutExtent.clamp(0.0, constraints.remainingPaintExtent),
      maxPaintExtent: maxExtent,
414
      maxScrollObstructionExtent: maxExtent,
415 416 417 418 419
      hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity.
    );
    return math.min(0.0, paintExtent - childExtent);
  }

420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455
  /// If the header isn't already fully exposed, then scroll it into view.
  void maybeStartSnapAnimation(ScrollDirection direction) {
    if (snapConfiguration == null)
      return;
    if (direction == ScrollDirection.forward && _effectiveScrollOffset <= 0.0)
      return;
    if (direction == ScrollDirection.reverse && _effectiveScrollOffset >= maxExtent)
      return;

    final TickerProvider vsync = snapConfiguration.vsync;
    final Duration duration = snapConfiguration.duration;
    _controller ??= new AnimationController(vsync: vsync, duration: duration)
      ..addListener(() {
        if (_effectiveScrollOffset == _animation.value)
          return;
        _effectiveScrollOffset = _animation.value;
        markNeedsLayout();
      });

    // Recreating the animation rather than updating a cached value, only
    // to avoid the extra complexity of managing the animation's lifetime.
    _animation = new Tween<double>(
      begin: _effectiveScrollOffset,
      end: direction == ScrollDirection.forward ? 0.0 : maxExtent,
    ).animate(new CurvedAnimation(
      parent: _controller,
      curve: snapConfiguration.curve,
    ));

    _controller.forward(from: 0.0);
  }

  /// If a header snap animation is underway then stop it.
  void maybeStopSnapAnimation(ScrollDirection direction) {
    _controller?.stop();
  }
456

Ian Hickson's avatar
Ian Hickson committed
457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475
  @override
  void performLayout() {
    final double maxExtent = this.maxExtent;
    if (_lastActualScrollOffset != null && // We've laid out at least once to get an initial position, and either
        ((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;
      final bool allowFloatingExpansion = constraints.userScrollDirection == ScrollDirection.forward;
      if (allowFloatingExpansion) {
        if (_effectiveScrollOffset > maxExtent) // We're scrolled off-screen, but should reveal, so
          _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.)
      }
      _effectiveScrollOffset = (_effectiveScrollOffset - delta).clamp(0.0, constraints.scrollOffset);
    } else {
      _effectiveScrollOffset = constraints.scrollOffset;
    }
476 477 478
    final bool overlapsContent = _effectiveScrollOffset < constraints.scrollOffset;
    excludeFromSemanticsScrolling = overlapsContent;
    layoutChild(_effectiveScrollOffset, maxExtent, overlapsContent: overlapsContent);
479
    _childPosition = updateGeometry();
Ian Hickson's avatar
Ian Hickson committed
480 481 482 483
    _lastActualScrollOffset = constraints.scrollOffset;
  }

  @override
484
  double childMainAxisPosition(RenderBox child) {
Ian Hickson's avatar
Ian Hickson committed
485 486 487 488 489
    assert(child == this.child);
    return _childPosition;
  }

  @override
490
  void debugFillProperties(DiagnosticPropertiesBuilder description) {
491 492
    super.debugFillProperties(description);
    description.add(new DoubleProperty('effective scroll offset', _effectiveScrollOffset));
Ian Hickson's avatar
Ian Hickson committed
493 494
  }
}
495

496 497 498 499 500 501 502 503
/// 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.
504
abstract class RenderSliverFloatingPinnedPersistentHeader extends RenderSliverFloatingPersistentHeader {
505 506 507
  /// 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.
508 509
  RenderSliverFloatingPinnedPersistentHeader({
    RenderBox child,
510 511
    FloatingHeaderSnapConfiguration snapConfiguration,
  }) : super(child: child, snapConfiguration: snapConfiguration);
512 513 514

  @override
  double updateGeometry() {
515
    final double minExtent = this.minExtent;
516 517 518 519 520 521 522 523
    final double maxExtent = this.maxExtent;
    final double paintExtent = (maxExtent - _effectiveScrollOffset);
    final double layoutExtent = (maxExtent - constraints.scrollOffset);
    geometry = new SliverGeometry(
      scrollExtent: maxExtent,
      paintExtent: paintExtent.clamp(minExtent, constraints.remainingPaintExtent),
      layoutExtent: layoutExtent.clamp(0.0, constraints.remainingPaintExtent - minExtent),
      maxPaintExtent: maxExtent,
524
      maxScrollObstructionExtent: maxExtent,
525 526 527 528 529
      hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity.
    );
    return 0.0;
  }
}