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