1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'package:flutter/material.dart';
6///
7/// @docImport 'page_storage.dart';
8/// @docImport 'safe_area.dart';
9/// @docImport 'scrollable.dart';
10library;
11
12import 'dart:math' as math;
13
14import 'package:flutter/foundation.dart';
15import 'package:flutter/gestures.dart';
16import 'package:flutter/rendering.dart';
17import 'package:flutter/scheduler.dart';
18
19import 'basic.dart';
20import 'framework.dart';
21import 'primary_scroll_controller.dart';
22import 'scroll_activity.dart';
23import 'scroll_configuration.dart';
24import 'scroll_context.dart';
25import 'scroll_controller.dart';
26import 'scroll_metrics.dart';
27import 'scroll_physics.dart';
28import 'scroll_position.dart';
29import 'scroll_view.dart';
30import 'sliver_fill.dart';
31import 'viewport.dart';
32
33/// Signature used by [NestedScrollView] for building its header.
34///
35/// The `innerBoxIsScrolled` argument is typically used to control the
36/// [SliverAppBar.forceElevated] property to ensure that the app bar shows a
37/// shadow, since it would otherwise not necessarily be aware that it had
38/// content ostensibly below it.
39typedef NestedScrollViewHeaderSliversBuilder =
40 List<Widget> Function(BuildContext context, bool innerBoxIsScrolled);
41
42/// A scrolling view inside of which can be nested other scrolling views, with
43/// their scroll positions being intrinsically linked.
44///
45/// The most common use case for this widget is a scrollable view with a
46/// flexible [SliverAppBar] containing a [TabBar] in the header (built by
47/// [headerSliverBuilder]), and with a [TabBarView] in the [body], such that the
48/// scrollable view's contents vary based on which tab is visible.
49///
50/// ## Motivation
51///
52/// In a normal [ScrollView], there is one set of slivers (the components of the
53/// scrolling view). If one of those slivers hosted a [TabBarView] which scrolls
54/// in the opposite direction (e.g. allowing the user to swipe horizontally
55/// between the pages represented by the tabs, while the list scrolls
56/// vertically), then any list inside that [TabBarView] would not interact with
57/// the outer [ScrollView]. For example, flinging the inner list to scroll to
58/// the top would not cause a collapsed [SliverAppBar] in the outer [ScrollView]
59/// to expand.
60///
61/// [NestedScrollView] solves this problem by providing custom
62/// [ScrollController]s for the outer [ScrollView] and the inner [ScrollView]s
63/// (those inside the [TabBarView], hooking them together so that they appear,
64/// to the user, as one coherent scroll view.
65///
66/// {@tool dartpad}
67/// This example shows a [NestedScrollView] whose header is the combination of a
68/// [TabBar] in a [SliverAppBar] and whose body is a [TabBarView]. It uses a
69/// [SliverOverlapAbsorber]/[SliverOverlapInjector] pair to make the inner lists
70/// align correctly, and it uses [SafeArea] to avoid any horizontal disturbances
71/// (e.g. the "notch" on iOS when the phone is horizontal). In addition,
72/// [PageStorageKey]s are used to remember the scroll position of each tab's
73/// list.
74///
75/// ** See code in examples/api/lib/widgets/nested_scroll_view/nested_scroll_view.0.dart **
76/// {@end-tool}
77///
78/// ## [SliverAppBar]s with [NestedScrollView]s
79///
80/// Using a [SliverAppBar] in the outer scroll view, or [headerSliverBuilder],
81/// of a [NestedScrollView] may require special configurations in order to work
82/// as it would if the outer and inner were one single scroll view, like a
83/// [CustomScrollView].
84///
85/// ### Pinned [SliverAppBar]s
86///
87/// A pinned [SliverAppBar] works in a [NestedScrollView] exactly as it would in
88/// another scroll view, like [CustomScrollView]. When using
89/// [SliverAppBar.pinned], the app bar remains visible at the top of the scroll
90/// view. The app bar can still expand and contract as the user scrolls, but it
91/// will remain visible rather than being scrolled out of view.
92///
93/// This works naturally in a [NestedScrollView], as the pinned [SliverAppBar]
94/// is not expected to move in or out of the visible portion of the viewport.
95/// As the inner or outer [Scrollable]s are moved, the app bar persists as
96/// expected.
97///
98/// If the app bar is floating, pinned, and using an expanded height, follow the
99/// floating convention laid out below.
100///
101/// ### Floating [SliverAppBar]s
102///
103/// When placed in the outer scrollable, or the [headerSliverBuilder],
104/// a [SliverAppBar] that floats, using [SliverAppBar.floating] will not be
105/// triggered to float over the inner scroll view, or [body], automatically.
106///
107/// This is because a floating app bar uses the scroll offset of its own
108/// [Scrollable] to dictate the floating action. Being two separate inner and
109/// outer [Scrollable]s, a [SliverAppBar] in the outer header is not aware of
110/// changes in the scroll offset of the inner body.
111///
112/// In order to float the outer, use [NestedScrollView.floatHeaderSlivers]. When
113/// set to true, the nested scrolling coordinator will prioritize floating in
114/// the header slivers before applying the remaining drag to the body.
115///
116/// Furthermore, the `floatHeaderSlivers` flag should also be used when using an
117/// app bar that is floating, pinned, and has an expanded height. In this
118/// configuration, the flexible space of the app bar will open and collapse,
119/// while the primary portion of the app bar remains pinned.
120///
121/// {@tool dartpad}
122/// This simple example shows a [NestedScrollView] whose header contains a
123/// floating [SliverAppBar]. By using the [floatHeaderSlivers] property, the
124/// floating behavior is coordinated between the outer and inner [Scrollable]s,
125/// so it behaves as it would in a single scrollable.
126///
127/// ** See code in examples/api/lib/widgets/nested_scroll_view/nested_scroll_view.1.dart **
128/// {@end-tool}
129///
130/// ### Snapping [SliverAppBar]s
131///
132/// Floating [SliverAppBar]s also have the option to perform a snapping animation.
133/// If [SliverAppBar.snap] is true, then a scroll that exposes the floating app
134/// bar will trigger an animation that slides the entire app bar into view.
135/// Similarly if a scroll dismisses the app bar, the animation will slide the
136/// app bar completely out of view.
137///
138/// It is possible with a [NestedScrollView] to perform just the snapping
139/// animation without floating the app bar in and out. By not using the
140/// [NestedScrollView.floatHeaderSlivers], the app bar will snap in and out
141/// without floating.
142///
143/// The [SliverAppBar.snap] animation should be used in conjunction with the
144/// [SliverOverlapAbsorber] and [SliverOverlapInjector] widgets when
145/// implemented in a [NestedScrollView]. These widgets take any overlapping
146/// behavior of the [SliverAppBar] in the header and redirect it to the
147/// [SliverOverlapInjector] in the body. If it is missing, then it is possible
148/// for the nested "inner" scroll view below to end up under the [SliverAppBar]
149/// even when the inner scroll view thinks it has not been scrolled.
150///
151/// {@tool dartpad}
152/// This simple example shows a [NestedScrollView] whose header contains a
153/// snapping, floating [SliverAppBar]. _Without_ setting any additional flags,
154/// e.g [NestedScrollView.floatHeaderSlivers], the [SliverAppBar] will animate
155/// in and out without floating. The [SliverOverlapAbsorber] and
156/// [SliverOverlapInjector] maintain the proper alignment between the two
157/// separate scroll views.
158///
159/// ** See code in examples/api/lib/widgets/nested_scroll_view/nested_scroll_view.2.dart **
160/// {@end-tool}
161///
162/// ### Snapping and Floating [SliverAppBar]s
163///
164// See https://github.com/flutter/flutter/issues/59189
165/// Currently, [NestedScrollView] does not support simultaneously floating and
166/// snapping the outer scrollable, e.g. when using [SliverAppBar.floating] &
167/// [SliverAppBar.snap] at the same time.
168///
169/// ### Stretching [SliverAppBar]s
170///
171// See https://github.com/flutter/flutter/issues/54059
172/// Currently, [NestedScrollView] does not support stretching the outer
173/// scrollable, e.g. when using [SliverAppBar.stretch].
174///
175/// See also:
176///
177/// * [SliverAppBar], for examples on different configurations like floating,
178/// pinned and snap behaviors.
179/// * [SliverOverlapAbsorber], a sliver that wraps another, forcing its layout
180/// extent to be treated as overlap.
181/// * [SliverOverlapInjector], a sliver that has a sliver geometry based on
182/// the values stored in a [SliverOverlapAbsorberHandle].
183class NestedScrollView extends StatefulWidget {
184 /// Creates a nested scroll view.
185 ///
186 /// The [reverse], [headerSliverBuilder], and [body] arguments must not be
187 /// null.
188 const NestedScrollView({
189 super.key,
190 this.controller,
191 this.scrollDirection = Axis.vertical,
192 this.reverse = false,
193 this.physics,
194 required this.headerSliverBuilder,
195 required this.body,
196 this.dragStartBehavior = DragStartBehavior.start,
197 this.floatHeaderSlivers = false,
198 this.clipBehavior = Clip.hardEdge,
199 this.hitTestBehavior = HitTestBehavior.opaque,
200 this.restorationId,
201 this.scrollBehavior,
202 });
203
204 /// An object that can be used to control the position to which the outer
205 /// scroll view is scrolled.
206 final ScrollController? controller;
207
208 /// {@macro flutter.widgets.scroll_view.scrollDirection}
209 ///
210 /// This property only applies to the [Axis] of the outer scroll view,
211 /// composed of the slivers returned from [headerSliverBuilder]. Since the
212 /// inner scroll view is not directly configured by the [NestedScrollView],
213 /// for the axes to match, configure the scroll view of the [body] the same
214 /// way if they are expected to scroll in the same orientation. This allows
215 /// for flexible configurations of the NestedScrollView.
216 final Axis scrollDirection;
217
218 /// Whether the scroll view scrolls in the reading direction.
219 ///
220 /// For example, if the reading direction is left-to-right and
221 /// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from
222 /// left to right when [reverse] is false and from right to left when
223 /// [reverse] is true.
224 ///
225 /// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view
226 /// scrolls from top to bottom when [reverse] is false and from bottom to top
227 /// when [reverse] is true.
228 ///
229 /// This property only applies to the outer scroll view, composed of the
230 /// slivers returned from [headerSliverBuilder]. Since the inner scroll view
231 /// is not directly configured by the [NestedScrollView]. For both to scroll
232 /// in reverse, configure the scroll view of the [body] the same way if they
233 /// are expected to match. This allows for flexible configurations of the
234 /// NestedScrollView.
235 ///
236 /// Defaults to false.
237 final bool reverse;
238
239 /// How the scroll view should respond to user input.
240 ///
241 /// For example, determines how the scroll view continues to animate after the
242 /// user stops dragging the scroll view (providing a custom implementation of
243 /// [ScrollPhysics.createBallisticSimulation] allows this particular aspect of
244 /// the physics to be overridden).
245 ///
246 /// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the
247 /// [ScrollPhysics] provided by that behavior will take precedence after
248 /// [physics].
249 ///
250 /// Defaults to matching platform conventions.
251 ///
252 /// The [ScrollPhysics.applyBoundaryConditions] implementation of the provided
253 /// object should not allow scrolling outside the scroll extent range
254 /// described by the [ScrollMetrics.minScrollExtent] and
255 /// [ScrollMetrics.maxScrollExtent] properties passed to that method. If that
256 /// invariant is not maintained, the nested scroll view may respond to user
257 /// scrolling erratically.
258 ///
259 /// This property only applies to the outer scroll view, composed of the
260 /// slivers returned from [headerSliverBuilder]. Since the inner scroll view
261 /// is not directly configured by the [NestedScrollView]. For both to scroll
262 /// with the same [ScrollPhysics], configure the scroll view of the [body]
263 /// the same way if they are expected to match, or use a [ScrollBehavior] as
264 /// an ancestor so both the inner and outer scroll views inherit the same
265 /// [ScrollPhysics]. This allows for flexible configurations of the
266 /// NestedScrollView.
267 ///
268 /// The [ScrollPhysics] also determine whether or not the [NestedScrollView]
269 /// can accept input from the user to change the scroll offset. For example,
270 /// [NeverScrollableScrollPhysics] typically will not allow the user to drag a
271 /// scroll view, but in this case, if one of the two scroll views can be
272 /// dragged, then dragging will be allowed. Configuring both scroll views with
273 /// [NeverScrollableScrollPhysics] will disallow dragging in this case.
274 final ScrollPhysics? physics;
275
276 /// A builder for any widgets that are to precede the inner scroll views (as
277 /// given by [body]).
278 ///
279 /// Typically this is used to create a [SliverAppBar] with a [TabBar].
280 final NestedScrollViewHeaderSliversBuilder headerSliverBuilder;
281
282 /// The widget to show inside the [NestedScrollView].
283 ///
284 /// Typically this will be [TabBarView].
285 ///
286 /// The [body] is built in a context that provides a [PrimaryScrollController]
287 /// that interacts with the [NestedScrollView]'s scroll controller. Any
288 /// [ListView] or other [Scrollable]-based widget inside the [body] that is
289 /// intended to scroll with the [NestedScrollView] should therefore not be
290 /// given an explicit [ScrollController], instead allowing it to default to
291 /// the [PrimaryScrollController] provided by the [NestedScrollView].
292 final Widget body;
293
294 /// {@macro flutter.widgets.scrollable.dragStartBehavior}
295 final DragStartBehavior dragStartBehavior;
296
297 /// Whether or not the [NestedScrollView]'s coordinator should prioritize the
298 /// outer scrollable over the inner when scrolling back.
299 ///
300 /// This is useful for an outer scrollable containing a [SliverAppBar] that
301 /// is expected to float.
302 final bool floatHeaderSlivers;
303
304 /// {@macro flutter.material.Material.clipBehavior}
305 ///
306 /// Defaults to [Clip.hardEdge].
307 final Clip clipBehavior;
308
309 /// {@macro flutter.widgets.scrollable.hitTestBehavior}
310 ///
311 /// Defaults to [HitTestBehavior.opaque].
312 final HitTestBehavior hitTestBehavior;
313
314 /// {@macro flutter.widgets.scrollable.restorationId}
315 final String? restorationId;
316
317 /// {@macro flutter.widgets.scrollable.scrollBehavior}
318 ///
319 /// The [ScrollBehavior] of the inherited [ScrollConfiguration] will be
320 /// modified by default to not apply a [Scrollbar]. This is because the
321 /// NestedScrollView cannot assume the configuration of the outer and inner
322 /// [Scrollable] widgets, particularly whether to treat them as one scrollable,
323 /// or separate and desirous of unique behaviors.
324 final ScrollBehavior? scrollBehavior;
325
326 /// Returns the [SliverOverlapAbsorberHandle] of the nearest ancestor
327 /// [NestedScrollView].
328 ///
329 /// This is necessary to configure the [SliverOverlapAbsorber] and
330 /// [SliverOverlapInjector] widgets.
331 ///
332 /// For sample code showing how to use this method, see the [NestedScrollView]
333 /// documentation.
334 static SliverOverlapAbsorberHandle sliverOverlapAbsorberHandleFor(BuildContext context) {
335 final _InheritedNestedScrollView? target = context
336 .dependOnInheritedWidgetOfExactType<_InheritedNestedScrollView>();
337 assert(
338 target != null,
339 'NestedScrollView.sliverOverlapAbsorberHandleFor must be called with a context that contains a NestedScrollView.',
340 );
341 return target!.state._absorberHandle;
342 }
343
344 List<Widget> _buildSlivers(
345 BuildContext context,
346 ScrollController innerController,
347 bool bodyIsScrolled,
348 ) {
349 return <Widget>[
350 ...headerSliverBuilder(context, bodyIsScrolled),
351 SliverFillRemaining(
352 // The inner (body) scroll view must use this scroll controller so that
353 // the independent scroll positions can be kept in sync.
354 child: PrimaryScrollController(
355 // The inner scroll view should always inherit this
356 // PrimaryScrollController, on every platform.
357 automaticallyInheritForPlatforms: TargetPlatform.values.toSet(),
358 // `PrimaryScrollController.scrollDirection` is not set, and so it is
359 // restricted to the default Axis.vertical.
360 // Ideally the inner and outer views would have the same
361 // scroll direction, and so we could assume
362 // `NestedScrollView.scrollDirection` for the PrimaryScrollController,
363 // but use cases already exist where the axes are mismatched.
364 // https://github.com/flutter/flutter/issues/102001
365 controller: innerController,
366 child: body,
367 ),
368 ),
369 ];
370 }
371
372 @override
373 NestedScrollViewState createState() => NestedScrollViewState();
374}
375
376/// The [State] for a [NestedScrollView].
377///
378/// The [ScrollController]s, [innerController] and [outerController], of the
379/// [NestedScrollView]'s children may be accessed through its state. This is
380/// useful for obtaining respective scroll positions in the [NestedScrollView].
381///
382/// If you want to access the inner or outer scroll controller of a
383/// [NestedScrollView], you can get its [NestedScrollViewState] by supplying a
384/// `GlobalKey<NestedScrollViewState>` to the [NestedScrollView.key] parameter).
385///
386/// {@tool dartpad}
387/// [NestedScrollViewState] can be obtained using a [GlobalKey].
388/// Using the following setup, you can access the inner scroll controller
389/// using `globalKey.currentState.innerController`.
390///
391/// ** See code in examples/api/lib/widgets/nested_scroll_view/nested_scroll_view_state.0.dart **
392/// {@end-tool}
393class NestedScrollViewState extends State<NestedScrollView> {
394 final SliverOverlapAbsorberHandle _absorberHandle = SliverOverlapAbsorberHandle();
395
396 /// The [ScrollController] provided to the [ScrollView] in
397 /// [NestedScrollView.body].
398 ///
399 /// Manipulating the [ScrollPosition] of this controller pushes the outer
400 /// header sliver(s) up and out of view. The position of the [outerController]
401 /// will be set to [ScrollPosition.maxScrollExtent], unless you use
402 /// [ScrollPosition.setPixels].
403 ///
404 /// See also:
405 ///
406 /// * [outerController], which exposes the [ScrollController] used by the
407 /// sliver(s) contained in [NestedScrollView.headerSliverBuilder].
408 ScrollController get innerController => _coordinator!._innerController;
409
410 /// The [ScrollController] provided to the [ScrollView] in
411 /// [NestedScrollView.headerSliverBuilder].
412 ///
413 /// This is equivalent to [NestedScrollView.controller], if provided.
414 ///
415 /// Manipulating the [ScrollPosition] of this controller pushes the inner body
416 /// sliver(s) down. The position of the [innerController] will be set to
417 /// [ScrollPosition.minScrollExtent], unless you use
418 /// [ScrollPosition.setPixels]. Visually, the inner body will be scrolled to
419 /// its beginning.
420 ///
421 /// See also:
422 ///
423 /// * [innerController], which exposes the [ScrollController] used by the
424 /// [ScrollView] contained in [NestedScrollView.body].
425 ScrollController get outerController => _coordinator!._outerController;
426
427 _NestedScrollCoordinator? _coordinator;
428
429 @protected
430 @override
431 void initState() {
432 super.initState();
433 _coordinator = _NestedScrollCoordinator(
434 this,
435 widget.controller,
436 _handleHasScrolledBodyChanged,
437 widget.floatHeaderSlivers,
438 );
439 }
440
441 @protected
442 @override
443 void didChangeDependencies() {
444 super.didChangeDependencies();
445 _coordinator!.setParent(widget.controller);
446 }
447
448 @protected
449 @override
450 void didUpdateWidget(NestedScrollView oldWidget) {
451 super.didUpdateWidget(oldWidget);
452 if (oldWidget.controller != widget.controller) {
453 _coordinator!.setParent(widget.controller);
454 }
455 }
456
457 @protected
458 @override
459 void dispose() {
460 _coordinator!.dispose();
461 _coordinator = null;
462 _absorberHandle.dispose();
463 super.dispose();
464 }
465
466 bool? _lastHasScrolledBody;
467
468 void _handleHasScrolledBodyChanged() {
469 if (!mounted) {
470 return;
471 }
472 final bool newHasScrolledBody = _coordinator!.hasScrolledBody;
473 if (_lastHasScrolledBody != newHasScrolledBody) {
474 setState(() {
475 // _coordinator.hasScrolledBody changed (we use it in the build method)
476 // (We record _lastHasScrolledBody in the build() method, rather than in
477 // this setState call, because the build() method may be called more
478 // often than just from here, and we want to only call setState when the
479 // new value is different than the last built value.)
480 });
481 }
482 }
483
484 @protected
485 @override
486 Widget build(BuildContext context) {
487 final ScrollPhysics scrollPhysics =
488 widget.physics?.applyTo(const ClampingScrollPhysics()) ??
489 widget.scrollBehavior?.getScrollPhysics(context).applyTo(const ClampingScrollPhysics()) ??
490 const ClampingScrollPhysics();
491
492 return _InheritedNestedScrollView(
493 state: this,
494 child: Builder(
495 builder: (BuildContext context) {
496 _lastHasScrolledBody = _coordinator!.hasScrolledBody;
497 return _NestedScrollViewCustomScrollView(
498 dragStartBehavior: widget.dragStartBehavior,
499 scrollDirection: widget.scrollDirection,
500 reverse: widget.reverse,
501 physics: scrollPhysics,
502 scrollBehavior:
503 widget.scrollBehavior ??
504 ScrollConfiguration.of(context).copyWith(scrollbars: false),
505 controller: _coordinator!._outerController,
506 slivers: widget._buildSlivers(
507 context,
508 _coordinator!._innerController,
509 _lastHasScrolledBody!,
510 ),
511 handle: _absorberHandle,
512 clipBehavior: widget.clipBehavior,
513 restorationId: widget.restorationId,
514 hitTestBehavior: widget.hitTestBehavior,
515 );
516 },
517 ),
518 );
519 }
520}
521
522class _NestedScrollViewCustomScrollView extends CustomScrollView {
523 const _NestedScrollViewCustomScrollView({
524 required super.scrollDirection,
525 required super.reverse,
526 required ScrollPhysics super.physics,
527 required ScrollBehavior super.scrollBehavior,
528 required ScrollController super.controller,
529 required super.slivers,
530 required this.handle,
531 required super.clipBehavior,
532 super.hitTestBehavior,
533 super.dragStartBehavior,
534 super.restorationId,
535 });
536
537 final SliverOverlapAbsorberHandle handle;
538
539 @override
540 Widget buildViewport(
541 BuildContext context,
542 ViewportOffset offset,
543 AxisDirection axisDirection,
544 List<Widget> slivers,
545 ) {
546 assert(!shrinkWrap);
547 return NestedScrollViewViewport(
548 axisDirection: axisDirection,
549 offset: offset,
550 slivers: slivers,
551 handle: handle,
552 clipBehavior: clipBehavior,
553 );
554 }
555}
556
557class _InheritedNestedScrollView extends InheritedWidget {
558 const _InheritedNestedScrollView({required this.state, required super.child});
559
560 final NestedScrollViewState state;
561
562 @override
563 bool updateShouldNotify(_InheritedNestedScrollView old) => state != old.state;
564}
565
566class _NestedScrollMetrics extends FixedScrollMetrics {
567 _NestedScrollMetrics({
568 required super.minScrollExtent,
569 required super.maxScrollExtent,
570 required super.pixels,
571 required super.viewportDimension,
572 required super.axisDirection,
573 required super.devicePixelRatio,
574 required this.minRange,
575 required this.maxRange,
576 required this.correctionOffset,
577 });
578
579 @override
580 _NestedScrollMetrics copyWith({
581 double? minScrollExtent,
582 double? maxScrollExtent,
583 double? pixels,
584 double? viewportDimension,
585 AxisDirection? axisDirection,
586 double? devicePixelRatio,
587 double? minRange,
588 double? maxRange,
589 double? correctionOffset,
590 }) {
591 return _NestedScrollMetrics(
592 minScrollExtent: minScrollExtent ?? (hasContentDimensions ? this.minScrollExtent : null),
593 maxScrollExtent: maxScrollExtent ?? (hasContentDimensions ? this.maxScrollExtent : null),
594 pixels: pixels ?? (hasPixels ? this.pixels : null),
595 viewportDimension:
596 viewportDimension ?? (hasViewportDimension ? this.viewportDimension : null),
597 axisDirection: axisDirection ?? this.axisDirection,
598 devicePixelRatio: devicePixelRatio ?? this.devicePixelRatio,
599 minRange: minRange ?? this.minRange,
600 maxRange: maxRange ?? this.maxRange,
601 correctionOffset: correctionOffset ?? this.correctionOffset,
602 );
603 }
604
605 final double minRange;
606
607 final double maxRange;
608
609 final double correctionOffset;
610}
611
612typedef _NestedScrollActivityGetter = ScrollActivity Function(_NestedScrollPosition position);
613
614class _NestedScrollCoordinator implements ScrollActivityDelegate, ScrollHoldController {
615 _NestedScrollCoordinator(
616 this._state,
617 this._parent,
618 this._onHasScrolledBodyChanged,
619 this._floatHeaderSlivers,
620 ) {
621 assert(debugMaybeDispatchCreated('widgets', '_NestedScrollCoordinator', this));
622 final double initialScrollOffset = _parent?.initialScrollOffset ?? 0.0;
623 _outerController = _NestedScrollController(
624 this,
625 initialScrollOffset: initialScrollOffset,
626 debugLabel: 'outer',
627 );
628 _innerController = _NestedScrollController(this, debugLabel: 'inner');
629 }
630
631 final NestedScrollViewState _state;
632 ScrollController? _parent;
633 final VoidCallback _onHasScrolledBodyChanged;
634 final bool _floatHeaderSlivers;
635
636 late _NestedScrollController _outerController;
637 late _NestedScrollController _innerController;
638
639 bool get outOfRange {
640 return (_outerPosition?.outOfRange ?? false) ||
641 _innerPositions.any((_NestedScrollPosition position) => position.outOfRange);
642 }
643
644 _NestedScrollPosition? get _outerPosition {
645 if (!_outerController.hasClients) {
646 return null;
647 }
648 return _outerController.nestedPositions.single;
649 }
650
651 Iterable<_NestedScrollPosition> get _innerPositions {
652 return _innerController.nestedPositions;
653 }
654
655 bool get canScrollBody {
656 final _NestedScrollPosition? outer = _outerPosition;
657 if (outer == null) {
658 return true;
659 }
660 return outer.haveDimensions && outer.extentAfter == 0.0;
661 }
662
663 bool get hasScrolledBody {
664 for (final _NestedScrollPosition position in _innerPositions) {
665 if (!position.hasContentDimensions || !position.hasPixels) {
666 // It's possible that NestedScrollView built twice before layout phase
667 // in the same frame. This can happen when the FocusManager schedules a microTask
668 // that marks NestedScrollView dirty during the warm up frame.
669 // https://github.com/flutter/flutter/pull/75308
670 continue;
671 } else if (position.pixels > position.minScrollExtent) {
672 return true;
673 }
674 }
675 return false;
676 }
677
678 void updateShadow() {
679 _onHasScrolledBodyChanged();
680 }
681
682 ScrollDirection get userScrollDirection => _userScrollDirection;
683 ScrollDirection _userScrollDirection = ScrollDirection.idle;
684
685 void updateUserScrollDirection(ScrollDirection value) {
686 if (userScrollDirection == value) {
687 return;
688 }
689 _userScrollDirection = value;
690 _outerPosition!.didUpdateScrollDirection(value);
691 for (final _NestedScrollPosition position in _innerPositions) {
692 position.didUpdateScrollDirection(value);
693 }
694 }
695
696 ScrollDragController? _currentDrag;
697
698 void beginActivity(
699 ScrollActivity newOuterActivity,
700 _NestedScrollActivityGetter innerActivityGetter,
701 ) {
702 _outerPosition!.beginActivity(newOuterActivity);
703 bool scrolling = newOuterActivity.isScrolling;
704 for (final _NestedScrollPosition position in _innerPositions) {
705 final ScrollActivity newInnerActivity = innerActivityGetter(position);
706 position.beginActivity(newInnerActivity);
707 scrolling = scrolling && newInnerActivity.isScrolling;
708 }
709 _currentDrag?.dispose();
710 _currentDrag = null;
711 if (!scrolling) {
712 updateUserScrollDirection(ScrollDirection.idle);
713 }
714 }
715
716 @override
717 AxisDirection get axisDirection => _outerPosition!.axisDirection;
718
719 static IdleScrollActivity _createIdleScrollActivity(_NestedScrollPosition position) {
720 return IdleScrollActivity(position);
721 }
722
723 @override
724 void goIdle() {
725 beginActivity(_createIdleScrollActivity(_outerPosition!), _createIdleScrollActivity);
726 }
727
728 @override
729 void goBallistic(double velocity) {
730 beginActivity(createOuterBallisticScrollActivity(velocity), (_NestedScrollPosition position) {
731 return createInnerBallisticScrollActivity(position, velocity);
732 });
733 }
734
735 ScrollActivity createOuterBallisticScrollActivity(double velocity) {
736 // This function creates a ballistic scroll for the outer scrollable.
737 //
738 // It assumes that the outer scrollable can't be overscrolled, and sets up a
739 // ballistic scroll over the combined space of the innerPositions and the
740 // outerPosition.
741
742 // First we must pick a representative inner position that we will care
743 // about. This is somewhat arbitrary. Ideally we'd pick the one that is "in
744 // the center" but there isn't currently a good way to do that so we
745 // arbitrarily pick the one that is the furthest away from the infinity we
746 // are heading towards.
747 _NestedScrollPosition? innerPosition;
748 if (velocity != 0.0) {
749 for (final _NestedScrollPosition position in _innerPositions) {
750 if (innerPosition != null) {
751 if (velocity > 0.0) {
752 if (innerPosition.pixels < position.pixels) {
753 continue;
754 }
755 } else {
756 assert(velocity < 0.0);
757 if (innerPosition.pixels > position.pixels) {
758 continue;
759 }
760 }
761 }
762 innerPosition = position;
763 }
764 }
765
766 if (innerPosition == null) {
767 // It's either just us or a velocity=0 situation.
768 return _outerPosition!.createBallisticScrollActivity(
769 _outerPosition!.physics.createBallisticSimulation(_outerPosition!, velocity),
770 mode: _NestedBallisticScrollActivityMode.independent,
771 );
772 }
773
774 final _NestedScrollMetrics metrics = _getMetrics(innerPosition, velocity);
775
776 return _outerPosition!.createBallisticScrollActivity(
777 _outerPosition!.physics.createBallisticSimulation(metrics, velocity),
778 mode: _NestedBallisticScrollActivityMode.outer,
779 metrics: metrics,
780 );
781 }
782
783 @protected
784 ScrollActivity createInnerBallisticScrollActivity(
785 _NestedScrollPosition position,
786 double velocity,
787 ) {
788 return position.createBallisticScrollActivity(
789 position.physics.createBallisticSimulation(_getMetrics(position, velocity), velocity),
790 mode: _NestedBallisticScrollActivityMode.inner,
791 );
792 }
793
794 _NestedScrollMetrics _getMetrics(_NestedScrollPosition innerPosition, double velocity) {
795 double pixels, minRange, maxRange, correctionOffset;
796 double extra = 0.0;
797 if (innerPosition.pixels == innerPosition.minScrollExtent) {
798 pixels = clampDouble(
799 _outerPosition!.pixels,
800 _outerPosition!.minScrollExtent,
801 _outerPosition!.maxScrollExtent,
802 ); // TODO(ianh): gracefully handle out-of-range outer positions
803 minRange = _outerPosition!.minScrollExtent;
804 maxRange = _outerPosition!.maxScrollExtent;
805 assert(minRange <= maxRange);
806 correctionOffset = 0.0;
807 } else {
808 assert(innerPosition.pixels != innerPosition.minScrollExtent);
809 if (innerPosition.pixels < innerPosition.minScrollExtent) {
810 pixels =
811 innerPosition.pixels - innerPosition.minScrollExtent + _outerPosition!.minScrollExtent;
812 } else {
813 assert(innerPosition.pixels > innerPosition.minScrollExtent);
814 pixels =
815 innerPosition.pixels - innerPosition.minScrollExtent + _outerPosition!.maxScrollExtent;
816 }
817 if ((velocity > 0.0) && (innerPosition.pixels > innerPosition.minScrollExtent)) {
818 // This handles going forward (fling up) and inner list is scrolled past
819 // zero. We want to grab the extra pixels immediately to shrink.
820 extra = _outerPosition!.maxScrollExtent - _outerPosition!.pixels;
821 assert(extra >= 0.0);
822 minRange = pixels;
823 maxRange = pixels + extra;
824 assert(minRange <= maxRange);
825 correctionOffset = _outerPosition!.pixels - pixels;
826 } else if ((velocity < 0.0) && (innerPosition.pixels < innerPosition.minScrollExtent)) {
827 // This handles going backward (fling down) and inner list is
828 // underscrolled. We want to grab the extra pixels immediately to grow.
829 extra = _outerPosition!.pixels - _outerPosition!.minScrollExtent;
830 assert(extra >= 0.0);
831 minRange = pixels - extra;
832 maxRange = pixels;
833 assert(minRange <= maxRange);
834 correctionOffset = _outerPosition!.pixels - pixels;
835 } else {
836 // This handles going forward (fling up) and inner list is
837 // underscrolled, OR, going backward (fling down) and inner list is
838 // scrolled past zero. We want to skip the pixels we don't need to grow
839 // or shrink over.
840 if (velocity > 0.0) {
841 // shrinking
842 extra = _outerPosition!.minScrollExtent - _outerPosition!.pixels;
843 } else if (velocity < 0.0) {
844 // growing
845 extra =
846 _outerPosition!.pixels -
847 (_outerPosition!.maxScrollExtent - _outerPosition!.minScrollExtent);
848 }
849 assert(extra <= 0.0);
850 minRange = _outerPosition!.minScrollExtent;
851 maxRange = _outerPosition!.maxScrollExtent + extra;
852 assert(minRange <= maxRange);
853 correctionOffset = 0.0;
854 }
855 }
856 return _NestedScrollMetrics(
857 minScrollExtent: _outerPosition!.minScrollExtent,
858 maxScrollExtent:
859 _outerPosition!.maxScrollExtent +
860 innerPosition.maxScrollExtent -
861 innerPosition.minScrollExtent +
862 extra,
863 pixels: pixels,
864 viewportDimension: _outerPosition!.viewportDimension,
865 axisDirection: _outerPosition!.axisDirection,
866 minRange: minRange,
867 maxRange: maxRange,
868 correctionOffset: correctionOffset,
869 devicePixelRatio: _outerPosition!.devicePixelRatio,
870 );
871 }
872
873 double unnestOffset(double value, _NestedScrollPosition source) {
874 if (source == _outerPosition) {
875 return clampDouble(value, _outerPosition!.minScrollExtent, _outerPosition!.maxScrollExtent);
876 }
877 if (value < source.minScrollExtent) {
878 return value - source.minScrollExtent + _outerPosition!.minScrollExtent;
879 }
880 return value - source.minScrollExtent + _outerPosition!.maxScrollExtent;
881 }
882
883 double nestOffset(double value, _NestedScrollPosition target) {
884 if (target == _outerPosition) {
885 return clampDouble(value, _outerPosition!.minScrollExtent, _outerPosition!.maxScrollExtent);
886 }
887 if (value < _outerPosition!.minScrollExtent) {
888 return value - _outerPosition!.minScrollExtent + target.minScrollExtent;
889 }
890 if (value > _outerPosition!.maxScrollExtent) {
891 return value - _outerPosition!.maxScrollExtent + target.minScrollExtent;
892 }
893 return target.minScrollExtent;
894 }
895
896 void updateCanDrag() {
897 if (!_outerPosition!.haveDimensions) {
898 return;
899 }
900 bool innerCanDrag = false;
901 for (final _NestedScrollPosition position in _innerPositions) {
902 if (!position.haveDimensions) {
903 return;
904 }
905 innerCanDrag =
906 innerCanDrag
907 // This refers to the physics of the actual inner scroll position, not
908 // the whole NestedScrollView, since it is possible to have different
909 // ScrollPhysics for the inner and outer positions.
910 ||
911 position.physics.shouldAcceptUserOffset(position);
912 }
913 _outerPosition!.updateCanDrag(innerCanDrag);
914 }
915
916 Future<void> animateTo(double to, {required Duration duration, required Curve curve}) async {
917 final DrivenScrollActivity outerActivity = _outerPosition!.createDrivenScrollActivity(
918 nestOffset(to, _outerPosition!),
919 duration,
920 curve,
921 );
922 final List<Future<void>> resultFutures = <Future<void>>[outerActivity.done];
923 beginActivity(outerActivity, (_NestedScrollPosition position) {
924 final DrivenScrollActivity innerActivity = position.createDrivenScrollActivity(
925 nestOffset(to, position),
926 duration,
927 curve,
928 );
929 resultFutures.add(innerActivity.done);
930 return innerActivity;
931 });
932 await Future.wait<void>(resultFutures);
933 }
934
935 void jumpTo(double to) {
936 goIdle();
937 _outerPosition!.localJumpTo(nestOffset(to, _outerPosition!));
938 for (final _NestedScrollPosition position in _innerPositions) {
939 position.localJumpTo(nestOffset(to, position));
940 }
941 goBallistic(0.0);
942 }
943
944 void pointerScroll(double delta) {
945 // If an update is made to pointer scrolling here, consider if the same
946 // (or similar) change should be made in
947 // ScrollPositionWithSingleContext.pointerScroll.
948 if (delta == 0.0) {
949 goBallistic(0.0);
950 return;
951 }
952
953 goIdle();
954 updateUserScrollDirection(delta < 0.0 ? ScrollDirection.forward : ScrollDirection.reverse);
955
956 // Handle notifications. Even if only one position actually receives
957 // the delta, the NestedScrollView's intention is to treat multiple
958 // ScrollPositions as one.
959 _outerPosition!.isScrollingNotifier.value = true;
960 _outerPosition!.didStartScroll();
961 for (final _NestedScrollPosition position in _innerPositions) {
962 position.isScrollingNotifier.value = true;
963 position.didStartScroll();
964 }
965
966 if (_innerPositions.isEmpty) {
967 // Does not enter overscroll.
968 _outerPosition!.applyClampedPointerSignalUpdate(delta);
969 } else if (delta > 0.0) {
970 // Dragging "up" - delta is positive
971 // Prioritize getting rid of any inner overscroll, and then the outer
972 // view, so that the app bar will scroll out of the way asap.
973 double outerDelta = delta;
974 for (final _NestedScrollPosition position in _innerPositions) {
975 if (position.pixels < 0.0) {
976 // This inner position is in overscroll.
977 final double potentialOuterDelta = position.applyClampedPointerSignalUpdate(delta);
978 // In case there are multiple positions in varying states of
979 // overscroll, the first to 'reach' the outer view above takes
980 // precedence.
981 outerDelta = math.max(outerDelta, potentialOuterDelta);
982 }
983 }
984 if (outerDelta != 0.0) {
985 final double innerDelta = _outerPosition!.applyClampedPointerSignalUpdate(outerDelta);
986 if (innerDelta != 0.0) {
987 for (final _NestedScrollPosition position in _innerPositions) {
988 position.applyClampedPointerSignalUpdate(innerDelta);
989 }
990 }
991 }
992 } else {
993 // Dragging "down" - delta is negative
994 double innerDelta = delta;
995 // Apply delta to the outer header first if it is configured to float.
996 if (_floatHeaderSlivers) {
997 innerDelta = _outerPosition!.applyClampedPointerSignalUpdate(delta);
998 }
999
1000 if (innerDelta != 0.0) {
1001 // Apply the innerDelta, if we have not floated in the outer scrollable,
1002 // any leftover delta after this will be passed on to the outer
1003 // scrollable by the outerDelta.
1004 double outerDelta = 0.0; // it will go negative if it changes
1005 for (final _NestedScrollPosition position in _innerPositions) {
1006 final double overscroll = position.applyClampedPointerSignalUpdate(innerDelta);
1007 outerDelta = math.min(outerDelta, overscroll);
1008 }
1009 if (outerDelta != 0.0) {
1010 _outerPosition!.applyClampedPointerSignalUpdate(outerDelta);
1011 }
1012 }
1013 }
1014
1015 _outerPosition!.didEndScroll();
1016 for (final _NestedScrollPosition position in _innerPositions) {
1017 position.didEndScroll();
1018 }
1019 goBallistic(0.0);
1020 }
1021
1022 @override
1023 double setPixels(double newPixels) {
1024 assert(false);
1025 return 0.0;
1026 }
1027
1028 ScrollHoldController hold(VoidCallback holdCancelCallback) {
1029 beginActivity(
1030 HoldScrollActivity(delegate: _outerPosition!, onHoldCanceled: holdCancelCallback),
1031 (_NestedScrollPosition position) => HoldScrollActivity(delegate: position),
1032 );
1033 return this;
1034 }
1035
1036 @override
1037 void cancel() {
1038 goBallistic(0.0);
1039 }
1040
1041 Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) {
1042 final ScrollDragController drag = ScrollDragController(
1043 delegate: this,
1044 details: details,
1045 onDragCanceled: dragCancelCallback,
1046 );
1047 beginActivity(
1048 DragScrollActivity(_outerPosition!, drag),
1049 (_NestedScrollPosition position) => DragScrollActivity(position, drag),
1050 );
1051 assert(_currentDrag == null);
1052 _currentDrag = drag;
1053 return drag;
1054 }
1055
1056 @override
1057 void applyUserOffset(double delta) {
1058 updateUserScrollDirection(delta > 0.0 ? ScrollDirection.forward : ScrollDirection.reverse);
1059 assert(delta != 0.0);
1060 if (_innerPositions.isEmpty) {
1061 _outerPosition!.applyFullDragUpdate(delta);
1062 } else if (delta < 0.0) {
1063 // Dragging "up"
1064 // Prioritize getting rid of any inner overscroll, and then the outer
1065 // view, so that the app bar will scroll out of the way asap.
1066 double outerDelta = delta;
1067 for (final _NestedScrollPosition position in _innerPositions) {
1068 if (position.pixels < 0.0) {
1069 // This inner position is in overscroll.
1070 final double potentialOuterDelta = position.applyClampedDragUpdate(delta);
1071 // In case there are multiple positions in varying states of
1072 // overscroll, the first to 'reach' the outer view above takes
1073 // precedence.
1074 outerDelta = math.max(outerDelta, potentialOuterDelta);
1075 }
1076 }
1077 if (outerDelta.abs() > precisionErrorTolerance) {
1078 final double innerDelta = _outerPosition!.applyClampedDragUpdate(outerDelta);
1079 if (innerDelta != 0.0) {
1080 for (final _NestedScrollPosition position in _innerPositions) {
1081 position.applyFullDragUpdate(innerDelta);
1082 }
1083 }
1084 }
1085 } else {
1086 // Dragging "down" - delta is positive
1087 double innerDelta = delta;
1088 // Apply delta to the outer header first if it is configured to float.
1089 if (_floatHeaderSlivers) {
1090 innerDelta = _outerPosition!.applyClampedDragUpdate(delta);
1091 }
1092
1093 if (innerDelta != 0.0) {
1094 // Apply the innerDelta, if we have not floated in the outer scrollable,
1095 // any leftover delta after this will be passed on to the outer
1096 // scrollable by the outerDelta.
1097 double outerDelta = 0.0; // it will go positive if it changes
1098 final List<double> overscrolls = <double>[];
1099 final List<_NestedScrollPosition> innerPositions = _innerPositions.toList();
1100 for (final _NestedScrollPosition position in innerPositions) {
1101 final double overscroll = position.applyClampedDragUpdate(innerDelta);
1102 outerDelta = math.max(outerDelta, overscroll);
1103 overscrolls.add(overscroll);
1104 }
1105 if (outerDelta != 0.0) {
1106 outerDelta -= _outerPosition!.applyClampedDragUpdate(outerDelta);
1107 }
1108
1109 // Now deal with any overscroll
1110 for (int i = 0; i < innerPositions.length; ++i) {
1111 final double remainingDelta = overscrolls[i] - outerDelta;
1112 if (remainingDelta > 0.0) {
1113 innerPositions[i].applyFullDragUpdate(remainingDelta);
1114 }
1115 }
1116 }
1117 }
1118 }
1119
1120 void setParent(ScrollController? value) {
1121 _parent = value;
1122 updateParent();
1123 }
1124
1125 void updateParent() {
1126 _outerPosition?.setParent(_parent ?? PrimaryScrollController.maybeOf(_state.context));
1127 }
1128
1129 @mustCallSuper
1130 void dispose() {
1131 assert(debugMaybeDispatchDisposed(this));
1132 _currentDrag?.dispose();
1133 _currentDrag = null;
1134 _outerController.dispose();
1135 _innerController.dispose();
1136 }
1137
1138 @override
1139 String toString() =>
1140 '${objectRuntimeType(this, '_NestedScrollCoordinator')}(outer=$_outerController; inner=$_innerController)';
1141}
1142
1143class _NestedScrollController extends ScrollController {
1144 _NestedScrollController(this.coordinator, {super.initialScrollOffset, super.debugLabel});
1145
1146 final _NestedScrollCoordinator coordinator;
1147
1148 @override
1149 ScrollPosition createScrollPosition(
1150 ScrollPhysics physics,
1151 ScrollContext context,
1152 ScrollPosition? oldPosition,
1153 ) {
1154 return _NestedScrollPosition(
1155 coordinator: coordinator,
1156 physics: physics,
1157 context: context,
1158 initialPixels: initialScrollOffset,
1159 oldPosition: oldPosition,
1160 debugLabel: debugLabel,
1161 );
1162 }
1163
1164 @override
1165 void attach(ScrollPosition position) {
1166 assert(position is _NestedScrollPosition);
1167 super.attach(position);
1168 coordinator.updateParent();
1169 coordinator.updateCanDrag();
1170 position.addListener(_scheduleUpdateShadow);
1171 _scheduleUpdateShadow();
1172 }
1173
1174 @override
1175 void detach(ScrollPosition position) {
1176 assert(position is _NestedScrollPosition);
1177 (position as _NestedScrollPosition).setParent(null);
1178 position.removeListener(_scheduleUpdateShadow);
1179 super.detach(position);
1180 _scheduleUpdateShadow();
1181 }
1182
1183 void _scheduleUpdateShadow() {
1184 // We do this asynchronously for attach() so that the new position has had
1185 // time to be initialized, and we do it asynchronously for detach() and from
1186 // the position change notifications because those happen synchronously
1187 // during a frame, at a time where it's too late to call setState. Since the
1188 // result is usually animated, the lag incurred is no big deal.
1189 SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
1190 coordinator.updateShadow();
1191 }, debugLabel: 'NestedScrollController.updateShadow');
1192 }
1193
1194 Iterable<_NestedScrollPosition> get nestedPositions {
1195 return positions.cast<_NestedScrollPosition>();
1196 }
1197}
1198
1199// The _NestedScrollPosition is used by both the inner and outer viewports of a
1200// NestedScrollView. It tracks the offset to use for those viewports, and knows
1201// about the _NestedScrollCoordinator, so that when activities are triggered on
1202// this class, they can defer, or be influenced by, the coordinator.
1203class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDelegate {
1204 _NestedScrollPosition({
1205 required super.physics,
1206 required super.context,
1207 double initialPixels = 0.0,
1208 super.oldPosition,
1209 super.debugLabel,
1210 required this.coordinator,
1211 }) {
1212 if (!hasPixels) {
1213 correctPixels(initialPixels);
1214 }
1215 if (activity == null) {
1216 goIdle();
1217 }
1218 assert(activity != null);
1219 saveScrollOffset(); // in case we didn't restore but could, so that we don't restore it later
1220 }
1221
1222 final _NestedScrollCoordinator coordinator;
1223
1224 TickerProvider get vsync => context.vsync;
1225
1226 ScrollController? _parent;
1227
1228 void setParent(ScrollController? value) {
1229 _parent?.detach(this);
1230 _parent = value;
1231 _parent?.attach(this);
1232 }
1233
1234 @override
1235 AxisDirection get axisDirection => context.axisDirection;
1236
1237 @override
1238 void absorb(ScrollPosition other) {
1239 super.absorb(other);
1240 activity!.updateDelegate(this);
1241 }
1242
1243 @override
1244 void restoreScrollOffset() {
1245 if (coordinator.canScrollBody) {
1246 super.restoreScrollOffset();
1247 }
1248 }
1249
1250 // Returns the amount of delta that was not used.
1251 //
1252 // Positive delta means going down (exposing stuff above), negative delta
1253 // going up (exposing stuff below).
1254 double applyClampedDragUpdate(double delta) {
1255 assert(delta != 0.0);
1256 // If we are going towards the maxScrollExtent (negative scroll offset),
1257 // then the furthest we can be in the minScrollExtent direction is negative
1258 // infinity. For example, if we are already overscrolled, then scrolling to
1259 // reduce the overscroll should not disallow the overscroll.
1260 //
1261 // If we are going towards the minScrollExtent (positive scroll offset),
1262 // then the furthest we can be in the minScrollExtent direction is wherever
1263 // we are now, if we are already overscrolled (in which case pixels is less
1264 // than the minScrollExtent), or the minScrollExtent if we are not.
1265 //
1266 // In other words, we cannot, via applyClampedDragUpdate, _enter_ an
1267 // overscroll situation.
1268 //
1269 // An overscroll situation might be nonetheless entered via several means.
1270 // One is if the physics allow it, via applyFullDragUpdate (see below). An
1271 // overscroll situation can also be forced, e.g. if the scroll position is
1272 // artificially set using the scroll controller.
1273 final double min = delta < 0.0 ? -double.infinity : math.min(minScrollExtent, pixels);
1274 // The logic for max is equivalent but on the other side.
1275 final double max = delta > 0.0
1276 ? double.infinity
1277 // If pixels < 0.0, then we are currently in overscroll. The max should be
1278 // 0.0, representing the end of the overscrolled portion.
1279 : pixels < 0.0
1280 ? 0.0
1281 : math.max(maxScrollExtent, pixels);
1282 final double oldPixels = pixels;
1283 final double newPixels = clampDouble(pixels - delta, min, max);
1284 final double clampedDelta = newPixels - pixels;
1285 if (clampedDelta == 0.0) {
1286 return delta;
1287 }
1288 final double overscroll = physics.applyBoundaryConditions(this, newPixels);
1289 final double actualNewPixels = newPixels - overscroll;
1290 final double offset = actualNewPixels - oldPixels;
1291 if (offset != 0.0) {
1292 forcePixels(actualNewPixels);
1293 didUpdateScrollPositionBy(offset);
1294 }
1295
1296 final double result = delta + offset;
1297 if (result.abs() < precisionErrorTolerance) {
1298 return 0.0;
1299 }
1300 return result;
1301 }
1302
1303 // Returns the overscroll.
1304 double applyFullDragUpdate(double delta) {
1305 assert(delta != 0.0);
1306 final double oldPixels = pixels;
1307 // Apply friction:
1308 final double newPixels = pixels - physics.applyPhysicsToUserOffset(this, delta);
1309 if ((oldPixels - newPixels).abs() < precisionErrorTolerance) {
1310 // Delta is so small we can drop it.
1311 return 0.0;
1312 }
1313 // Check for overscroll:
1314 final double overscroll = physics.applyBoundaryConditions(this, newPixels);
1315 final double actualNewPixels = newPixels - overscroll;
1316 if (actualNewPixels != oldPixels) {
1317 forcePixels(actualNewPixels);
1318 didUpdateScrollPositionBy(actualNewPixels - oldPixels);
1319 }
1320 if (overscroll != 0.0) {
1321 didOverscrollBy(overscroll);
1322 return overscroll;
1323 }
1324 return 0.0;
1325 }
1326
1327 // Returns the amount of delta that was not used.
1328 //
1329 // Negative delta represents a forward ScrollDirection, while the positive
1330 // would be a reverse ScrollDirection.
1331 //
1332 // The method doesn't take into account the effects of [ScrollPhysics].
1333 double applyClampedPointerSignalUpdate(double delta) {
1334 assert(delta != 0.0);
1335
1336 final double min = delta > 0.0 ? -double.infinity : math.min(minScrollExtent, pixels);
1337 // The logic for max is equivalent but on the other side.
1338 final double max = delta < 0.0 ? double.infinity : math.max(maxScrollExtent, pixels);
1339 final double newPixels = clampDouble(pixels + delta, min, max);
1340 final double clampedDelta = newPixels - pixels;
1341 if (clampedDelta == 0.0) {
1342 return delta;
1343 }
1344 forcePixels(newPixels);
1345 didUpdateScrollPositionBy(clampedDelta);
1346 return delta - clampedDelta;
1347 }
1348
1349 @override
1350 ScrollDirection get userScrollDirection => coordinator.userScrollDirection;
1351
1352 DrivenScrollActivity createDrivenScrollActivity(double to, Duration duration, Curve curve) {
1353 return DrivenScrollActivity(
1354 this,
1355 from: pixels,
1356 to: to,
1357 duration: duration,
1358 curve: curve,
1359 vsync: vsync,
1360 );
1361 }
1362
1363 @override
1364 double applyUserOffset(double delta) {
1365 assert(false);
1366 return 0.0;
1367 }
1368
1369 // This is called by activities when they finish their work.
1370 @override
1371 void goIdle() {
1372 beginActivity(IdleScrollActivity(this));
1373 coordinator.updateUserScrollDirection(ScrollDirection.idle);
1374 }
1375
1376 // This is called by activities when they finish their work and want to go
1377 // ballistic.
1378 @override
1379 void goBallistic(double velocity) {
1380 Simulation? simulation;
1381 if (velocity != 0.0 || outOfRange) {
1382 simulation = physics.createBallisticSimulation(this, velocity);
1383 }
1384 beginActivity(
1385 createBallisticScrollActivity(
1386 simulation,
1387 mode: _NestedBallisticScrollActivityMode.independent,
1388 ),
1389 );
1390 }
1391
1392 ScrollActivity createBallisticScrollActivity(
1393 Simulation? simulation, {
1394 required _NestedBallisticScrollActivityMode mode,
1395 _NestedScrollMetrics? metrics,
1396 }) {
1397 if (simulation == null) {
1398 return IdleScrollActivity(this);
1399 }
1400
1401 switch (mode) {
1402 case _NestedBallisticScrollActivityMode.outer:
1403 assert(metrics != null);
1404 if (metrics!.minRange == metrics.maxRange) {
1405 return IdleScrollActivity(this);
1406 }
1407 return _NestedOuterBallisticScrollActivity(
1408 coordinator,
1409 this,
1410 metrics,
1411 simulation,
1412 context.vsync,
1413 shouldIgnorePointer,
1414 );
1415 case _NestedBallisticScrollActivityMode.inner:
1416 return _NestedInnerBallisticScrollActivity(
1417 coordinator,
1418 this,
1419 simulation,
1420 context.vsync,
1421 shouldIgnorePointer,
1422 );
1423 case _NestedBallisticScrollActivityMode.independent:
1424 return BallisticScrollActivity(this, simulation, context.vsync, shouldIgnorePointer);
1425 }
1426 }
1427
1428 @override
1429 Future<void> animateTo(double to, {required Duration duration, required Curve curve}) {
1430 return coordinator.animateTo(
1431 coordinator.unnestOffset(to, this),
1432 duration: duration,
1433 curve: curve,
1434 );
1435 }
1436
1437 @override
1438 void jumpTo(double value) {
1439 return coordinator.jumpTo(coordinator.unnestOffset(value, this));
1440 }
1441
1442 @override
1443 void pointerScroll(double delta) {
1444 return coordinator.pointerScroll(delta);
1445 }
1446
1447 @override
1448 void jumpToWithoutSettling(double value) {
1449 assert(false);
1450 }
1451
1452 void localJumpTo(double value) {
1453 if (pixels != value) {
1454 final double oldPixels = pixels;
1455 forcePixels(value);
1456 didStartScroll();
1457 didUpdateScrollPositionBy(pixels - oldPixels);
1458 didEndScroll();
1459 }
1460 }
1461
1462 @override
1463 void applyNewDimensions() {
1464 super.applyNewDimensions();
1465 coordinator.updateCanDrag();
1466 }
1467
1468 void updateCanDrag(bool innerCanDrag) {
1469 // This is only called for the outer position
1470 assert(coordinator._outerPosition == this);
1471 context.setCanDrag(
1472 // This refers to the physics of the actual outer scroll position, not
1473 // the whole NestedScrollView, since it is possible to have different
1474 // ScrollPhysics for the inner and outer positions.
1475 physics.shouldAcceptUserOffset(this) || innerCanDrag,
1476 );
1477 }
1478
1479 @override
1480 ScrollHoldController hold(VoidCallback holdCancelCallback) {
1481 return coordinator.hold(holdCancelCallback);
1482 }
1483
1484 @override
1485 Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) {
1486 return coordinator.drag(details, dragCancelCallback);
1487 }
1488}
1489
1490enum _NestedBallisticScrollActivityMode { outer, inner, independent }
1491
1492class _NestedInnerBallisticScrollActivity extends BallisticScrollActivity {
1493 _NestedInnerBallisticScrollActivity(
1494 this.coordinator,
1495 _NestedScrollPosition position,
1496 Simulation simulation,
1497 TickerProvider vsync,
1498 bool shouldIgnorePointer,
1499 ) : super(position, simulation, vsync, shouldIgnorePointer);
1500
1501 final _NestedScrollCoordinator coordinator;
1502
1503 @override
1504 _NestedScrollPosition get delegate => super.delegate as _NestedScrollPosition;
1505
1506 @override
1507 void resetActivity() {
1508 delegate.beginActivity(coordinator.createInnerBallisticScrollActivity(delegate, velocity));
1509 }
1510
1511 @override
1512 void applyNewDimensions() {
1513 delegate.beginActivity(coordinator.createInnerBallisticScrollActivity(delegate, velocity));
1514 }
1515
1516 @override
1517 bool applyMoveTo(double value) {
1518 return super.applyMoveTo(coordinator.nestOffset(value, delegate));
1519 }
1520}
1521
1522class _NestedOuterBallisticScrollActivity extends BallisticScrollActivity {
1523 _NestedOuterBallisticScrollActivity(
1524 this.coordinator,
1525 _NestedScrollPosition position,
1526 this.metrics,
1527 Simulation simulation,
1528 TickerProvider vsync,
1529 bool shouldIgnorePointer,
1530 ) : assert(metrics.minRange != metrics.maxRange),
1531 assert(metrics.maxRange > metrics.minRange),
1532 super(position, simulation, vsync, shouldIgnorePointer);
1533
1534 final _NestedScrollCoordinator coordinator;
1535 final _NestedScrollMetrics metrics;
1536
1537 @override
1538 _NestedScrollPosition get delegate => super.delegate as _NestedScrollPosition;
1539
1540 @override
1541 void resetActivity() {
1542 delegate.beginActivity(coordinator.createOuterBallisticScrollActivity(velocity));
1543 }
1544
1545 @override
1546 void applyNewDimensions() {
1547 delegate.beginActivity(coordinator.createOuterBallisticScrollActivity(velocity));
1548 }
1549
1550 @override
1551 bool applyMoveTo(double value) {
1552 bool done = false;
1553 if (velocity > 0.0) {
1554 if (value < metrics.minRange) {
1555 return true;
1556 }
1557 if (value > metrics.maxRange) {
1558 value = metrics.maxRange;
1559 done = true;
1560 }
1561 } else if (velocity < 0.0) {
1562 if (value > metrics.maxRange) {
1563 return true;
1564 }
1565 if (value < metrics.minRange) {
1566 value = metrics.minRange;
1567 done = true;
1568 }
1569 } else {
1570 value = clampDouble(value, metrics.minRange, metrics.maxRange);
1571 done = true;
1572 }
1573 final bool result = super.applyMoveTo(value + metrics.correctionOffset);
1574 assert(result); // since we tried to pass an in-range value, it shouldn't ever overflow
1575 return !done;
1576 }
1577
1578 @override
1579 String toString() {
1580 return '${objectRuntimeType(this, '_NestedOuterBallisticScrollActivity')}(${metrics.minRange} .. ${metrics.maxRange}; correcting by ${metrics.correctionOffset})';
1581 }
1582}
1583
1584/// Handle to provide to a [SliverOverlapAbsorber], a [SliverOverlapInjector],
1585/// and an [NestedScrollViewViewport], to shift overlap in a [NestedScrollView].
1586///
1587/// A particular [SliverOverlapAbsorberHandle] can only be assigned to a single
1588/// [SliverOverlapAbsorber] at a time. It can also be (and normally is) assigned
1589/// to one or more [SliverOverlapInjector]s, which must be later descendants of
1590/// the same [NestedScrollViewViewport] as the [SliverOverlapAbsorber]. The
1591/// [SliverOverlapAbsorber] must be a direct descendant of the
1592/// [NestedScrollViewViewport], taking part in the same sliver layout. (The
1593/// [SliverOverlapInjector] can be a descendant that takes part in a nested
1594/// scroll view's sliver layout.)
1595///
1596/// Whenever the [NestedScrollViewViewport] is marked dirty for layout, it will
1597/// cause its assigned [SliverOverlapAbsorberHandle] to fire notifications. It
1598/// is the responsibility of the [SliverOverlapInjector]s (and any other
1599/// clients) to mark themselves dirty when this happens, in case the geometry
1600/// subsequently changes during layout.
1601///
1602/// See also:
1603///
1604/// * [NestedScrollView], which uses a [NestedScrollViewViewport] and a
1605/// [SliverOverlapAbsorber] to align its children, and which shows sample
1606/// usage for this class.
1607class SliverOverlapAbsorberHandle extends ChangeNotifier {
1608 /// Creates a [SliverOverlapAbsorberHandle].
1609 SliverOverlapAbsorberHandle() {
1610 if (kFlutterMemoryAllocationsEnabled) {
1611 ChangeNotifier.maybeDispatchObjectCreation(this);
1612 }
1613 }
1614
1615 // Incremented when a RenderSliverOverlapAbsorber takes ownership of this
1616 // object, decremented when it releases it. This allows us to find cases where
1617 // the same handle is being passed to two render objects.
1618 int _writers = 0;
1619
1620 /// The current amount of overlap being absorbed by the
1621 /// [SliverOverlapAbsorber].
1622 ///
1623 /// This corresponds to the [SliverGeometry.layoutExtent] of the child of the
1624 /// [SliverOverlapAbsorber].
1625 ///
1626 /// This is updated during the layout of the [SliverOverlapAbsorber]. It
1627 /// should not change at any other time. No notifications are sent when it
1628 /// changes; clients (e.g. [SliverOverlapInjector]s) are responsible for
1629 /// marking themselves dirty whenever this object sends notifications, which
1630 /// happens any time the [SliverOverlapAbsorber] might subsequently change the
1631 /// value during that layout.
1632 double? get layoutExtent => _layoutExtent;
1633 double? _layoutExtent;
1634
1635 /// The total scroll extent of the gap being absorbed by the
1636 /// [SliverOverlapAbsorber].
1637 ///
1638 /// This corresponds to the [SliverGeometry.scrollExtent] of the child of the
1639 /// [SliverOverlapAbsorber].
1640 ///
1641 /// This is updated during the layout of the [SliverOverlapAbsorber]. It
1642 /// should not change at any other time. No notifications are sent when it
1643 /// changes; clients (e.g. [SliverOverlapInjector]s) are responsible for
1644 /// marking themselves dirty whenever this object sends notifications, which
1645 /// happens any time the [SliverOverlapAbsorber] might subsequently change the
1646 /// value during that layout.
1647 double? get scrollExtent => _scrollExtent;
1648 double? _scrollExtent;
1649
1650 void _setExtents(double? layoutValue, double? scrollValue) {
1651 assert(
1652 _writers == 1,
1653 'Multiple RenderSliverOverlapAbsorbers have been provided the same SliverOverlapAbsorberHandle.',
1654 );
1655 _layoutExtent = layoutValue;
1656 _scrollExtent = scrollValue;
1657 }
1658
1659 void _markNeedsLayout() => notifyListeners();
1660
1661 @override
1662 String toString() {
1663 final String? extra = switch (_writers) {
1664 0 => ', orphan',
1665 1 => null, // normal case
1666 _ => ', $_writers WRITERS ASSIGNED',
1667 };
1668 return '${objectRuntimeType(this, 'SliverOverlapAbsorberHandle')}($layoutExtent$extra)';
1669 }
1670}
1671
1672/// A sliver that wraps another, forcing its layout extent to be treated as
1673/// overlap.
1674///
1675/// The difference between the overlap requested by the child `sliver` and the
1676/// overlap reported by this widget, called the _absorbed overlap_, is reported
1677/// to the [SliverOverlapAbsorberHandle], which is typically passed to a
1678/// [SliverOverlapInjector].
1679///
1680/// See also:
1681///
1682/// * [NestedScrollView], whose documentation has sample code showing how to
1683/// use this widget.
1684class SliverOverlapAbsorber extends SingleChildRenderObjectWidget {
1685 /// Creates a sliver that absorbs overlap and reports it to a
1686 /// [SliverOverlapAbsorberHandle].
1687 const SliverOverlapAbsorber({super.key, required this.handle, Widget? sliver})
1688 : super(child: sliver);
1689
1690 /// The object in which the absorbed overlap is recorded.
1691 ///
1692 /// A particular [SliverOverlapAbsorberHandle] can only be assigned to a
1693 /// single [SliverOverlapAbsorber] at a time.
1694 final SliverOverlapAbsorberHandle handle;
1695
1696 @override
1697 RenderSliverOverlapAbsorber createRenderObject(BuildContext context) {
1698 return RenderSliverOverlapAbsorber(handle: handle);
1699 }
1700
1701 @override
1702 void updateRenderObject(BuildContext context, RenderSliverOverlapAbsorber renderObject) {
1703 renderObject.handle = handle;
1704 }
1705
1706 @override
1707 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
1708 super.debugFillProperties(properties);
1709 properties.add(DiagnosticsProperty<SliverOverlapAbsorberHandle>('handle', handle));
1710 }
1711}
1712
1713/// A sliver that wraps another, forcing its layout extent to be treated as
1714/// overlap.
1715///
1716/// The difference between the overlap requested by the child `sliver` and the
1717/// overlap reported by this widget, called the _absorbed overlap_, is reported
1718/// to the [SliverOverlapAbsorberHandle], which is typically passed to a
1719/// [RenderSliverOverlapInjector].
1720class RenderSliverOverlapAbsorber extends RenderSliver
1721 with RenderObjectWithChildMixin<RenderSliver> {
1722 /// Create a sliver that absorbs overlap and reports it to a
1723 /// [SliverOverlapAbsorberHandle].
1724 ///
1725 /// The [sliver] must be a [RenderSliver].
1726 RenderSliverOverlapAbsorber({required SliverOverlapAbsorberHandle handle, RenderSliver? sliver})
1727 : _handle = handle {
1728 child = sliver;
1729 }
1730
1731 /// The object in which the absorbed overlap is recorded.
1732 ///
1733 /// A particular [SliverOverlapAbsorberHandle] can only be assigned to a
1734 /// single [RenderSliverOverlapAbsorber] at a time.
1735 SliverOverlapAbsorberHandle get handle => _handle;
1736 SliverOverlapAbsorberHandle _handle;
1737 set handle(SliverOverlapAbsorberHandle value) {
1738 if (handle == value) {
1739 return;
1740 }
1741 if (attached) {
1742 handle._writers -= 1;
1743 value._writers += 1;
1744 value._setExtents(handle.layoutExtent, handle.scrollExtent);
1745 }
1746 _handle = value;
1747 }
1748
1749 @override
1750 void attach(PipelineOwner owner) {
1751 super.attach(owner);
1752 handle._writers += 1;
1753 }
1754
1755 @override
1756 void detach() {
1757 handle._writers -= 1;
1758 super.detach();
1759 }
1760
1761 @override
1762 void performLayout() {
1763 assert(
1764 handle._writers == 1,
1765 'A SliverOverlapAbsorberHandle cannot be passed to multiple RenderSliverOverlapAbsorber objects at the same time.',
1766 );
1767 if (child == null) {
1768 geometry = SliverGeometry.zero;
1769 return;
1770 }
1771 child!.layout(constraints, parentUsesSize: true);
1772 final SliverGeometry childLayoutGeometry = child!.geometry!;
1773 geometry = childLayoutGeometry.copyWith(
1774 scrollExtent:
1775 childLayoutGeometry.scrollExtent - childLayoutGeometry.maxScrollObstructionExtent,
1776 layoutExtent: math.max(
1777 0,
1778 childLayoutGeometry.paintExtent - childLayoutGeometry.maxScrollObstructionExtent,
1779 ),
1780 );
1781 handle._setExtents(
1782 childLayoutGeometry.maxScrollObstructionExtent,
1783 childLayoutGeometry.maxScrollObstructionExtent,
1784 );
1785 }
1786
1787 @override
1788 void applyPaintTransform(RenderObject child, Matrix4 transform) {
1789 // child is always at our origin
1790 }
1791
1792 @override
1793 bool hitTestChildren(
1794 SliverHitTestResult result, {
1795 required double mainAxisPosition,
1796 required double crossAxisPosition,
1797 }) {
1798 if (child != null) {
1799 return child!.hitTest(
1800 result,
1801 mainAxisPosition: mainAxisPosition,
1802 crossAxisPosition: crossAxisPosition,
1803 );
1804 }
1805 return false;
1806 }
1807
1808 @override
1809 void paint(PaintingContext context, Offset offset) {
1810 if (child != null) {
1811 context.paintChild(child!, offset);
1812 }
1813 }
1814
1815 @override
1816 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
1817 super.debugFillProperties(properties);
1818 properties.add(DiagnosticsProperty<SliverOverlapAbsorberHandle>('handle', handle));
1819 }
1820}
1821
1822/// A sliver that has a sliver geometry based on the values stored in a
1823/// [SliverOverlapAbsorberHandle].
1824///
1825/// The [SliverOverlapAbsorber] must be an earlier descendant of a common
1826/// ancestor [Viewport], so that it will always be laid out before the
1827/// [SliverOverlapInjector] during a particular frame.
1828///
1829/// See also:
1830///
1831/// * [NestedScrollView], which uses a [SliverOverlapAbsorber] to align its
1832/// children, and which shows sample usage for this class.
1833class SliverOverlapInjector extends SingleChildRenderObjectWidget {
1834 /// Creates a sliver that is as tall as the value of the given [handle]'s
1835 /// layout extent.
1836 const SliverOverlapInjector({super.key, required this.handle, Widget? sliver})
1837 : super(child: sliver);
1838
1839 /// The handle to the [SliverOverlapAbsorber] that is feeding this injector.
1840 ///
1841 /// This should be a handle owned by a [SliverOverlapAbsorber] and a
1842 /// [NestedScrollViewViewport].
1843 final SliverOverlapAbsorberHandle handle;
1844
1845 @override
1846 RenderSliverOverlapInjector createRenderObject(BuildContext context) {
1847 return RenderSliverOverlapInjector(handle: handle);
1848 }
1849
1850 @override
1851 void updateRenderObject(BuildContext context, RenderSliverOverlapInjector renderObject) {
1852 renderObject.handle = handle;
1853 }
1854
1855 @override
1856 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
1857 super.debugFillProperties(properties);
1858 properties.add(DiagnosticsProperty<SliverOverlapAbsorberHandle>('handle', handle));
1859 }
1860}
1861
1862/// A sliver that has a sliver geometry based on the values stored in a
1863/// [SliverOverlapAbsorberHandle].
1864///
1865/// The [RenderSliverOverlapAbsorber] must be an earlier descendant of a common
1866/// ancestor [RenderViewport] (probably a [RenderNestedScrollViewViewport]), so
1867/// that it will always be laid out before the [RenderSliverOverlapInjector]
1868/// during a particular frame.
1869class RenderSliverOverlapInjector extends RenderSliver {
1870 /// Creates a sliver that is as tall as the value of the given [handle]'s extent.
1871 RenderSliverOverlapInjector({required SliverOverlapAbsorberHandle handle}) : _handle = handle;
1872
1873 double? _currentLayoutExtent;
1874 double? _currentMaxExtent;
1875
1876 /// The object that specifies how wide to make the gap injected by this render
1877 /// object.
1878 ///
1879 /// This should be a handle owned by a [RenderSliverOverlapAbsorber] and a
1880 /// [RenderNestedScrollViewViewport].
1881 SliverOverlapAbsorberHandle get handle => _handle;
1882 SliverOverlapAbsorberHandle _handle;
1883 set handle(SliverOverlapAbsorberHandle value) {
1884 if (handle == value) {
1885 return;
1886 }
1887 if (attached) {
1888 handle.removeListener(markNeedsLayout);
1889 }
1890 _handle = value;
1891 if (attached) {
1892 handle.addListener(markNeedsLayout);
1893 if (handle.layoutExtent != _currentLayoutExtent || handle.scrollExtent != _currentMaxExtent) {
1894 markNeedsLayout();
1895 }
1896 }
1897 }
1898
1899 @override
1900 void attach(PipelineOwner owner) {
1901 super.attach(owner);
1902 handle.addListener(markNeedsLayout);
1903 if (handle.layoutExtent != _currentLayoutExtent || handle.scrollExtent != _currentMaxExtent) {
1904 markNeedsLayout();
1905 }
1906 }
1907
1908 @override
1909 void detach() {
1910 handle.removeListener(markNeedsLayout);
1911 super.detach();
1912 }
1913
1914 @override
1915 void performLayout() {
1916 _currentLayoutExtent = handle.layoutExtent;
1917 _currentMaxExtent = handle.layoutExtent;
1918 assert(
1919 _currentLayoutExtent != null && _currentMaxExtent != null,
1920 'SliverOverlapInjector has found no absorbed extent to inject.\n '
1921 'The SliverOverlapAbsorber must be an earlier descendant of a common '
1922 'ancestor Viewport, so that it will always be laid out before the '
1923 'SliverOverlapInjector during a particular frame.\n '
1924 'The SliverOverlapAbsorber is typically contained in the list of slivers '
1925 'provided by NestedScrollView.headerSliverBuilder.\n',
1926 );
1927 final double clampedLayoutExtent = math.min(
1928 _currentLayoutExtent! - constraints.scrollOffset,
1929 constraints.remainingPaintExtent,
1930 );
1931 geometry = SliverGeometry(
1932 scrollExtent: _currentLayoutExtent!,
1933 paintExtent: math.max(0.0, clampedLayoutExtent),
1934 maxPaintExtent: _currentMaxExtent!,
1935 );
1936 }
1937
1938 @override
1939 void debugPaint(PaintingContext context, Offset offset) {
1940 assert(() {
1941 if (debugPaintSizeEnabled) {
1942 final Paint paint = Paint()
1943 ..color = const Color(0xFFCC9933)
1944 ..strokeWidth = 3.0
1945 ..style = PaintingStyle.stroke;
1946 Offset start, end, delta;
1947 switch (constraints.axis) {
1948 case Axis.vertical:
1949 final double x = offset.dx + constraints.crossAxisExtent / 2.0;
1950 start = Offset(x, offset.dy);
1951 end = Offset(x, offset.dy + geometry!.paintExtent);
1952 delta = Offset(constraints.crossAxisExtent / 5.0, 0.0);
1953 case Axis.horizontal:
1954 final double y = offset.dy + constraints.crossAxisExtent / 2.0;
1955 start = Offset(offset.dx, y);
1956 end = Offset(offset.dy + geometry!.paintExtent, y);
1957 delta = Offset(0.0, constraints.crossAxisExtent / 5.0);
1958 }
1959 for (int index = -2; index <= 2; index += 1) {
1960 paintZigZag(
1961 context.canvas,
1962 paint,
1963 start - delta * index.toDouble(),
1964 end - delta * index.toDouble(),
1965 10,
1966 10.0,
1967 );
1968 }
1969 }
1970 return true;
1971 }());
1972 }
1973
1974 @override
1975 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
1976 super.debugFillProperties(properties);
1977 properties.add(DiagnosticsProperty<SliverOverlapAbsorberHandle>('handle', handle));
1978 }
1979}
1980
1981/// The [Viewport] variant used by [NestedScrollView].
1982///
1983/// This viewport takes a [SliverOverlapAbsorberHandle] and notifies it any time
1984/// the viewport needs to recompute its layout (e.g. when it is scrolled).
1985class NestedScrollViewViewport extends Viewport {
1986 /// Creates a variant of [Viewport] that has a [SliverOverlapAbsorberHandle].
1987 NestedScrollViewViewport({
1988 super.key,
1989 super.axisDirection,
1990 super.crossAxisDirection,
1991 super.anchor,
1992 required super.offset,
1993 super.center,
1994 super.slivers,
1995 required this.handle,
1996 super.clipBehavior,
1997 });
1998
1999 /// The handle to the [SliverOverlapAbsorber] that is feeding this injector.
2000 final SliverOverlapAbsorberHandle handle;
2001
2002 @override
2003 RenderNestedScrollViewViewport createRenderObject(BuildContext context) {
2004 return RenderNestedScrollViewViewport(
2005 axisDirection: axisDirection,
2006 crossAxisDirection:
2007 crossAxisDirection ?? Viewport.getDefaultCrossAxisDirection(context, axisDirection),
2008 anchor: anchor,
2009 offset: offset,
2010 handle: handle,
2011 clipBehavior: clipBehavior,
2012 );
2013 }
2014
2015 @override
2016 void updateRenderObject(BuildContext context, RenderNestedScrollViewViewport renderObject) {
2017 renderObject
2018 ..axisDirection = axisDirection
2019 ..crossAxisDirection =
2020 crossAxisDirection ?? Viewport.getDefaultCrossAxisDirection(context, axisDirection)
2021 ..anchor = anchor
2022 ..offset = offset
2023 ..handle = handle
2024 ..clipBehavior = clipBehavior;
2025 }
2026
2027 @override
2028 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
2029 super.debugFillProperties(properties);
2030 properties.add(DiagnosticsProperty<SliverOverlapAbsorberHandle>('handle', handle));
2031 }
2032}
2033
2034/// The [RenderViewport] variant used by [NestedScrollView].
2035///
2036/// This viewport takes a [SliverOverlapAbsorberHandle] and notifies it any time
2037/// the viewport needs to recompute its layout (e.g. when it is scrolled).
2038class RenderNestedScrollViewViewport extends RenderViewport {
2039 /// Create a variant of [RenderViewport] that has a
2040 /// [SliverOverlapAbsorberHandle].
2041 RenderNestedScrollViewViewport({
2042 super.axisDirection,
2043 required super.crossAxisDirection,
2044 required super.offset,
2045 super.anchor,
2046 super.children,
2047 super.center,
2048 required SliverOverlapAbsorberHandle handle,
2049 super.clipBehavior,
2050 }) : _handle = handle;
2051
2052 /// The object to notify when [markNeedsLayout] is called.
2053 SliverOverlapAbsorberHandle get handle => _handle;
2054 SliverOverlapAbsorberHandle _handle;
2055
2056 /// Setting this will trigger notifications on the new object.
2057 set handle(SliverOverlapAbsorberHandle value) {
2058 if (handle == value) {
2059 return;
2060 }
2061 _handle = value;
2062 handle._markNeedsLayout();
2063 }
2064
2065 @override
2066 void markNeedsLayout() {
2067 handle._markNeedsLayout();
2068 super.markNeedsLayout();
2069 }
2070
2071 @override
2072 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
2073 super.debugFillProperties(properties);
2074 properties.add(DiagnosticsProperty<SliverOverlapAbsorberHandle>('handle', handle));
2075 }
2076}
2077