nested_scroll_view.dart 71.1 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
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/foundation.dart';
8 9 10 11 12 13 14 15
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';

import 'basic.dart';
import 'framework.dart';
import 'primary_scroll_controller.dart';
import 'scroll_activity.dart';
16
import 'scroll_configuration.dart';
17 18 19 20 21 22
import 'scroll_context.dart';
import 'scroll_controller.dart';
import 'scroll_metrics.dart';
import 'scroll_physics.dart';
import 'scroll_position.dart';
import 'scroll_view.dart';
23
import 'sliver_fill.dart';
24 25
import 'viewport.dart';

26
/// Signature used by [NestedScrollView] for building its header.
27 28 29 30 31
///
/// The `innerBoxIsScrolled` argument is typically used to control the
/// [SliverAppBar.forceElevated] property to ensure that the app bar shows a
/// shadow, since it would otherwise not necessarily be aware that it had
/// content ostensibly below it.
32
typedef NestedScrollViewHeaderSliversBuilder = List<Widget> Function(BuildContext context, bool innerBoxIsScrolled);
33

34 35 36 37
/// A scrolling view inside of which can be nested other scrolling views, with
/// their scroll positions being intrinsically linked.
///
/// The most common use case for this widget is a scrollable view with a
38
/// flexible [SliverAppBar] containing a [TabBar] in the header (built by
39 40 41 42 43 44 45 46 47 48
/// [headerSliverBuilder], and with a [TabBarView] in the [body], such that the
/// scrollable view's contents vary based on which tab is visible.
///
/// ## Motivation
///
/// In a normal [ScrollView], there is one set of slivers (the components of the
/// scrolling view). If one of those slivers hosted a [TabBarView] which scrolls
/// in the opposite direction (e.g. allowing the user to swipe horizontally
/// between the pages represented by the tabs, while the list scrolls
/// vertically), then any list inside that [TabBarView] would not interact with
49
/// the outer [ScrollView]. For example, flinging the inner list to scroll to
50 51 52 53 54 55 56 57
/// the top would not cause a collapsed [SliverAppBar] in the outer [ScrollView]
/// to expand.
///
/// [NestedScrollView] solves this problem by providing custom
/// [ScrollController]s for the outer [ScrollView] and the inner [ScrollView]s
/// (those inside the [TabBarView], hooking them together so that they appear,
/// to the user, as one coherent scroll view.
///
58
/// {@tool sample}
59 60 61 62 63 64 65 66
/// This example shows a [NestedScrollView] whose header is the combination of a
/// [TabBar] in a [SliverAppBar] and whose body is a [TabBarView]. It uses a
/// [SliverOverlapAbsorber]/[SliverOverlapInjector] pair to make the inner lists
/// align correctly, and it uses [SafeArea] to avoid any horizontal disturbances
/// (e.g. the "notch" on iOS when the phone is horizontal). In addition,
/// [PageStorageKey]s are used to remember the scroll position of each tab's
/// list.
///
67
/// ** See code in examples/api/lib/widgets/nested_scroll_view/nested_scroll_view.0.dart **
68
/// {@end-tool}
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
///
/// ## [SliverAppBar]s with [NestedScrollView]s
///
/// Using a [SliverAppBar] in the outer scroll view, or [headerSliverBuilder],
/// of a [NestedScrollView] may require special configurations in order to work
/// as it would if the outer and inner were one single scroll view, like a
/// [CustomScrollView].
///
/// ### Pinned [SliverAppBar]s
///
/// A pinned [SliverAppBar] works in a [NestedScrollView] exactly as it would in
/// another scroll view, like [CustomScrollView]. When using
/// [SliverAppBar.pinned], the app bar remains visible at the top of the scroll
/// view. The app bar can still expand and contract as the user scrolls, but it
/// will remain visible rather than being scrolled out of view.
///
85
/// This works naturally in a [NestedScrollView], as the pinned [SliverAppBar]
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112
/// is not expected to move in or out of the visible portion of the viewport.
/// As the inner or outer [Scrollable]s are moved, the app bar persists as
/// expected.
///
/// If the app bar is floating, pinned, and using an expanded height, follow the
/// floating convention laid out below.
///
/// ### Floating [SliverAppBar]s
///
/// When placed in the outer scrollable, or the [headerSliverBuilder],
/// a [SliverAppBar] that floats, using [SliverAppBar.floating] will not be
/// triggered to float over the inner scroll view, or [body], automatically.
///
/// This is because a floating app bar uses the scroll offset of its own
/// [Scrollable] to dictate the floating action. Being two separate inner and
/// outer [Scrollable]s, a [SliverAppBar] in the outer header is not aware of
/// changes in the scroll offset of the inner body.
///
/// In order to float the outer, use [NestedScrollView.floatHeaderSlivers]. When
/// set to true, the nested scrolling coordinator will prioritize floating in
/// the header slivers before applying the remaining drag to the body.
///
/// Furthermore, the `floatHeaderSlivers` flag should also be used when using an
/// app bar that is floating, pinned, and has an expanded height. In this
/// configuration, the flexible space of the app bar will open and collapse,
/// while the primary portion of the app bar remains pinned.
///
113
/// {@tool sample}
114 115 116 117 118
/// This simple example shows a [NestedScrollView] whose header contains a
/// floating [SliverAppBar]. By using the [floatHeaderSlivers] property, the
/// floating behavior is coordinated between the outer and inner [Scrollable]s,
/// so it behaves as it would in a single scrollable.
///
119
/// ** See code in examples/api/lib/widgets/nested_scroll_view/nested_scroll_view.1.dart **
120 121 122 123
/// {@end-tool}
///
/// ### Snapping [SliverAppBar]s
///
124
/// Floating [SliverAppBar]s also have the option to perform a snapping animation.
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
/// If [SliverAppBar.snap] is true, then a scroll that exposes the floating app
/// bar will trigger an animation that slides the entire app bar into view.
/// Similarly if a scroll dismisses the app bar, the animation will slide the
/// app bar completely out of view.
///
/// It is possible with a [NestedScrollView] to perform just the snapping
/// animation without floating the app bar in and out. By not using the
/// [NestedScrollView.floatHeaderSlivers], the app bar will snap in and out
/// without floating.
///
/// The [SliverAppBar.snap] animation should be used in conjunction with the
/// [SliverOverlapAbsorber] and  [SliverOverlapInjector] widgets when
/// implemented in a [NestedScrollView]. These widgets take any overlapping
/// behavior of the [SliverAppBar] in the header and redirect it to the
/// [SliverOverlapInjector] in the body. If it is missing, then it is possible
/// for the nested "inner" scroll view below to end up under the [SliverAppBar]
/// even when the inner scroll view thinks it has not been scrolled.
///
143
/// {@tool sample}
144 145
/// This simple example shows a [NestedScrollView] whose header contains a
/// snapping, floating [SliverAppBar]. _Without_ setting any additional flags,
146 147 148 149
/// e.g [NestedScrollView.floatHeaderSlivers], the [SliverAppBar] will animate
/// in and out without floating. The [SliverOverlapAbsorber] and
/// [SliverOverlapInjector] maintain the proper alignment between the two
/// separate scroll views.
150
///
151
/// ** See code in examples/api/lib/widgets/nested_scroll_view/nested_scroll_view.2.dart **
152 153 154 155 156 157 158 159 160 161 162
/// {@end-tool}
///
/// ### Snapping and Floating [SliverAppBar]s
///
// See https://github.com/flutter/flutter/issues/59189
/// Currently, [NestedScrollView] does not support simultaneously floating and
/// snapping the outer scrollable, e.g. when using [SliverAppBar.floating] &
/// [SliverAppBar.snap] at the same time.
///
/// ### Stretching [SliverAppBar]s
///
163
// See https://github.com/flutter/flutter/issues/54059
164 165 166 167 168 169 170 171 172 173 174
/// Currently, [NestedScrollView] does not support stretching the outer
/// scrollable, e.g. when using [SliverAppBar.stretch].
///
/// See also:
///
///  * [SliverAppBar], for examples on different configurations like floating,
///    pinned and snap behaviors.
///  * [SliverOverlapAbsorber], a sliver that wraps another, forcing its layout
///    extent to be treated as overlap.
///  * [SliverOverlapInjector], a sliver that has a sliver geometry based on
///    the values stored in a [SliverOverlapAbsorberHandle].
175
class NestedScrollView extends StatefulWidget {
176 177
  /// Creates a nested scroll view.
  ///
178 179
  /// The [reverse], [headerSliverBuilder], and [body] arguments must not be
  /// null.
180
  const NestedScrollView({
181
    Key? key,
182
    this.controller,
183 184
    this.scrollDirection = Axis.vertical,
    this.reverse = false,
185
    this.physics,
186 187
    required this.headerSliverBuilder,
    required this.body,
188
    this.dragStartBehavior = DragStartBehavior.start,
189
    this.floatHeaderSlivers = false,
190
    this.clipBehavior = Clip.hardEdge,
191
    this.restorationId,
192
    this.scrollBehavior,
193 194 195 196
  }) : assert(scrollDirection != null),
       assert(reverse != null),
       assert(headerSliverBuilder != null),
       assert(body != null),
197
       assert(floatHeaderSlivers != null),
198
       assert(clipBehavior != null),
199
       super(key: key);
200

201 202
  /// An object that can be used to control the position to which the outer
  /// scroll view is scrolled.
203
  final ScrollController? controller;
204

205 206 207
  /// The axis along which the scroll view scrolls.
  ///
  /// Defaults to [Axis.vertical].
208 209
  final Axis scrollDirection;

210 211 212 213 214 215 216 217 218 219 220 221
  /// Whether the scroll view scrolls in the reading direction.
  ///
  /// For example, if the reading direction is left-to-right and
  /// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from
  /// left to right when [reverse] is false and from right to left when
  /// [reverse] is true.
  ///
  /// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view
  /// scrolls from top to bottom when [reverse] is false and from bottom to top
  /// when [reverse] is true.
  ///
  /// Defaults to false.
222 223
  final bool reverse;

224 225 226
  /// How the scroll view should respond to user input.
  ///
  /// For example, determines how the scroll view continues to animate after the
227 228 229
  /// user stops dragging the scroll view (providing a custom implementation of
  /// [ScrollPhysics.createBallisticSimulation] allows this particular aspect of
  /// the physics to be overridden).
230
  ///
231 232 233 234
  /// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the
  /// [ScrollPhysics] provided by that behavior will take precedence after
  /// [physics].
  ///
235
  /// Defaults to matching platform conventions.
236 237 238 239 240 241 242
  ///
  /// The [ScrollPhysics.applyBoundaryConditions] implementation of the provided
  /// object should not allow scrolling outside the scroll extent range
  /// described by the [ScrollMetrics.minScrollExtent] and
  /// [ScrollMetrics.maxScrollExtent] properties passed to that method. If that
  /// invariant is not maintained, the nested scroll view may respond to user
  /// scrolling erratically.
243
  final ScrollPhysics? physics;
244

245 246 247 248
  /// A builder for any widgets that are to precede the inner scroll views (as
  /// given by [body]).
  ///
  /// Typically this is used to create a [SliverAppBar] with a [TabBar].
249
  final NestedScrollViewHeaderSliversBuilder headerSliverBuilder;
250

251 252 253 254 255
  /// The widget to show inside the [NestedScrollView].
  ///
  /// Typically this will be [TabBarView].
  ///
  /// The [body] is built in a context that provides a [PrimaryScrollController]
256 257 258 259 260
  /// that interacts with the [NestedScrollView]'s scroll controller. Any
  /// [ListView] or other [Scrollable]-based widget inside the [body] that is
  /// intended to scroll with the [NestedScrollView] should therefore not be
  /// given an explicit [ScrollController], instead allowing it to default to
  /// the [PrimaryScrollController] provided by the [NestedScrollView].
261 262
  final Widget body;

263 264 265
  /// {@macro flutter.widgets.scrollable.dragStartBehavior}
  final DragStartBehavior dragStartBehavior;

266 267 268 269 270 271 272
  /// Whether or not the [NestedScrollView]'s coordinator should prioritize the
  /// outer scrollable over the inner when scrolling back.
  ///
  /// This is useful for an outer scrollable containing a [SliverAppBar] that
  /// is expected to float. This cannot be null.
  final bool floatHeaderSlivers;

273
  /// {@macro flutter.material.Material.clipBehavior}
274 275 276 277
  ///
  /// Defaults to [Clip.hardEdge].
  final Clip clipBehavior;

278
  /// {@macro flutter.widgets.scrollable.restorationId}
279
  final String? restorationId;
280

281 282 283 284 285 286 287 288 289 290 291 292 293 294
  /// {@macro flutter.widgets.shadow.scrollBehavior}
  ///
  /// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit
  /// [ScrollPhysics] is provided in [physics], it will take precedence,
  /// followed by [scrollBehavior], and then the inherited ancestor
  /// [ScrollBehavior].
  ///
  /// The [ScrollBehavior] of the inherited [ScrollConfiguration] will be
  /// modified by default to not apply a [Scrollbar]. This is because the
  /// NestedScrollView cannot assume the configuration of the outer and inner
  /// [Scrollable] widgets, particularly whether to treat them as one scrollable,
  /// or separate and desirous of unique behaviors.
  final ScrollBehavior? scrollBehavior;

295 296 297 298 299 300 301 302
  /// Returns the [SliverOverlapAbsorberHandle] of the nearest ancestor
  /// [NestedScrollView].
  ///
  /// This is necessary to configure the [SliverOverlapAbsorber] and
  /// [SliverOverlapInjector] widgets.
  ///
  /// For sample code showing how to use this method, see the [NestedScrollView]
  /// documentation.
303
  static SliverOverlapAbsorberHandle sliverOverlapAbsorberHandleFor(BuildContext context) {
304
    final _InheritedNestedScrollView? target = context.dependOnInheritedWidgetOfExactType<_InheritedNestedScrollView>();
305 306 307 308
    assert(
      target != null,
      'NestedScrollView.sliverOverlapAbsorberHandleFor must be called with a context that contains a NestedScrollView.',
    );
309
    return target!.state._absorberHandle;
310 311
  }

312
  List<Widget> _buildSlivers(BuildContext context, ScrollController innerController, bool bodyIsScrolled) {
313 314 315 316 317 318 319
    return <Widget>[
      ...headerSliverBuilder(context, bodyIsScrolled),
      SliverFillRemaining(
        child: PrimaryScrollController(
          controller: innerController,
          child: body,
        ),
320
      ),
321
    ];
322 323 324
  }

  @override
325
  NestedScrollViewState createState() => NestedScrollViewState();
326 327
}

328 329 330 331 332 333 334 335 336 337
/// The [State] for a [NestedScrollView].
///
/// The [ScrollController]s, [innerController] and [outerController], of the
/// [NestedScrollView]'s children may be accessed through its state. This is
/// useful for obtaining respective scroll positions in the [NestedScrollView].
///
/// If you want to access the inner or outer scroll controller of a
/// [NestedScrollView], you can get its [NestedScrollViewState] by supplying a
/// `GlobalKey<NestedScrollViewState>` to the [NestedScrollView.key] parameter).
///
338
/// {@tool dartpad}
339 340 341 342
/// [NestedScrollViewState] can be obtained using a [GlobalKey].
/// Using the following setup, you can access the inner scroll controller
/// using `globalKey.currentState.innerController`.
///
343
/// ** See code in examples/api/lib/widgets/nested_scroll_view/nested_scroll_view_state.0.dart **
344 345
/// {@end-tool}
class NestedScrollViewState extends State<NestedScrollView> {
346
  final SliverOverlapAbsorberHandle _absorberHandle = SliverOverlapAbsorberHandle();
347

348 349 350 351 352 353 354 355 356 357 358
  /// The [ScrollController] provided to the [ScrollView] in
  /// [NestedScrollView.body].
  ///
  /// Manipulating the [ScrollPosition] of this controller pushes the outer
  /// header sliver(s) up and out of view. The position of the [outerController]
  /// will be set to [ScrollPosition.maxScrollExtent], unless you use
  /// [ScrollPosition.setPixels].
  ///
  /// See also:
  ///
  ///  * [outerController], which exposes the [ScrollController] used by the
Pierre-Louis's avatar
Pierre-Louis committed
359
  ///    sliver(s) contained in [NestedScrollView.headerSliverBuilder].
360
  ScrollController get innerController => _coordinator!._innerController;
361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376

  /// The [ScrollController] provided to the [ScrollView] in
  /// [NestedScrollView.headerSliverBuilder].
  ///
  /// This is equivalent to [NestedScrollView.controller], if provided.
  ///
  /// Manipulating the [ScrollPosition] of this controller pushes the inner body
  /// sliver(s) down. The position of the [innerController] will be set to
  /// [ScrollPosition.minScrollExtent], unless you use
  /// [ScrollPosition.setPixels]. Visually, the inner body will be scrolled to
  /// its beginning.
  ///
  /// See also:
  ///
  ///  * [innerController], which exposes the [ScrollController] used by the
  ///    [ScrollView] contained in [NestedScrollView.body].
377
  ScrollController get outerController => _coordinator!._outerController;
378

379
  _NestedScrollCoordinator? _coordinator;
380 381 382 383

  @override
  void initState() {
    super.initState();
384
    _coordinator = _NestedScrollCoordinator(
385 386
      this,
      widget.controller,
387
      _handleHasScrolledBodyChanged,
388
      widget.floatHeaderSlivers,
389
    );
390 391 392 393 394
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
395
    _coordinator!.setParent(widget.controller);
396 397 398 399 400 401
  }

  @override
  void didUpdateWidget(NestedScrollView oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.controller != widget.controller)
402
      _coordinator!.setParent(widget.controller);
403 404 405 406
  }

  @override
  void dispose() {
407
    _coordinator!.dispose();
408 409 410 411
    _coordinator = null;
    super.dispose();
  }

412
  bool? _lastHasScrolledBody;
413

414 415 416
  void _handleHasScrolledBodyChanged() {
    if (!mounted)
      return;
417
    final bool newHasScrolledBody = _coordinator!.hasScrolledBody;
418 419 420 421 422 423 424 425 426
    if (_lastHasScrolledBody != newHasScrolledBody) {
      setState(() {
        // _coordinator.hasScrolledBody changed (we use it in the build method)
        // (We record _lastHasScrolledBody in the build() method, rather than in
        // this setState call, because the build() method may be called more
        // often than just from here, and we want to only call setState when the
        // new value is different than the last built value.)
      });
    }
427 428
  }

429 430
  @override
  Widget build(BuildContext context) {
431 432 433 434
    final ScrollPhysics _scrollPhysics = widget.physics?.applyTo(const ClampingScrollPhysics())
      ?? widget.scrollBehavior?.getScrollPhysics(context).applyTo(const ClampingScrollPhysics())
      ?? const ClampingScrollPhysics();

435
    return _InheritedNestedScrollView(
436
      state: this,
437
      child: Builder(
438
        builder: (BuildContext context) {
439
          _lastHasScrolledBody = _coordinator!.hasScrolledBody;
440
          return _NestedScrollViewCustomScrollView(
441
            dragStartBehavior: widget.dragStartBehavior,
442 443
            scrollDirection: widget.scrollDirection,
            reverse: widget.reverse,
444 445
            physics: _scrollPhysics,
            scrollBehavior: widget.scrollBehavior ?? ScrollConfiguration.of(context).copyWith(scrollbars: false),
446
            controller: _coordinator!._outerController,
447 448
            slivers: widget._buildSlivers(
              context,
449 450
              _coordinator!._innerController,
              _lastHasScrolledBody!,
451 452
            ),
            handle: _absorberHandle,
453
            clipBehavior: widget.clipBehavior,
454
            restorationId: widget.restorationId,
455 456 457
          );
        },
      ),
458 459 460 461
    );
  }
}

462
class _NestedScrollViewCustomScrollView extends CustomScrollView {
463
  const _NestedScrollViewCustomScrollView({
464 465 466
    required Axis scrollDirection,
    required bool reverse,
    required ScrollPhysics physics,
467
    required ScrollBehavior scrollBehavior,
468 469 470 471
    required ScrollController controller,
    required List<Widget> slivers,
    required this.handle,
    required Clip clipBehavior,
472
    DragStartBehavior dragStartBehavior = DragStartBehavior.start,
473
    String? restorationId,
474 475 476 477
  }) : super(
         scrollDirection: scrollDirection,
         reverse: reverse,
         physics: physics,
478
         scrollBehavior: scrollBehavior,
479 480
         controller: controller,
         slivers: slivers,
481
         dragStartBehavior: dragStartBehavior,
482
         restorationId: restorationId,
483
         clipBehavior: clipBehavior,
484 485 486 487 488 489 490 491 492 493 494 495
       );

  final SliverOverlapAbsorberHandle handle;

  @override
  Widget buildViewport(
    BuildContext context,
    ViewportOffset offset,
    AxisDirection axisDirection,
    List<Widget> slivers,
  ) {
    assert(!shrinkWrap);
496
    return NestedScrollViewViewport(
497 498 499 500
      axisDirection: axisDirection,
      offset: offset,
      slivers: slivers,
      handle: handle,
501
      clipBehavior: clipBehavior,
502 503 504 505 506 507
    );
  }
}

class _InheritedNestedScrollView extends InheritedWidget {
  const _InheritedNestedScrollView({
508 509 510
    Key? key,
    required this.state,
    required Widget child,
511 512 513 514
  }) : assert(state != null),
       assert(child != null),
       super(key: key, child: child);

515
  final NestedScrollViewState state;
516 517 518 519 520

  @override
  bool updateShouldNotify(_InheritedNestedScrollView old) => state != old.state;
}

521 522
class _NestedScrollMetrics extends FixedScrollMetrics {
  _NestedScrollMetrics({
523 524 525 526
    required double? minScrollExtent,
    required double? maxScrollExtent,
    required double? pixels,
    required double? viewportDimension,
527 528 529 530
    required AxisDirection axisDirection,
    required this.minRange,
    required this.maxRange,
    required this.correctionOffset,
531 532 533 534 535 536 537 538
  }) : super(
    minScrollExtent: minScrollExtent,
    maxScrollExtent: maxScrollExtent,
    pixels: pixels,
    viewportDimension: viewportDimension,
    axisDirection: axisDirection,
  );

539 540
  @override
  _NestedScrollMetrics copyWith({
541 542 543 544 545 546 547 548
    double? minScrollExtent,
    double? maxScrollExtent,
    double? pixels,
    double? viewportDimension,
    AxisDirection? axisDirection,
    double? minRange,
    double? maxRange,
    double? correctionOffset,
549
  }) {
550
    return _NestedScrollMetrics(
551 552 553 554
      minScrollExtent: minScrollExtent ?? (hasContentDimensions ? this.minScrollExtent : null),
      maxScrollExtent: maxScrollExtent ?? (hasContentDimensions ? this.maxScrollExtent : null),
      pixels: pixels ?? (hasPixels ? this.pixels : null),
      viewportDimension: viewportDimension ?? (hasViewportDimension ? this.viewportDimension : null),
555 556 557 558 559 560 561
      axisDirection: axisDirection ?? this.axisDirection,
      minRange: minRange ?? this.minRange,
      maxRange: maxRange ?? this.maxRange,
      correctionOffset: correctionOffset ?? this.correctionOffset,
    );
  }

562 563 564 565 566 567 568
  final double minRange;

  final double maxRange;

  final double correctionOffset;
}

569
typedef _NestedScrollActivityGetter = ScrollActivity Function(_NestedScrollPosition position);
570

571
class _NestedScrollCoordinator implements ScrollActivityDelegate, ScrollHoldController {
572 573 574 575 576 577
  _NestedScrollCoordinator(
    this._state,
    this._parent,
    this._onHasScrolledBodyChanged,
    this._floatHeaderSlivers,
  ) {
578
    final double initialScrollOffset = _parent?.initialScrollOffset ?? 0.0;
579 580 581 582 583 584 585 586 587
    _outerController = _NestedScrollController(
      this,
      initialScrollOffset: initialScrollOffset,
      debugLabel: 'outer',
    );
    _innerController = _NestedScrollController(
      this,
      debugLabel: 'inner',
    );
588 589
  }

590
  final NestedScrollViewState _state;
591
  ScrollController? _parent;
592
  final VoidCallback _onHasScrolledBodyChanged;
593
  final bool _floatHeaderSlivers;
594

595 596
  late _NestedScrollController _outerController;
  late _NestedScrollController _innerController;
597

598
  _NestedScrollPosition? get _outerPosition {
599 600 601 602 603 604 605 606 607
    if (!_outerController.hasClients)
      return null;
    return _outerController.nestedPositions.single;
  }

  Iterable<_NestedScrollPosition> get _innerPositions {
    return _innerController.nestedPositions;
  }

608
  bool get canScrollBody {
609
    final _NestedScrollPosition? outer = _outerPosition;
610 611 612 613 614
    if (outer == null)
      return true;
    return outer.haveDimensions && outer.extentAfter == 0.0;
  }

615
  bool get hasScrolledBody {
616
    for (final _NestedScrollPosition position in _innerPositions) {
617 618 619 620 621 622 623
      if (!position.hasContentDimensions || !position.hasPixels) {
        // It's possible that NestedScrollView built twice before layout phase
        // in the same frame. This can happen when the FocusManager schedules a microTask
        // that marks NestedScrollView dirty during the warm up frame.
        // https://github.com/flutter/flutter/pull/75308
        continue;
      } else if (position.pixels > position.minScrollExtent) {
624
        return true;
625
      }
626 627 628 629
    }
    return false;
  }

630
  void updateShadow() { _onHasScrolledBodyChanged(); }
631

632 633 634 635 636 637 638 639
  ScrollDirection get userScrollDirection => _userScrollDirection;
  ScrollDirection _userScrollDirection = ScrollDirection.idle;

  void updateUserScrollDirection(ScrollDirection value) {
    assert(value != null);
    if (userScrollDirection == value)
      return;
    _userScrollDirection = value;
640
    _outerPosition!.didUpdateScrollDirection(value);
641
    for (final _NestedScrollPosition position in _innerPositions)
642 643 644
      position.didUpdateScrollDirection(value);
  }

645
  ScrollDragController? _currentDrag;
646 647

  void beginActivity(ScrollActivity newOuterActivity, _NestedScrollActivityGetter innerActivityGetter) {
648
    _outerPosition!.beginActivity(newOuterActivity);
649
    bool scrolling = newOuterActivity.isScrolling;
650
    for (final _NestedScrollPosition position in _innerPositions) {
651 652 653 654 655 656 657 658 659 660 661
      final ScrollActivity newInnerActivity = innerActivityGetter(position);
      position.beginActivity(newInnerActivity);
      scrolling = scrolling && newInnerActivity.isScrolling;
    }
    _currentDrag?.dispose();
    _currentDrag = null;
    if (!scrolling)
      updateUserScrollDirection(ScrollDirection.idle);
  }

  @override
662
  AxisDirection get axisDirection => _outerPosition!.axisDirection;
663 664

  static IdleScrollActivity _createIdleScrollActivity(_NestedScrollPosition position) {
665
    return IdleScrollActivity(position);
666 667 668 669
  }

  @override
  void goIdle() {
670
    beginActivity(
671
      _createIdleScrollActivity(_outerPosition!),
672 673
      _createIdleScrollActivity,
    );
674 675 676 677 678 679
  }

  @override
  void goBallistic(double velocity) {
    beginActivity(
      createOuterBallisticScrollActivity(velocity),
680 681 682 683 684 685
      (_NestedScrollPosition position) {
        return createInnerBallisticScrollActivity(
          position,
          velocity,
        );
      },
686 687 688 689 690 691 692 693 694 695 696 697 698 699 700
    );
  }

  ScrollActivity createOuterBallisticScrollActivity(double velocity) {
    // This function creates a ballistic scroll for the outer scrollable.
    //
    // It assumes that the outer scrollable can't be overscrolled, and sets up a
    // ballistic scroll over the combined space of the innerPositions and the
    // outerPosition.

    // First we must pick a representative inner position that we will care
    // about. This is somewhat arbitrary. Ideally we'd pick the one that is "in
    // the center" but there isn't currently a good way to do that so we
    // arbitrarily pick the one that is the furthest away from the infinity we
    // are heading towards.
701
    _NestedScrollPosition? innerPosition;
702
    if (velocity != 0.0) {
703
      for (final _NestedScrollPosition position in _innerPositions) {
704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719
        if (innerPosition != null) {
          if (velocity > 0.0) {
            if (innerPosition.pixels < position.pixels)
              continue;
          } else {
            assert(velocity < 0.0);
            if (innerPosition.pixels > position.pixels)
              continue;
          }
        }
        innerPosition = position;
      }
    }

    if (innerPosition == null) {
      // It's either just us or a velocity=0 situation.
720 721 722
      return _outerPosition!.createBallisticScrollActivity(
        _outerPosition!.physics.createBallisticSimulation(
          _outerPosition!,
723 724
          velocity,
        ),
725 726 727 728 729 730
        mode: _NestedBallisticScrollActivityMode.independent,
      );
    }

    final _NestedScrollMetrics metrics = _getMetrics(innerPosition, velocity);

731 732
    return _outerPosition!.createBallisticScrollActivity(
      _outerPosition!.physics.createBallisticSimulation(metrics, velocity),
733 734 735 736 737 738 739 740 741
      mode: _NestedBallisticScrollActivityMode.outer,
      metrics: metrics,
    );
  }

  @protected
  ScrollActivity createInnerBallisticScrollActivity(_NestedScrollPosition position, double velocity) {
    return position.createBallisticScrollActivity(
      position.physics.createBallisticSimulation(
742
        _getMetrics(position, velocity),
743 744 745 746 747 748 749 750
        velocity,
      ),
      mode: _NestedBallisticScrollActivityMode.inner,
    );
  }

  _NestedScrollMetrics _getMetrics(_NestedScrollPosition innerPosition, double velocity) {
    assert(innerPosition != null);
751 752
    double pixels, minRange, maxRange, correctionOffset;
    double extra = 0.0;
753
    if (innerPosition.pixels == innerPosition.minScrollExtent) {
754 755 756 757 758 759
      pixels = _outerPosition!.pixels.clamp(
        _outerPosition!.minScrollExtent,
        _outerPosition!.maxScrollExtent,
      ); // TODO(ianh): gracefully handle out-of-range outer positions
      minRange = _outerPosition!.minScrollExtent;
      maxRange = _outerPosition!.maxScrollExtent;
760 761 762 763 764
      assert(minRange <= maxRange);
      correctionOffset = 0.0;
    } else {
      assert(innerPosition.pixels != innerPosition.minScrollExtent);
      if (innerPosition.pixels < innerPosition.minScrollExtent) {
765
        pixels = innerPosition.pixels - innerPosition.minScrollExtent + _outerPosition!.minScrollExtent;
766 767
      } else {
        assert(innerPosition.pixels > innerPosition.minScrollExtent);
768
        pixels = innerPosition.pixels - innerPosition.minScrollExtent + _outerPosition!.maxScrollExtent;
769 770 771 772
      }
      if ((velocity > 0.0) && (innerPosition.pixels > innerPosition.minScrollExtent)) {
        // This handles going forward (fling up) and inner list is scrolled past
        // zero. We want to grab the extra pixels immediately to shrink.
773
        extra = _outerPosition!.maxScrollExtent - _outerPosition!.pixels;
774 775 776 777
        assert(extra >= 0.0);
        minRange = pixels;
        maxRange = pixels + extra;
        assert(minRange <= maxRange);
778
        correctionOffset = _outerPosition!.pixels - pixels;
779 780 781
      } else if ((velocity < 0.0) && (innerPosition.pixels < innerPosition.minScrollExtent)) {
        // This handles going backward (fling down) and inner list is
        // underscrolled. We want to grab the extra pixels immediately to grow.
782
        extra = _outerPosition!.pixels - _outerPosition!.minScrollExtent;
783 784 785 786
        assert(extra >= 0.0);
        minRange = pixels - extra;
        maxRange = pixels;
        assert(minRange <= maxRange);
787
        correctionOffset = _outerPosition!.pixels - pixels;
788 789 790 791 792 793 794
      } else {
        // This handles going forward (fling up) and inner list is
        // underscrolled, OR, going backward (fling down) and inner list is
        // scrolled past zero. We want to skip the pixels we don't need to grow
        // or shrink over.
        if (velocity > 0.0) {
          // shrinking
795
          extra = _outerPosition!.minScrollExtent - _outerPosition!.pixels;
796
        } else if (velocity < 0.0) {
797
          // growing
798
          extra = _outerPosition!.pixels - (_outerPosition!.maxScrollExtent - _outerPosition!.minScrollExtent);
799 800
        }
        assert(extra <= 0.0);
801 802
        minRange = _outerPosition!.minScrollExtent;
        maxRange = _outerPosition!.maxScrollExtent + extra;
803 804 805 806
        assert(minRange <= maxRange);
        correctionOffset = 0.0;
      }
    }
807
    return _NestedScrollMetrics(
808 809
      minScrollExtent: _outerPosition!.minScrollExtent,
      maxScrollExtent: _outerPosition!.maxScrollExtent + innerPosition.maxScrollExtent - innerPosition.minScrollExtent + extra,
810
      pixels: pixels,
811 812
      viewportDimension: _outerPosition!.viewportDimension,
      axisDirection: _outerPosition!.axisDirection,
813 814 815 816 817 818 819 820
      minRange: minRange,
      maxRange: maxRange,
      correctionOffset: correctionOffset,
    );
  }

  double unnestOffset(double value, _NestedScrollPosition source) {
    if (source == _outerPosition)
821
      return value.clamp(
822 823 824
        _outerPosition!.minScrollExtent,
        _outerPosition!.maxScrollExtent,
      );
825
    if (value < source.minScrollExtent)
826 827
      return value - source.minScrollExtent + _outerPosition!.minScrollExtent;
    return value - source.minScrollExtent + _outerPosition!.maxScrollExtent;
828 829 830 831
  }

  double nestOffset(double value, _NestedScrollPosition target) {
    if (target == _outerPosition)
832
      return value.clamp(
833 834 835 836 837 838 839
        _outerPosition!.minScrollExtent,
        _outerPosition!.maxScrollExtent,
      );
    if (value < _outerPosition!.minScrollExtent)
      return value - _outerPosition!.minScrollExtent + target.minScrollExtent;
    if (value > _outerPosition!.maxScrollExtent)
      return value - _outerPosition!.maxScrollExtent + target.minScrollExtent;
840 841 842 843
    return target.minScrollExtent;
  }

  void updateCanDrag() {
844
    if (!_outerPosition!.haveDimensions)
845 846
      return;
    double maxInnerExtent = 0.0;
847
    for (final _NestedScrollPosition position in _innerPositions) {
848 849
      if (!position.haveDimensions)
        return;
850 851 852 853
      maxInnerExtent = math.max(
        maxInnerExtent,
        position.maxScrollExtent - position.minScrollExtent,
      );
854
    }
855
    _outerPosition!.updateCanDrag(maxInnerExtent);
856 857
  }

858 859
  Future<void> animateTo(
    double to, {
860 861
    required Duration duration,
    required Curve curve,
862
  }) async {
863 864
    final DrivenScrollActivity outerActivity = _outerPosition!.createDrivenScrollActivity(
      nestOffset(to, _outerPosition!),
865 866 867
      duration,
      curve,
    );
868
    final List<Future<void>> resultFutures = <Future<void>>[outerActivity.done];
869 870 871 872 873 874 875 876 877 878 879 880
    beginActivity(
      outerActivity,
      (_NestedScrollPosition position) {
        final DrivenScrollActivity innerActivity = position.createDrivenScrollActivity(
          nestOffset(to, position),
          duration,
          curve,
        );
        resultFutures.add(innerActivity.done);
        return innerActivity;
      },
    );
881
    await Future.wait<void>(resultFutures);
882 883 884 885
  }

  void jumpTo(double to) {
    goIdle();
886
    _outerPosition!.localJumpTo(nestOffset(to, _outerPosition!));
887
    for (final _NestedScrollPosition position in _innerPositions)
888 889 890 891
      position.localJumpTo(nestOffset(to, position));
    goBallistic(0.0);
  }

892 893 894 895 896
  void pointerScroll(double delta) {
    assert(delta != 0.0);

    goIdle();
    updateUserScrollDirection(
897
        delta < 0.0 ? ScrollDirection.forward : ScrollDirection.reverse,
898 899
    );

900 901 902 903 904 905 906
    // Set the isScrollingNotifier. Even if only one position actually receives
    // the delta, the NestedScrollView's intention is to treat multiple
    // ScrollPositions as one.
    _outerPosition!.isScrollingNotifier.value = true;
    for (final _NestedScrollPosition position in _innerPositions)
      position.isScrollingNotifier.value = true;

907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925
    if (_innerPositions.isEmpty) {
      // Does not enter overscroll.
      _outerPosition!.applyClampedPointerSignalUpdate(delta);
    } else if (delta > 0.0) {
      // Dragging "up" - delta is positive
      // Prioritize getting rid of any inner overscroll, and then the outer
      // view, so that the app bar will scroll out of the way asap.
      double outerDelta = delta;
      for (final _NestedScrollPosition position in _innerPositions) {
        if (position.pixels < 0.0) { // This inner position is in overscroll.
          final double potentialOuterDelta = position.applyClampedPointerSignalUpdate(delta);
          // In case there are multiple positions in varying states of
          // overscroll, the first to 'reach' the outer view above takes
          // precedence.
          outerDelta = math.max(outerDelta, potentialOuterDelta);
        }
      }
      if (outerDelta != 0.0) {
        final double innerDelta = _outerPosition!.applyClampedPointerSignalUpdate(
926
            outerDelta,
927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955
        );
        if (innerDelta != 0.0) {
          for (final _NestedScrollPosition position in _innerPositions)
            position.applyClampedPointerSignalUpdate(innerDelta);
        }
      }
    } else {
      // Dragging "down" - delta is negative
      double innerDelta = delta;
      // Apply delta to the outer header first if it is configured to float.
      if (_floatHeaderSlivers)
        innerDelta = _outerPosition!.applyClampedPointerSignalUpdate(delta);

      if (innerDelta != 0.0) {
        // Apply the innerDelta, if we have not floated in the outer scrollable,
        // any leftover delta after this will be passed on to the outer
        // scrollable by the outerDelta.
        double outerDelta = 0.0; // it will go negative if it changes
        for (final _NestedScrollPosition position in _innerPositions) {
          final double overscroll = position.applyClampedPointerSignalUpdate(innerDelta);
          outerDelta = math.min(outerDelta, overscroll);
        }
        if (outerDelta != 0.0)
          _outerPosition!.applyClampedPointerSignalUpdate(outerDelta);
      }
    }
    goBallistic(0.0);
  }

956 957 958 959 960 961
  @override
  double setPixels(double newPixels) {
    assert(false);
    return 0.0;
  }

962 963
  ScrollHoldController hold(VoidCallback holdCancelCallback) {
    beginActivity(
964
      HoldScrollActivity(
965
        delegate: _outerPosition!,
966 967
        onHoldCanceled: holdCancelCallback,
      ),
968
      (_NestedScrollPosition position) => HoldScrollActivity(delegate: position),
969 970 971 972 973 974 975
    );
    return this;
  }

  @override
  void cancel() {
    goBallistic(0.0);
976 977 978
  }

  Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) {
979
    final ScrollDragController drag = ScrollDragController(
980 981 982
      delegate: this,
      details: details,
      onDragCanceled: dragCancelCallback,
983 984
    );
    beginActivity(
985
      DragScrollActivity(_outerPosition!, drag),
986
      (_NestedScrollPosition position) => DragScrollActivity(position, drag),
987 988 989 990 991 992 993 994
    );
    assert(_currentDrag == null);
    _currentDrag = drag;
    return drag;
  }

  @override
  void applyUserOffset(double delta) {
995
    updateUserScrollDirection(
996
      delta > 0.0 ? ScrollDirection.forward : ScrollDirection.reverse,
997
    );
998 999
    assert(delta != 0.0);
    if (_innerPositions.isEmpty) {
1000
      _outerPosition!.applyFullDragUpdate(delta);
1001
    } else if (delta < 0.0) {
1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015
      // Dragging "up"
      // Prioritize getting rid of any inner overscroll, and then the outer
      // view, so that the app bar will scroll out of the way asap.
      double outerDelta = delta;
      for (final _NestedScrollPosition position in _innerPositions) {
        if (position.pixels < 0.0) { // This inner position is in overscroll.
          final double potentialOuterDelta = position.applyClampedDragUpdate(delta);
          // In case there are multiple positions in varying states of
          // overscroll, the first to 'reach' the outer view above takes
          // precedence.
          outerDelta = math.max(outerDelta, potentialOuterDelta);
        }
      }
      if (outerDelta != 0.0) {
1016
        final double innerDelta = _outerPosition!.applyClampedDragUpdate(
1017
          outerDelta,
1018 1019 1020 1021 1022
        );
        if (innerDelta != 0.0) {
          for (final _NestedScrollPosition position in _innerPositions)
            position.applyFullDragUpdate(innerDelta);
        }
1023 1024
      }
    } else {
1025
      // Dragging "down" - delta is positive
1026 1027 1028
      double innerDelta = delta;
      // Apply delta to the outer header first if it is configured to float.
      if (_floatHeaderSlivers)
1029
        innerDelta = _outerPosition!.applyClampedDragUpdate(delta);
1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043

      if (innerDelta != 0.0) {
        // Apply the innerDelta, if we have not floated in the outer scrollable,
        // any leftover delta after this will be passed on to the outer
        // scrollable by the outerDelta.
        double outerDelta = 0.0; // it will go positive if it changes
        final List<double> overscrolls = <double>[];
        final List<_NestedScrollPosition> innerPositions = _innerPositions.toList();
        for (final _NestedScrollPosition position in innerPositions) {
          final double overscroll = position.applyClampedDragUpdate(innerDelta);
          outerDelta = math.max(outerDelta, overscroll);
          overscrolls.add(overscroll);
        }
        if (outerDelta != 0.0)
1044
          outerDelta -= _outerPosition!.applyClampedDragUpdate(outerDelta);
1045 1046 1047 1048 1049 1050 1051

        // Now deal with any overscroll
        for (int i = 0; i < innerPositions.length; ++i) {
          final double remainingDelta = overscrolls[i] - outerDelta;
          if (remainingDelta > 0.0)
            innerPositions[i].applyFullDragUpdate(remainingDelta);
        }
1052 1053 1054 1055
      }
    }
  }

1056
  void setParent(ScrollController? value) {
1057 1058 1059 1060
    _parent = value;
    updateParent();
  }

1061
  void updateParent() {
1062
    _outerPosition?.setParent(
1063
      _parent ?? PrimaryScrollController.of(_state.context),
1064
    );
1065 1066 1067 1068 1069 1070 1071 1072 1073
  }

  @mustCallSuper
  void dispose() {
    _currentDrag?.dispose();
    _currentDrag = null;
    _outerController.dispose();
    _innerController.dispose();
  }
1074 1075

  @override
1076
  String toString() => '${objectRuntimeType(this, '_NestedScrollCoordinator')}(outer=$_outerController; inner=$_innerController)';
1077 1078 1079
}

class _NestedScrollController extends ScrollController {
1080 1081
  _NestedScrollController(
    this.coordinator, {
1082
    double initialScrollOffset = 0.0,
1083
    String? debugLabel,
1084 1085
  }) : super(initialScrollOffset: initialScrollOffset, debugLabel: debugLabel);

1086
  final _NestedScrollCoordinator coordinator;
1087 1088 1089 1090 1091

  @override
  ScrollPosition createScrollPosition(
    ScrollPhysics physics,
    ScrollContext context,
1092
    ScrollPosition? oldPosition,
1093
  ) {
1094
    return _NestedScrollPosition(
1095
      coordinator: coordinator,
1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107
      physics: physics,
      context: context,
      initialPixels: initialScrollOffset,
      oldPosition: oldPosition,
      debugLabel: debugLabel,
    );
  }

  @override
  void attach(ScrollPosition position) {
    assert(position is _NestedScrollPosition);
    super.attach(position);
1108 1109
    coordinator.updateParent();
    coordinator.updateCanDrag();
1110
    position.addListener(_scheduleUpdateShadow);
1111 1112 1113 1114 1115 1116
    _scheduleUpdateShadow();
  }

  @override
  void detach(ScrollPosition position) {
    assert(position is _NestedScrollPosition);
1117
    (position as _NestedScrollPosition).setParent(null);
1118
    position.removeListener(_scheduleUpdateShadow);
1119 1120 1121 1122 1123 1124
    super.detach(position);
    _scheduleUpdateShadow();
  }

  void _scheduleUpdateShadow() {
    // We do this asynchronously for attach() so that the new position has had
1125 1126 1127 1128
    // time to be initialized, and we do it asynchronously for detach() and from
    // the position change notifications because those happen synchronously
    // during a frame, at a time where it's too late to call setState. Since the
    // result is usually animated, the lag incurred is no big deal.
1129
    SchedulerBinding.instance!.addPostFrameCallback(
1130 1131
      (Duration timeStamp) {
        coordinator.updateShadow();
1132
      },
1133
    );
1134 1135 1136
  }

  Iterable<_NestedScrollPosition> get nestedPositions sync* {
1137
    // TODO(vegorov): use instance method version of castFrom when it is available.
1138
    yield* Iterable.castFrom<ScrollPosition, _NestedScrollPosition>(positions);
1139 1140 1141
  }
}

1142 1143 1144 1145
// The _NestedScrollPosition is used by both the inner and outer viewports of a
// NestedScrollView. It tracks the offset to use for those viewports, and knows
// about the _NestedScrollCoordinator, so that when activities are triggered on
// this class, they can defer, or be influenced by, the coordinator.
1146 1147
class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDelegate {
  _NestedScrollPosition({
1148 1149
    required ScrollPhysics physics,
    required ScrollContext context,
1150
    double initialPixels = 0.0,
1151 1152 1153
    ScrollPosition? oldPosition,
    String? debugLabel,
    required this.coordinator,
1154 1155 1156 1157 1158 1159
  }) : super(
    physics: physics,
    context: context,
    oldPosition: oldPosition,
    debugLabel: debugLabel,
  ) {
1160
    if (!hasPixels && initialPixels != null)
1161 1162 1163 1164
      correctPixels(initialPixels);
    if (activity == null)
      goIdle();
    assert(activity != null);
1165
    saveScrollOffset(); // in case we didn't restore but could, so that we don't restore it later
1166 1167
  }

1168
  final _NestedScrollCoordinator coordinator;
1169 1170 1171

  TickerProvider get vsync => context.vsync;

1172
  ScrollController? _parent;
1173

1174
  void setParent(ScrollController? value) {
1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185
    _parent?.detach(this);
    _parent = value;
    _parent?.attach(this);
  }

  @override
  AxisDirection get axisDirection => context.axisDirection;

  @override
  void absorb(ScrollPosition other) {
    super.absorb(other);
1186
    activity!.updateDelegate(this);
1187 1188
  }

1189 1190 1191 1192 1193 1194
  @override
  void restoreScrollOffset() {
    if (coordinator.canScrollBody)
      super.restoreScrollOffset();
  }

1195
  // Returns the amount of delta that was not used.
1196 1197 1198
  //
  // Positive delta means going down (exposing stuff above), negative delta
  // going up (exposing stuff below).
1199 1200
  double applyClampedDragUpdate(double delta) {
    assert(delta != 0.0);
1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217
    // If we are going towards the maxScrollExtent (negative scroll offset),
    // then the furthest we can be in the minScrollExtent direction is negative
    // infinity. For example, if we are already overscrolled, then scrolling to
    // reduce the overscroll should not disallow the overscroll.
    //
    // If we are going towards the minScrollExtent (positive scroll offset),
    // then the furthest we can be in the minScrollExtent direction is wherever
    // we are now, if we are already overscrolled (in which case pixels is less
    // than the minScrollExtent), or the minScrollExtent if we are not.
    //
    // In other words, we cannot, via applyClampedDragUpdate, _enter_ an
    // overscroll situation.
    //
    // An overscroll situation might be nonetheless entered via several means.
    // One is if the physics allow it, via applyFullDragUpdate (see below). An
    // overscroll situation can also be forced, e.g. if the scroll position is
    // artificially set using the scroll controller.
1218 1219 1220
    final double min = delta < 0.0
      ? -double.infinity
      : math.min(minScrollExtent, pixels);
1221
    // The logic for max is equivalent but on the other side.
1222 1223
    final double max = delta > 0.0
      ? double.infinity
1224 1225 1226
      // If pixels < 0.0, then we are currently in overscroll. The max should be
      // 0.0, representing the end of the overscrolled portion.
      : pixels < 0.0 ? 0.0 : math.max(maxScrollExtent, pixels);
1227
    final double oldPixels = pixels;
1228
    final double newPixels = (pixels - delta).clamp(min, max);
1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245
    final double clampedDelta = newPixels - pixels;
    if (clampedDelta == 0.0)
      return delta;
    final double overscroll = physics.applyBoundaryConditions(this, newPixels);
    final double actualNewPixels = newPixels - overscroll;
    final double offset = actualNewPixels - oldPixels;
    if (offset != 0.0) {
      forcePixels(actualNewPixels);
      didUpdateScrollPositionBy(offset);
    }
    return delta + offset;
  }

  // Returns the overscroll.
  double applyFullDragUpdate(double delta) {
    assert(delta != 0.0);
    final double oldPixels = pixels;
1246
    // Apply friction:
1247 1248 1249 1250
    final double newPixels = pixels - physics.applyPhysicsToUserOffset(
      this,
      delta,
    );
1251 1252
    if (oldPixels == newPixels)
      return 0.0; // delta must have been so small we dropped it during floating point addition
1253
    // Check for overscroll:
1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266
    final double overscroll = physics.applyBoundaryConditions(this, newPixels);
    final double actualNewPixels = newPixels - overscroll;
    if (actualNewPixels != oldPixels) {
      forcePixels(actualNewPixels);
      didUpdateScrollPositionBy(actualNewPixels - oldPixels);
    }
    if (overscroll != 0.0) {
      didOverscrollBy(overscroll);
      return overscroll;
    }
    return 0.0;
  }

1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292

  // Returns the amount of delta that was not used.
  //
  // Negative delta represents a forward ScrollDirection, while the positive
  // would be a reverse ScrollDirection.
  //
  // The method doesn't take into account the effects of [ScrollPhysics].
  double applyClampedPointerSignalUpdate(double delta) {
    assert(delta != 0.0);

    final double min = delta > 0.0
        ? -double.infinity
        : math.min(minScrollExtent, pixels);
    // The logic for max is equivalent but on the other side.
    final double max = delta < 0.0
        ? double.infinity
        : math.max(maxScrollExtent, pixels);
    final double newPixels = (pixels + delta).clamp(min, max);
    final double clampedDelta = newPixels - pixels;
    if (clampedDelta == 0.0)
      return delta;
    forcePixels(newPixels);
    didUpdateScrollPositionBy(clampedDelta);
    return delta - clampedDelta;
  }

1293
  @override
1294
  ScrollDirection get userScrollDirection => coordinator.userScrollDirection;
1295 1296

  DrivenScrollActivity createDrivenScrollActivity(double to, Duration duration, Curve curve) {
1297
    return DrivenScrollActivity(
1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315
      this,
      from: pixels,
      to: to,
      duration: duration,
      curve: curve,
      vsync: vsync,
    );
  }

  @override
  double applyUserOffset(double delta) {
    assert(false);
    return 0.0;
  }

  // This is called by activities when they finish their work.
  @override
  void goIdle() {
1316
    beginActivity(IdleScrollActivity(this));
1317 1318
  }

1319 1320
  // This is called by activities when they finish their work and want to go
  // ballistic.
1321 1322
  @override
  void goBallistic(double velocity) {
1323
    Simulation? simulation;
1324 1325 1326 1327 1328 1329 1330 1331
    if (velocity != 0.0 || outOfRange)
      simulation = physics.createBallisticSimulation(this, velocity);
    beginActivity(createBallisticScrollActivity(
      simulation,
      mode: _NestedBallisticScrollActivityMode.independent,
    ));
  }

1332
  ScrollActivity createBallisticScrollActivity(
1333 1334 1335
    Simulation? simulation, {
    required _NestedBallisticScrollActivityMode mode,
    _NestedScrollMetrics? metrics,
1336 1337
  }) {
    if (simulation == null)
1338
      return IdleScrollActivity(this);
1339 1340 1341 1342
    assert(mode != null);
    switch (mode) {
      case _NestedBallisticScrollActivityMode.outer:
        assert(metrics != null);
1343
        if (metrics!.minRange == metrics.maxRange)
1344
          return IdleScrollActivity(this);
1345 1346 1347 1348 1349 1350 1351
        return _NestedOuterBallisticScrollActivity(
          coordinator,
          this,
          metrics,
          simulation,
          context.vsync,
        );
1352
      case _NestedBallisticScrollActivityMode.inner:
1353 1354 1355 1356 1357 1358
        return _NestedInnerBallisticScrollActivity(
          coordinator,
          this,
          simulation,
          context.vsync,
        );
1359
      case _NestedBallisticScrollActivityMode.independent:
1360
        return BallisticScrollActivity(this, simulation, context.vsync);
1361 1362 1363 1364
    }
  }

  @override
1365 1366
  Future<void> animateTo(
    double to, {
1367 1368
    required Duration duration,
    required Curve curve,
1369
  }) {
1370 1371 1372 1373 1374
    return coordinator.animateTo(
      coordinator.unnestOffset(to, this),
      duration: duration,
      curve: curve,
    );
1375 1376 1377 1378
  }

  @override
  void jumpTo(double value) {
1379
    return coordinator.jumpTo(coordinator.unnestOffset(value, this));
1380 1381
  }

1382 1383 1384 1385 1386 1387
  @override
  void pointerScroll(double delta) {
    return coordinator.pointerScroll(delta);
  }


1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405
  @override
  void jumpToWithoutSettling(double value) {
    assert(false);
  }

  void localJumpTo(double value) {
    if (pixels != value) {
      final double oldPixels = pixels;
      forcePixels(value);
      didStartScroll();
      didUpdateScrollPositionBy(pixels - oldPixels);
      didEndScroll();
    }
  }

  @override
  void applyNewDimensions() {
    super.applyNewDimensions();
1406
    coordinator.updateCanDrag();
1407 1408 1409 1410 1411 1412 1413
  }

  void updateCanDrag(double totalExtent) {
    context.setCanDrag(totalExtent > (viewportDimension - maxScrollExtent) || minScrollExtent != maxScrollExtent);
  }

  @override
1414 1415
  ScrollHoldController hold(VoidCallback holdCancelCallback) {
    return coordinator.hold(holdCancelCallback);
1416 1417 1418 1419
  }

  @override
  Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) {
1420
    return coordinator.drag(details, dragCancelCallback);
1421 1422 1423 1424 1425 1426 1427
  }
}

enum _NestedBallisticScrollActivityMode { outer, inner, independent }

class _NestedInnerBallisticScrollActivity extends BallisticScrollActivity {
  _NestedInnerBallisticScrollActivity(
1428
    this.coordinator,
1429 1430 1431 1432 1433
    _NestedScrollPosition position,
    Simulation simulation,
    TickerProvider vsync,
  ) : super(position, simulation, vsync);

1434
  final _NestedScrollCoordinator coordinator;
1435 1436

  @override
1437
  _NestedScrollPosition get delegate => super.delegate as _NestedScrollPosition;
1438 1439 1440

  @override
  void resetActivity() {
1441 1442 1443 1444
    delegate.beginActivity(coordinator.createInnerBallisticScrollActivity(
      delegate,
      velocity,
    ));
1445 1446 1447 1448
  }

  @override
  void applyNewDimensions() {
1449 1450 1451 1452
    delegate.beginActivity(coordinator.createInnerBallisticScrollActivity(
      delegate,
      velocity,
    ));
1453 1454 1455 1456
  }

  @override
  bool applyMoveTo(double value) {
1457
    return super.applyMoveTo(coordinator.nestOffset(value, delegate));
1458 1459 1460 1461 1462
  }
}

class _NestedOuterBallisticScrollActivity extends BallisticScrollActivity {
  _NestedOuterBallisticScrollActivity(
1463
    this.coordinator,
1464 1465 1466 1467
    _NestedScrollPosition position,
    this.metrics,
    Simulation simulation,
    TickerProvider vsync,
1468 1469 1470
  ) : assert(metrics.minRange != metrics.maxRange),
      assert(metrics.maxRange > metrics.minRange),
      super(position, simulation, vsync);
1471

1472
  final _NestedScrollCoordinator coordinator;
1473 1474 1475
  final _NestedScrollMetrics metrics;

  @override
1476
  _NestedScrollPosition get delegate => super.delegate as _NestedScrollPosition;
1477 1478 1479

  @override
  void resetActivity() {
1480
    delegate.beginActivity(
1481
      coordinator.createOuterBallisticScrollActivity(velocity),
1482
    );
1483 1484 1485 1486
  }

  @override
  void applyNewDimensions() {
1487
    delegate.beginActivity(
1488
      coordinator.createOuterBallisticScrollActivity(velocity),
1489
    );
1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509
  }

  @override
  bool applyMoveTo(double value) {
    bool done = false;
    if (velocity > 0.0) {
      if (value < metrics.minRange)
        return true;
      if (value > metrics.maxRange) {
        value = metrics.maxRange;
        done = true;
      }
    } else if (velocity < 0.0) {
      if (value > metrics.maxRange)
        return true;
      if (value < metrics.minRange) {
        value = metrics.minRange;
        done = true;
      }
    } else {
1510
      value = value.clamp(metrics.minRange, metrics.maxRange);
1511 1512 1513 1514 1515 1516 1517 1518 1519
      done = true;
    }
    final bool result = super.applyMoveTo(value + metrics.correctionOffset);
    assert(result); // since we tried to pass an in-range value, it shouldn't ever overflow
    return !done;
  }

  @override
  String toString() {
1520
    return '${objectRuntimeType(this, '_NestedOuterBallisticScrollActivity')}(${metrics.minRange} .. ${metrics.maxRange}; correcting by ${metrics.correctionOffset})';
1521 1522
  }
}
1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564

/// Handle to provide to a [SliverOverlapAbsorber], a [SliverOverlapInjector],
/// and an [NestedScrollViewViewport], to shift overlap in a [NestedScrollView].
///
/// A particular [SliverOverlapAbsorberHandle] can only be assigned to a single
/// [SliverOverlapAbsorber] at a time. It can also be (and normally is) assigned
/// to one or more [SliverOverlapInjector]s, which must be later descendants of
/// the same [NestedScrollViewViewport] as the [SliverOverlapAbsorber]. The
/// [SliverOverlapAbsorber] must be a direct descendant of the
/// [NestedScrollViewViewport], taking part in the same sliver layout. (The
/// [SliverOverlapInjector] can be a descendant that takes part in a nested
/// scroll view's sliver layout.)
///
/// Whenever the [NestedScrollViewViewport] is marked dirty for layout, it will
/// cause its assigned [SliverOverlapAbsorberHandle] to fire notifications. It
/// is the responsibility of the [SliverOverlapInjector]s (and any other
/// clients) to mark themselves dirty when this happens, in case the geometry
/// subsequently changes during layout.
///
/// See also:
///
///  * [NestedScrollView], which uses a [NestedScrollViewViewport] and a
///    [SliverOverlapAbsorber] to align its children, and which shows sample
///    usage for this class.
class SliverOverlapAbsorberHandle extends ChangeNotifier {
  // Incremented when a RenderSliverOverlapAbsorber takes ownership of this
  // object, decremented when it releases it. This allows us to find cases where
  // the same handle is being passed to two render objects.
  int _writers = 0;

  /// The current amount of overlap being absorbed by the
  /// [SliverOverlapAbsorber].
  ///
  /// This corresponds to the [SliverGeometry.layoutExtent] of the child of the
  /// [SliverOverlapAbsorber].
  ///
  /// This is updated during the layout of the [SliverOverlapAbsorber]. It
  /// should not change at any other time. No notifications are sent when it
  /// changes; clients (e.g. [SliverOverlapInjector]s) are responsible for
  /// marking themselves dirty whenever this object sends notifications, which
  /// happens any time the [SliverOverlapAbsorber] might subsequently change the
  /// value during that layout.
1565 1566
  double? get layoutExtent => _layoutExtent;
  double? _layoutExtent;
1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579

  /// The total scroll extent of the gap being absorbed by the
  /// [SliverOverlapAbsorber].
  ///
  /// This corresponds to the [SliverGeometry.scrollExtent] of the child of the
  /// [SliverOverlapAbsorber].
  ///
  /// This is updated during the layout of the [SliverOverlapAbsorber]. It
  /// should not change at any other time. No notifications are sent when it
  /// changes; clients (e.g. [SliverOverlapInjector]s) are responsible for
  /// marking themselves dirty whenever this object sends notifications, which
  /// happens any time the [SliverOverlapAbsorber] might subsequently change the
  /// value during that layout.
1580 1581
  double? get scrollExtent => _scrollExtent;
  double? _scrollExtent;
1582

1583
  void _setExtents(double? layoutValue, double? scrollValue) {
1584 1585 1586 1587
    assert(
      _writers == 1,
      'Multiple RenderSliverOverlapAbsorbers have been provided the same SliverOverlapAbsorberHandle.',
    );
1588 1589 1590 1591 1592 1593 1594 1595
    _layoutExtent = layoutValue;
    _scrollExtent = scrollValue;
  }

  void _markNeedsLayout() => notifyListeners();

  @override
  String toString() {
1596
    String? extra;
1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607
    switch (_writers) {
      case 0:
        extra = ', orphan';
        break;
      case 1:
        // normal case
        break;
      default:
        extra = ', $_writers WRITERS ASSIGNED';
        break;
    }
1608
    return '${objectRuntimeType(this, 'SliverOverlapAbsorberHandle')}($layoutExtent$extra)';
1609 1610 1611 1612 1613 1614
  }
}

/// A sliver that wraps another, forcing its layout extent to be treated as
/// overlap.
///
1615
/// The difference between the overlap requested by the child `sliver` and the
1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629
/// overlap reported by this widget, called the _absorbed overlap_, is reported
/// to the [SliverOverlapAbsorberHandle], which is typically passed to a
/// [SliverOverlapInjector].
///
/// See also:
///
///  * [NestedScrollView], whose documentation has sample code showing how to
///    use this widget.
class SliverOverlapAbsorber extends SingleChildRenderObjectWidget {
  /// Creates a sliver that absorbs overlap and reports it to a
  /// [SliverOverlapAbsorberHandle].
  ///
  /// The [handle] must not be null.
  const SliverOverlapAbsorber({
1630 1631 1632
    Key? key,
    required this.handle,
    Widget? sliver,
1633
  }) : assert(handle != null),
1634
      super(key: key, child: sliver);
1635 1636 1637 1638 1639 1640 1641 1642 1643

  /// The object in which the absorbed overlap is recorded.
  ///
  /// A particular [SliverOverlapAbsorberHandle] can only be assigned to a
  /// single [SliverOverlapAbsorber] at a time.
  final SliverOverlapAbsorberHandle handle;

  @override
  RenderSliverOverlapAbsorber createRenderObject(BuildContext context) {
1644
    return RenderSliverOverlapAbsorber(
1645 1646 1647 1648 1649 1650
      handle: handle,
    );
  }

  @override
  void updateRenderObject(BuildContext context, RenderSliverOverlapAbsorber renderObject) {
1651
    renderObject.handle = handle;
1652 1653 1654
  }

  @override
1655 1656
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
1657
    properties.add(DiagnosticsProperty<SliverOverlapAbsorberHandle>('handle', handle));
1658 1659 1660 1661 1662 1663
  }
}

/// A sliver that wraps another, forcing its layout extent to be treated as
/// overlap.
///
1664
/// The difference between the overlap requested by the child `sliver` and the
1665 1666 1667 1668 1669 1670 1671 1672 1673
/// overlap reported by this widget, called the _absorbed overlap_, is reported
/// to the [SliverOverlapAbsorberHandle], which is typically passed to a
/// [RenderSliverOverlapInjector].
class RenderSliverOverlapAbsorber extends RenderSliver with RenderObjectWithChildMixin<RenderSliver> {
  /// Create a sliver that absorbs overlap and reports it to a
  /// [SliverOverlapAbsorberHandle].
  ///
  /// The [handle] must not be null.
  ///
1674
  /// The [sliver] must be a [RenderSliver].
1675
  RenderSliverOverlapAbsorber({
1676 1677
    required SliverOverlapAbsorberHandle handle,
    RenderSliver? sliver,
1678 1679
  }) : assert(handle != null),
       _handle = handle {
1680
    child = sliver;
1681 1682
  }

1683 1684 1685 1686
  /// The object in which the absorbed overlap is recorded.
  ///
  /// A particular [SliverOverlapAbsorberHandle] can only be assigned to a
  /// single [RenderSliverOverlapAbsorber] at a time.
1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714
  SliverOverlapAbsorberHandle get handle => _handle;
  SliverOverlapAbsorberHandle _handle;
  set handle(SliverOverlapAbsorberHandle value) {
    assert(value != null);
    if (handle == value)
      return;
    if (attached) {
      handle._writers -= 1;
      value._writers += 1;
      value._setExtents(handle.layoutExtent, handle.scrollExtent);
    }
    _handle = value;
  }

  @override
  void attach(PipelineOwner owner) {
    super.attach(owner);
    handle._writers += 1;
  }

  @override
  void detach() {
    handle._writers -= 1;
    super.detach();
  }

  @override
  void performLayout() {
1715 1716 1717 1718
    assert(
      handle._writers == 1,
      'A SliverOverlapAbsorberHandle cannot be passed to multiple RenderSliverOverlapAbsorber objects at the same time.',
    );
1719
    if (child == null) {
1720
      geometry = SliverGeometry.zero;
1721 1722
      return;
    }
1723 1724
    child!.layout(constraints, parentUsesSize: true);
    final SliverGeometry childLayoutGeometry = child!.geometry!;
1725
    geometry = SliverGeometry(
1726 1727 1728
      scrollExtent: childLayoutGeometry.scrollExtent - childLayoutGeometry.maxScrollObstructionExtent,
      paintExtent: childLayoutGeometry.paintExtent,
      paintOrigin: childLayoutGeometry.paintOrigin,
1729
      layoutExtent: math.max(0, childLayoutGeometry.paintExtent - childLayoutGeometry.maxScrollObstructionExtent),
1730 1731 1732 1733 1734 1735 1736
      maxPaintExtent: childLayoutGeometry.maxPaintExtent,
      maxScrollObstructionExtent: childLayoutGeometry.maxScrollObstructionExtent,
      hitTestExtent: childLayoutGeometry.hitTestExtent,
      visible: childLayoutGeometry.visible,
      hasVisualOverflow: childLayoutGeometry.hasVisualOverflow,
      scrollOffsetCorrection: childLayoutGeometry.scrollOffsetCorrection,
    );
1737 1738 1739 1740
    handle._setExtents(
      childLayoutGeometry.maxScrollObstructionExtent,
      childLayoutGeometry.maxScrollObstructionExtent,
    );
1741 1742 1743 1744 1745 1746 1747 1748
  }

  @override
  void applyPaintTransform(RenderObject child, Matrix4 transform) {
    // child is always at our origin
  }

  @override
1749
  bool hitTestChildren(SliverHitTestResult result, { required double mainAxisPosition, required double crossAxisPosition }) {
1750
    if (child != null)
1751
      return child!.hitTest(
1752 1753 1754 1755
        result,
        mainAxisPosition: mainAxisPosition,
        crossAxisPosition: crossAxisPosition,
      );
1756 1757 1758 1759 1760 1761
    return false;
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    if (child != null)
1762
      context.paintChild(child!, offset);
1763 1764 1765
  }

  @override
1766 1767
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
1768
    properties.add(DiagnosticsProperty<SliverOverlapAbsorberHandle>('handle', handle));
1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788
  }
}

/// A sliver that has a sliver geometry based on the values stored in a
/// [SliverOverlapAbsorberHandle].
///
/// The [SliverOverlapAbsorber] must be an earlier descendant of a common
/// ancestor [Viewport], so that it will always be laid out before the
/// [SliverOverlapInjector] during a particular frame.
///
/// See also:
///
///  * [NestedScrollView], which uses a [SliverOverlapAbsorber] to align its
///    children, and which shows sample usage for this class.
class SliverOverlapInjector extends SingleChildRenderObjectWidget {
  /// Creates a sliver that is as tall as the value of the given [handle]'s
  /// layout extent.
  ///
  /// The [handle] must not be null.
  const SliverOverlapInjector({
1789 1790 1791
    Key? key,
    required this.handle,
    Widget? sliver,
1792
  }) : assert(handle != null),
1793
       super(key: key, child: sliver);
1794 1795 1796 1797 1798 1799 1800 1801 1802

  /// The handle to the [SliverOverlapAbsorber] that is feeding this injector.
  ///
  /// This should be a handle owned by a [SliverOverlapAbsorber] and a
  /// [NestedScrollViewViewport].
  final SliverOverlapAbsorberHandle handle;

  @override
  RenderSliverOverlapInjector createRenderObject(BuildContext context) {
1803
    return RenderSliverOverlapInjector(
1804 1805 1806 1807 1808 1809
      handle: handle,
    );
  }

  @override
  void updateRenderObject(BuildContext context, RenderSliverOverlapInjector renderObject) {
1810
    renderObject.handle = handle;
1811 1812 1813
  }

  @override
1814 1815
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
1816
    properties.add(DiagnosticsProperty<SliverOverlapAbsorberHandle>('handle', handle));
1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831
  }
}

/// A sliver that has a sliver geometry based on the values stored in a
/// [SliverOverlapAbsorberHandle].
///
/// The [RenderSliverOverlapAbsorber] must be an earlier descendant of a common
/// ancestor [RenderViewport] (probably a [RenderNestedScrollViewViewport]), so
/// that it will always be laid out before the [RenderSliverOverlapInjector]
/// during a particular frame.
class RenderSliverOverlapInjector extends RenderSliver {
  /// Creates a sliver that is as tall as the value of the given [handle]'s extent.
  ///
  /// The [handle] must not be null.
  RenderSliverOverlapInjector({
1832
    required SliverOverlapAbsorberHandle handle,
1833 1834
  }) : assert(handle != null),
       _handle = handle;
1835

1836 1837
  double? _currentLayoutExtent;
  double? _currentMaxExtent;
1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880

  /// The object that specifies how wide to make the gap injected by this render
  /// object.
  ///
  /// This should be a handle owned by a [RenderSliverOverlapAbsorber] and a
  /// [RenderNestedScrollViewViewport].
  SliverOverlapAbsorberHandle get handle => _handle;
  SliverOverlapAbsorberHandle _handle;
  set handle(SliverOverlapAbsorberHandle value) {
    assert(value != null);
    if (handle == value)
      return;
    if (attached) {
      handle.removeListener(markNeedsLayout);
    }
    _handle = value;
    if (attached) {
      handle.addListener(markNeedsLayout);
      if (handle.layoutExtent != _currentLayoutExtent ||
          handle.scrollExtent != _currentMaxExtent)
        markNeedsLayout();
    }
  }

  @override
  void attach(PipelineOwner owner) {
    super.attach(owner);
    handle.addListener(markNeedsLayout);
    if (handle.layoutExtent != _currentLayoutExtent ||
        handle.scrollExtent != _currentMaxExtent)
      markNeedsLayout();
  }

  @override
  void detach() {
    handle.removeListener(markNeedsLayout);
    super.detach();
  }

  @override
  void performLayout() {
    _currentLayoutExtent = handle.layoutExtent;
    _currentMaxExtent = handle.layoutExtent;
1881
    final double clampedLayoutExtent = math.min(
1882
      _currentLayoutExtent! - constraints.scrollOffset,
1883 1884
      constraints.remainingPaintExtent,
    );
1885
    geometry = SliverGeometry(
1886
      scrollExtent: _currentLayoutExtent!,
1887
      paintExtent: math.max(0.0, clampedLayoutExtent),
1888
      maxPaintExtent: _currentMaxExtent!,
1889 1890 1891 1892 1893 1894 1895
    );
  }

  @override
  void debugPaint(PaintingContext context, Offset offset) {
    assert(() {
      if (debugPaintSizeEnabled) {
1896
        final Paint paint = Paint()
1897 1898 1899 1900 1901 1902 1903
          ..color = const Color(0xFFCC9933)
          ..strokeWidth = 3.0
          ..style = PaintingStyle.stroke;
        Offset start, end, delta;
        switch (constraints.axis) {
          case Axis.vertical:
            final double x = offset.dx + constraints.crossAxisExtent / 2.0;
1904
            start = Offset(x, offset.dy);
1905
            end = Offset(x, offset.dy + geometry!.paintExtent);
1906
            delta = Offset(constraints.crossAxisExtent / 5.0, 0.0);
1907 1908 1909
            break;
          case Axis.horizontal:
            final double y = offset.dy + constraints.crossAxisExtent / 2.0;
1910
            start = Offset(offset.dx, y);
1911
            end = Offset(offset.dy + geometry!.paintExtent, y);
1912
            delta = Offset(0.0, constraints.crossAxisExtent / 5.0);
1913 1914 1915
            break;
        }
        for (int index = -2; index <= 2; index += 1) {
1916 1917 1918 1919 1920 1921 1922 1923
          paintZigZag(
            context.canvas,
            paint,
            start - delta * index.toDouble(),
            end - delta * index.toDouble(),
            10,
            10.0,
          );
1924 1925 1926 1927 1928 1929 1930
        }
      }
      return true;
    }());
  }

  @override
1931 1932
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
1933
    properties.add(DiagnosticsProperty<SliverOverlapAbsorberHandle>('handle', handle));
1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945
  }
}

/// The [Viewport] variant used by [NestedScrollView].
///
/// This viewport takes a [SliverOverlapAbsorberHandle] and notifies it any time
/// the viewport needs to recompute its layout (e.g. when it is scrolled).
class NestedScrollViewViewport extends Viewport {
  /// Creates a variant of [Viewport] that has a [SliverOverlapAbsorberHandle].
  ///
  /// The [handle] must not be null.
  NestedScrollViewViewport({
1946
    Key? key,
1947
    AxisDirection axisDirection = AxisDirection.down,
1948
    AxisDirection? crossAxisDirection,
1949
    double anchor = 0.0,
1950 1951
    required ViewportOffset offset,
    Key? center,
1952
    List<Widget> slivers = const <Widget>[],
1953
    required this.handle,
1954
    Clip clipBehavior = Clip.hardEdge,
1955 1956 1957 1958 1959 1960 1961 1962 1963
  }) : assert(handle != null),
       super(
         key: key,
         axisDirection: axisDirection,
         crossAxisDirection: crossAxisDirection,
         anchor: anchor,
         offset: offset,
         center: center,
         slivers: slivers,
1964
         clipBehavior: clipBehavior,
1965 1966 1967 1968 1969 1970 1971
       );

  /// The handle to the [SliverOverlapAbsorber] that is feeding this injector.
  final SliverOverlapAbsorberHandle handle;

  @override
  RenderNestedScrollViewViewport createRenderObject(BuildContext context) {
1972
    return RenderNestedScrollViewViewport(
1973
      axisDirection: axisDirection,
1974 1975 1976 1977
      crossAxisDirection: crossAxisDirection ?? Viewport.getDefaultCrossAxisDirection(
        context,
        axisDirection,
      ),
1978 1979 1980
      anchor: anchor,
      offset: offset,
      handle: handle,
1981
      clipBehavior: clipBehavior,
1982 1983 1984 1985 1986 1987 1988
    );
  }

  @override
  void updateRenderObject(BuildContext context, RenderNestedScrollViewViewport renderObject) {
    renderObject
      ..axisDirection = axisDirection
1989 1990 1991 1992
      ..crossAxisDirection = crossAxisDirection ?? Viewport.getDefaultCrossAxisDirection(
        context,
        axisDirection,
      )
1993 1994
      ..anchor = anchor
      ..offset = offset
1995 1996
      ..handle = handle
      ..clipBehavior = clipBehavior;
1997 1998 1999
  }

  @override
2000 2001
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
2002
    properties.add(DiagnosticsProperty<SliverOverlapAbsorberHandle>('handle', handle));
2003 2004 2005 2006 2007 2008 2009 2010
  }
}

/// The [RenderViewport] variant used by [NestedScrollView].
///
/// This viewport takes a [SliverOverlapAbsorberHandle] and notifies it any time
/// the viewport needs to recompute its layout (e.g. when it is scrolled).
class RenderNestedScrollViewViewport extends RenderViewport {
2011 2012
  /// Create a variant of [RenderViewport] that has a
  /// [SliverOverlapAbsorberHandle].
2013 2014 2015
  ///
  /// The [handle] must not be null.
  RenderNestedScrollViewViewport({
2016
    AxisDirection axisDirection = AxisDirection.down,
2017 2018
    required AxisDirection crossAxisDirection,
    required ViewportOffset offset,
2019
    double anchor = 0.0,
2020 2021 2022
    List<RenderSliver>? children,
    RenderSliver? center,
    required SliverOverlapAbsorberHandle handle,
2023
    Clip clipBehavior = Clip.hardEdge,
2024 2025 2026 2027 2028 2029 2030 2031 2032
  }) : assert(handle != null),
       _handle = handle,
       super(
         axisDirection: axisDirection,
         crossAxisDirection: crossAxisDirection,
         offset: offset,
         anchor: anchor,
         children: children,
         center: center,
2033
         clipBehavior: clipBehavior,
2034 2035 2036 2037 2038 2039 2040 2041 2042 2043 2044 2045 2046 2047 2048 2049 2050 2051 2052 2053 2054
       );

  /// The object to notify when [markNeedsLayout] is called.
  SliverOverlapAbsorberHandle get handle => _handle;
  SliverOverlapAbsorberHandle _handle;
  /// Setting this will trigger notifications on the new object.
  set handle(SliverOverlapAbsorberHandle value) {
    assert(value != null);
    if (handle == value)
      return;
    _handle = value;
    handle._markNeedsLayout();
  }

  @override
  void markNeedsLayout() {
    handle._markNeedsLayout();
    super.markNeedsLayout();
  }

  @override
2055 2056
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
2057
    properties.add(DiagnosticsProperty<SliverOverlapAbsorberHandle>('handle', handle));
2058 2059
  }
}