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 =
336 context.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 =
1276 delta > 0.0
1277 ? double.infinity
1278 // If pixels < 0.0, then we are currently in overscroll. The max should be
1279 // 0.0, representing the end of the overscrolled portion.
1280 : pixels < 0.0
1281 ? 0.0
1282 : math.max(maxScrollExtent, pixels);
1283 final double oldPixels = pixels;
1284 final double newPixels = clampDouble(pixels - delta, min, max);
1285 final double clampedDelta = newPixels - pixels;
1286 if (clampedDelta == 0.0) {
1287 return delta;
1288 }
1289 final double overscroll = physics.applyBoundaryConditions(this, newPixels);
1290 final double actualNewPixels = newPixels - overscroll;
1291 final double offset = actualNewPixels - oldPixels;
1292 if (offset != 0.0) {
1293 forcePixels(actualNewPixels);
1294 didUpdateScrollPositionBy(offset);
1295 }
1296
1297 final double result = delta + offset;
1298 if (result.abs() < precisionErrorTolerance) {
1299 return 0.0;
1300 }
1301 return result;
1302 }
1303
1304 // Returns the overscroll.
1305 double applyFullDragUpdate(double delta) {
1306 assert(delta != 0.0);
1307 final double oldPixels = pixels;
1308 // Apply friction:
1309 final double newPixels = pixels - physics.applyPhysicsToUserOffset(this, delta);
1310 if ((oldPixels - newPixels).abs() < precisionErrorTolerance) {
1311 // Delta is so small we can drop it.
1312 return 0.0;
1313 }
1314 // Check for overscroll:
1315 final double overscroll = physics.applyBoundaryConditions(this, newPixels);
1316 final double actualNewPixels = newPixels - overscroll;
1317 if (actualNewPixels != oldPixels) {
1318 forcePixels(actualNewPixels);
1319 didUpdateScrollPositionBy(actualNewPixels - oldPixels);
1320 }
1321 if (overscroll != 0.0) {
1322 didOverscrollBy(overscroll);
1323 return overscroll;
1324 }
1325 return 0.0;
1326 }
1327
1328 // Returns the amount of delta that was not used.
1329 //
1330 // Negative delta represents a forward ScrollDirection, while the positive
1331 // would be a reverse ScrollDirection.
1332 //
1333 // The method doesn't take into account the effects of [ScrollPhysics].
1334 double applyClampedPointerSignalUpdate(double delta) {
1335 assert(delta != 0.0);
1336
1337 final double min = delta > 0.0 ? -double.infinity : math.min(minScrollExtent, pixels);
1338 // The logic for max is equivalent but on the other side.
1339 final double max = delta < 0.0 ? double.infinity : 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(
1386 createBallisticScrollActivity(
1387 simulation,
1388 mode: _NestedBallisticScrollActivityMode.independent,
1389 ),
1390 );
1391 }
1392
1393 ScrollActivity createBallisticScrollActivity(
1394 Simulation? simulation, {
1395 required _NestedBallisticScrollActivityMode mode,
1396 _NestedScrollMetrics? metrics,
1397 }) {
1398 if (simulation == null) {
1399 return IdleScrollActivity(this);
1400 }
1401
1402 switch (mode) {
1403 case _NestedBallisticScrollActivityMode.outer:
1404 assert(metrics != null);
1405 if (metrics!.minRange == metrics.maxRange) {
1406 return IdleScrollActivity(this);
1407 }
1408 return _NestedOuterBallisticScrollActivity(
1409 coordinator,
1410 this,
1411 metrics,
1412 simulation,
1413 context.vsync,
1414 shouldIgnorePointer,
1415 );
1416 case _NestedBallisticScrollActivityMode.inner:
1417 return _NestedInnerBallisticScrollActivity(
1418 coordinator,
1419 this,
1420 simulation,
1421 context.vsync,
1422 shouldIgnorePointer,
1423 );
1424 case _NestedBallisticScrollActivityMode.independent:
1425 return BallisticScrollActivity(this, simulation, context.vsync, shouldIgnorePointer);
1426 }
1427 }
1428
1429 @override
1430 Future<void> animateTo(double to, {required Duration duration, required Curve curve}) {
1431 return coordinator.animateTo(
1432 coordinator.unnestOffset(to, this),
1433 duration: duration,
1434 curve: curve,
1435 );
1436 }
1437
1438 @override
1439 void jumpTo(double value) {
1440 return coordinator.jumpTo(coordinator.unnestOffset(value, this));
1441 }
1442
1443 @override
1444 void pointerScroll(double delta) {
1445 return coordinator.pointerScroll(delta);
1446 }
1447
1448 @override
1449 void jumpToWithoutSettling(double value) {
1450 assert(false);
1451 }
1452
1453 void localJumpTo(double value) {
1454 if (pixels != value) {
1455 final double oldPixels = pixels;
1456 forcePixels(value);
1457 didStartScroll();
1458 didUpdateScrollPositionBy(pixels - oldPixels);
1459 didEndScroll();
1460 }
1461 }
1462
1463 @override
1464 void applyNewDimensions() {
1465 super.applyNewDimensions();
1466 coordinator.updateCanDrag();
1467 }
1468
1469 void updateCanDrag(bool innerCanDrag) {
1470 // This is only called for the outer position
1471 assert(coordinator._outerPosition == this);
1472 context.setCanDrag(
1473 // This refers to the physics of the actual outer scroll position, not
1474 // the whole NestedScrollView, since it is possible to have different
1475 // ScrollPhysics for the inner and outer positions.
1476 physics.shouldAcceptUserOffset(this) || innerCanDrag,
1477 );
1478 }
1479
1480 @override
1481 ScrollHoldController hold(VoidCallback holdCancelCallback) {
1482 return coordinator.hold(holdCancelCallback);
1483 }
1484
1485 @override
1486 Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) {
1487 return coordinator.drag(details, dragCancelCallback);
1488 }
1489}
1490
1491enum _NestedBallisticScrollActivityMode { outer, inner, independent }
1492
1493class _NestedInnerBallisticScrollActivity extends BallisticScrollActivity {
1494 _NestedInnerBallisticScrollActivity(
1495 this.coordinator,
1496 _NestedScrollPosition position,
1497 Simulation simulation,
1498 TickerProvider vsync,
1499 bool shouldIgnorePointer,
1500 ) : super(position, simulation, vsync, shouldIgnorePointer);
1501
1502 final _NestedScrollCoordinator coordinator;
1503
1504 @override
1505 _NestedScrollPosition get delegate => super.delegate as _NestedScrollPosition;
1506
1507 @override
1508 void resetActivity() {
1509 delegate.beginActivity(coordinator.createInnerBallisticScrollActivity(delegate, velocity));
1510 }
1511
1512 @override
1513 void applyNewDimensions() {
1514 delegate.beginActivity(coordinator.createInnerBallisticScrollActivity(delegate, velocity));
1515 }
1516
1517 @override
1518 bool applyMoveTo(double value) {
1519 return super.applyMoveTo(coordinator.nestOffset(value, delegate));
1520 }
1521}
1522
1523class _NestedOuterBallisticScrollActivity extends BallisticScrollActivity {
1524 _NestedOuterBallisticScrollActivity(
1525 this.coordinator,
1526 _NestedScrollPosition position,
1527 this.metrics,
1528 Simulation simulation,
1529 TickerProvider vsync,
1530 bool shouldIgnorePointer,
1531 ) : assert(metrics.minRange != metrics.maxRange),
1532 assert(metrics.maxRange > metrics.minRange),
1533 super(position, simulation, vsync, shouldIgnorePointer);
1534
1535 final _NestedScrollCoordinator coordinator;
1536 final _NestedScrollMetrics metrics;
1537
1538 @override
1539 _NestedScrollPosition get delegate => super.delegate as _NestedScrollPosition;
1540
1541 @override
1542 void resetActivity() {
1543 delegate.beginActivity(coordinator.createOuterBallisticScrollActivity(velocity));
1544 }
1545
1546 @override
1547 void applyNewDimensions() {
1548 delegate.beginActivity(coordinator.createOuterBallisticScrollActivity(velocity));
1549 }
1550
1551 @override
1552 bool applyMoveTo(double value) {
1553 bool done = false;
1554 if (velocity > 0.0) {
1555 if (value < metrics.minRange) {
1556 return true;
1557 }
1558 if (value > metrics.maxRange) {
1559 value = metrics.maxRange;
1560 done = true;
1561 }
1562 } else if (velocity < 0.0) {
1563 if (value > metrics.maxRange) {
1564 return true;
1565 }
1566 if (value < metrics.minRange) {
1567 value = metrics.minRange;
1568 done = true;
1569 }
1570 } else {
1571 value = clampDouble(value, metrics.minRange, metrics.maxRange);
1572 done = true;
1573 }
1574 final bool result = super.applyMoveTo(value + metrics.correctionOffset);
1575 assert(result); // since we tried to pass an in-range value, it shouldn't ever overflow
1576 return !done;
1577 }
1578
1579 @override
1580 String toString() {
1581 return '${objectRuntimeType(this, '_NestedOuterBallisticScrollActivity')}(${metrics.minRange} .. ${metrics.maxRange}; correcting by ${metrics.correctionOffset})';
1582 }
1583}
1584
1585/// Handle to provide to a [SliverOverlapAbsorber], a [SliverOverlapInjector],
1586/// and an [NestedScrollViewViewport], to shift overlap in a [NestedScrollView].
1587///
1588/// A particular [SliverOverlapAbsorberHandle] can only be assigned to a single
1589/// [SliverOverlapAbsorber] at a time. It can also be (and normally is) assigned
1590/// to one or more [SliverOverlapInjector]s, which must be later descendants of
1591/// the same [NestedScrollViewViewport] as the [SliverOverlapAbsorber]. The
1592/// [SliverOverlapAbsorber] must be a direct descendant of the
1593/// [NestedScrollViewViewport], taking part in the same sliver layout. (The
1594/// [SliverOverlapInjector] can be a descendant that takes part in a nested
1595/// scroll view's sliver layout.)
1596///
1597/// Whenever the [NestedScrollViewViewport] is marked dirty for layout, it will
1598/// cause its assigned [SliverOverlapAbsorberHandle] to fire notifications. It
1599/// is the responsibility of the [SliverOverlapInjector]s (and any other
1600/// clients) to mark themselves dirty when this happens, in case the geometry
1601/// subsequently changes during layout.
1602///
1603/// See also:
1604///
1605/// * [NestedScrollView], which uses a [NestedScrollViewViewport] and a
1606/// [SliverOverlapAbsorber] to align its children, and which shows sample
1607/// usage for this class.
1608class SliverOverlapAbsorberHandle extends ChangeNotifier {
1609 /// Creates a [SliverOverlapAbsorberHandle].
1610 SliverOverlapAbsorberHandle() {
1611 if (kFlutterMemoryAllocationsEnabled) {
1612 ChangeNotifier.maybeDispatchObjectCreation(this);
1613 }
1614 }
1615
1616 // Incremented when a RenderSliverOverlapAbsorber takes ownership of this
1617 // object, decremented when it releases it. This allows us to find cases where
1618 // the same handle is being passed to two render objects.
1619 int _writers = 0;
1620
1621 /// The current amount of overlap being absorbed by the
1622 /// [SliverOverlapAbsorber].
1623 ///
1624 /// This corresponds to the [SliverGeometry.layoutExtent] of the child of the
1625 /// [SliverOverlapAbsorber].
1626 ///
1627 /// This is updated during the layout of the [SliverOverlapAbsorber]. It
1628 /// should not change at any other time. No notifications are sent when it
1629 /// changes; clients (e.g. [SliverOverlapInjector]s) are responsible for
1630 /// marking themselves dirty whenever this object sends notifications, which
1631 /// happens any time the [SliverOverlapAbsorber] might subsequently change the
1632 /// value during that layout.
1633 double? get layoutExtent => _layoutExtent;
1634 double? _layoutExtent;
1635
1636 /// The total scroll extent of the gap being absorbed by the
1637 /// [SliverOverlapAbsorber].
1638 ///
1639 /// This corresponds to the [SliverGeometry.scrollExtent] of the child of the
1640 /// [SliverOverlapAbsorber].
1641 ///
1642 /// This is updated during the layout of the [SliverOverlapAbsorber]. It
1643 /// should not change at any other time. No notifications are sent when it
1644 /// changes; clients (e.g. [SliverOverlapInjector]s) are responsible for
1645 /// marking themselves dirty whenever this object sends notifications, which
1646 /// happens any time the [SliverOverlapAbsorber] might subsequently change the
1647 /// value during that layout.
1648 double? get scrollExtent => _scrollExtent;
1649 double? _scrollExtent;
1650
1651 void _setExtents(double? layoutValue, double? scrollValue) {
1652 assert(
1653 _writers == 1,
1654 'Multiple RenderSliverOverlapAbsorbers have been provided the same SliverOverlapAbsorberHandle.',
1655 );
1656 _layoutExtent = layoutValue;
1657 _scrollExtent = scrollValue;
1658 }
1659
1660 void _markNeedsLayout() => notifyListeners();
1661
1662 @override
1663 String toString() {
1664 final String? extra = switch (_writers) {
1665 0 => ', orphan',
1666 1 => null, // normal case
1667 _ => ', $_writers WRITERS ASSIGNED',
1668 };
1669 return '${objectRuntimeType(this, 'SliverOverlapAbsorberHandle')}($layoutExtent$extra)';
1670 }
1671}
1672
1673/// A sliver that wraps another, forcing its layout extent to be treated as
1674/// overlap.
1675///
1676/// The difference between the overlap requested by the child `sliver` and the
1677/// overlap reported by this widget, called the _absorbed overlap_, is reported
1678/// to the [SliverOverlapAbsorberHandle], which is typically passed to a
1679/// [SliverOverlapInjector].
1680///
1681/// See also:
1682///
1683/// * [NestedScrollView], whose documentation has sample code showing how to
1684/// use this widget.
1685class SliverOverlapAbsorber extends SingleChildRenderObjectWidget {
1686 /// Creates a sliver that absorbs overlap and reports it to a
1687 /// [SliverOverlapAbsorberHandle].
1688 const SliverOverlapAbsorber({super.key, required this.handle, Widget? sliver})
1689 : super(child: sliver);
1690
1691 /// The object in which the absorbed overlap is recorded.
1692 ///
1693 /// A particular [SliverOverlapAbsorberHandle] can only be assigned to a
1694 /// single [SliverOverlapAbsorber] at a time.
1695 final SliverOverlapAbsorberHandle handle;
1696
1697 @override
1698 RenderSliverOverlapAbsorber createRenderObject(BuildContext context) {
1699 return RenderSliverOverlapAbsorber(handle: handle);
1700 }
1701
1702 @override
1703 void updateRenderObject(BuildContext context, RenderSliverOverlapAbsorber renderObject) {
1704 renderObject.handle = handle;
1705 }
1706
1707 @override
1708 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
1709 super.debugFillProperties(properties);
1710 properties.add(DiagnosticsProperty<SliverOverlapAbsorberHandle>('handle', handle));
1711 }
1712}
1713
1714/// A sliver that wraps another, forcing its layout extent to be treated as
1715/// overlap.
1716///
1717/// The difference between the overlap requested by the child `sliver` and the
1718/// overlap reported by this widget, called the _absorbed overlap_, is reported
1719/// to the [SliverOverlapAbsorberHandle], which is typically passed to a
1720/// [RenderSliverOverlapInjector].
1721class RenderSliverOverlapAbsorber extends RenderSliver
1722 with RenderObjectWithChildMixin<RenderSliver> {
1723 /// Create a sliver that absorbs overlap and reports it to a
1724 /// [SliverOverlapAbsorberHandle].
1725 ///
1726 /// The [sliver] must be a [RenderSliver].
1727 RenderSliverOverlapAbsorber({required SliverOverlapAbsorberHandle handle, RenderSliver? sliver})
1728 : _handle = handle {
1729 child = sliver;
1730 }
1731
1732 /// The object in which the absorbed overlap is recorded.
1733 ///
1734 /// A particular [SliverOverlapAbsorberHandle] can only be assigned to a
1735 /// single [RenderSliverOverlapAbsorber] at a time.
1736 SliverOverlapAbsorberHandle get handle => _handle;
1737 SliverOverlapAbsorberHandle _handle;
1738 set handle(SliverOverlapAbsorberHandle value) {
1739 if (handle == value) {
1740 return;
1741 }
1742 if (attached) {
1743 handle._writers -= 1;
1744 value._writers += 1;
1745 value._setExtents(handle.layoutExtent, handle.scrollExtent);
1746 }
1747 _handle = value;
1748 }
1749
1750 @override
1751 void attach(PipelineOwner owner) {
1752 super.attach(owner);
1753 handle._writers += 1;
1754 }
1755
1756 @override
1757 void detach() {
1758 handle._writers -= 1;
1759 super.detach();
1760 }
1761
1762 @override
1763 void performLayout() {
1764 assert(
1765 handle._writers == 1,
1766 'A SliverOverlapAbsorberHandle cannot be passed to multiple RenderSliverOverlapAbsorber objects at the same time.',
1767 );
1768 if (child == null) {
1769 geometry = SliverGeometry.zero;
1770 return;
1771 }
1772 child!.layout(constraints, parentUsesSize: true);
1773 final SliverGeometry childLayoutGeometry = child!.geometry!;
1774 geometry = childLayoutGeometry.copyWith(
1775 scrollExtent:
1776 childLayoutGeometry.scrollExtent - childLayoutGeometry.maxScrollObstructionExtent,
1777 layoutExtent: math.max(
1778 0,
1779 childLayoutGeometry.paintExtent - childLayoutGeometry.maxScrollObstructionExtent,
1780 ),
1781 );
1782 handle._setExtents(
1783 childLayoutGeometry.maxScrollObstructionExtent,
1784 childLayoutGeometry.maxScrollObstructionExtent,
1785 );
1786 }
1787
1788 @override
1789 void applyPaintTransform(RenderObject child, Matrix4 transform) {
1790 // child is always at our origin
1791 }
1792
1793 @override
1794 bool hitTestChildren(
1795 SliverHitTestResult result, {
1796 required double mainAxisPosition,
1797 required double crossAxisPosition,
1798 }) {
1799 if (child != null) {
1800 return child!.hitTest(
1801 result,
1802 mainAxisPosition: mainAxisPosition,
1803 crossAxisPosition: crossAxisPosition,
1804 );
1805 }
1806 return false;
1807 }
1808
1809 @override
1810 void paint(PaintingContext context, Offset offset) {
1811 if (child != null) {
1812 context.paintChild(child!, offset);
1813 }
1814 }
1815
1816 @override
1817 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
1818 super.debugFillProperties(properties);
1819 properties.add(DiagnosticsProperty<SliverOverlapAbsorberHandle>('handle', handle));
1820 }
1821}
1822
1823/// A sliver that has a sliver geometry based on the values stored in a
1824/// [SliverOverlapAbsorberHandle].
1825///
1826/// The [SliverOverlapAbsorber] must be an earlier descendant of a common
1827/// ancestor [Viewport], so that it will always be laid out before the
1828/// [SliverOverlapInjector] during a particular frame.
1829///
1830/// See also:
1831///
1832/// * [NestedScrollView], which uses a [SliverOverlapAbsorber] to align its
1833/// children, and which shows sample usage for this class.
1834class SliverOverlapInjector extends SingleChildRenderObjectWidget {
1835 /// Creates a sliver that is as tall as the value of the given [handle]'s
1836 /// layout extent.
1837 const SliverOverlapInjector({super.key, required this.handle, Widget? sliver})
1838 : super(child: sliver);
1839
1840 /// The handle to the [SliverOverlapAbsorber] that is feeding this injector.
1841 ///
1842 /// This should be a handle owned by a [SliverOverlapAbsorber] and a
1843 /// [NestedScrollViewViewport].
1844 final SliverOverlapAbsorberHandle handle;
1845
1846 @override
1847 RenderSliverOverlapInjector createRenderObject(BuildContext context) {
1848 return RenderSliverOverlapInjector(handle: handle);
1849 }
1850
1851 @override
1852 void updateRenderObject(BuildContext context, RenderSliverOverlapInjector renderObject) {
1853 renderObject.handle = handle;
1854 }
1855
1856 @override
1857 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
1858 super.debugFillProperties(properties);
1859 properties.add(DiagnosticsProperty<SliverOverlapAbsorberHandle>('handle', handle));
1860 }
1861}
1862
1863/// A sliver that has a sliver geometry based on the values stored in a
1864/// [SliverOverlapAbsorberHandle].
1865///
1866/// The [RenderSliverOverlapAbsorber] must be an earlier descendant of a common
1867/// ancestor [RenderViewport] (probably a [RenderNestedScrollViewViewport]), so
1868/// that it will always be laid out before the [RenderSliverOverlapInjector]
1869/// during a particular frame.
1870class RenderSliverOverlapInjector extends RenderSliver {
1871 /// Creates a sliver that is as tall as the value of the given [handle]'s extent.
1872 RenderSliverOverlapInjector({required SliverOverlapAbsorberHandle handle}) : _handle = handle;
1873
1874 double? _currentLayoutExtent;
1875 double? _currentMaxExtent;
1876
1877 /// The object that specifies how wide to make the gap injected by this render
1878 /// object.
1879 ///
1880 /// This should be a handle owned by a [RenderSliverOverlapAbsorber] and a
1881 /// [RenderNestedScrollViewViewport].
1882 SliverOverlapAbsorberHandle get handle => _handle;
1883 SliverOverlapAbsorberHandle _handle;
1884 set handle(SliverOverlapAbsorberHandle value) {
1885 if (handle == value) {
1886 return;
1887 }
1888 if (attached) {
1889 handle.removeListener(markNeedsLayout);
1890 }
1891 _handle = value;
1892 if (attached) {
1893 handle.addListener(markNeedsLayout);
1894 if (handle.layoutExtent != _currentLayoutExtent || handle.scrollExtent != _currentMaxExtent) {
1895 markNeedsLayout();
1896 }
1897 }
1898 }
1899
1900 @override
1901 void attach(PipelineOwner owner) {
1902 super.attach(owner);
1903 handle.addListener(markNeedsLayout);
1904 if (handle.layoutExtent != _currentLayoutExtent || handle.scrollExtent != _currentMaxExtent) {
1905 markNeedsLayout();
1906 }
1907 }
1908
1909 @override
1910 void detach() {
1911 handle.removeListener(markNeedsLayout);
1912 super.detach();
1913 }
1914
1915 @override
1916 void performLayout() {
1917 _currentLayoutExtent = handle.layoutExtent;
1918 _currentMaxExtent = handle.layoutExtent;
1919 assert(
1920 _currentLayoutExtent != null && _currentMaxExtent != null,
1921 'SliverOverlapInjector has found no absorbed extent to inject.\n '
1922 'The SliverOverlapAbsorber must be an earlier descendant of a common '
1923 'ancestor Viewport, so that it will always be laid out before the '
1924 'SliverOverlapInjector during a particular frame.\n '
1925 'The SliverOverlapAbsorber is typically contained in the list of slivers '
1926 'provided by NestedScrollView.headerSliverBuilder.\n',
1927 );
1928 final double clampedLayoutExtent = math.min(
1929 _currentLayoutExtent! - constraints.scrollOffset,
1930 constraints.remainingPaintExtent,
1931 );
1932 geometry = SliverGeometry(
1933 scrollExtent: _currentLayoutExtent!,
1934 paintExtent: math.max(0.0, clampedLayoutExtent),
1935 maxPaintExtent: _currentMaxExtent!,
1936 );
1937 }
1938
1939 @override
1940 void debugPaint(PaintingContext context, Offset offset) {
1941 assert(() {
1942 if (debugPaintSizeEnabled) {
1943 final Paint paint =
1944 Paint()
1945 ..color = const Color(0xFFCC9933)
1946 ..strokeWidth = 3.0
1947 ..style = PaintingStyle.stroke;
1948 Offset start, end, delta;
1949 switch (constraints.axis) {
1950 case Axis.vertical:
1951 final double x = offset.dx + constraints.crossAxisExtent / 2.0;
1952 start = Offset(x, offset.dy);
1953 end = Offset(x, offset.dy + geometry!.paintExtent);
1954 delta = Offset(constraints.crossAxisExtent / 5.0, 0.0);
1955 case Axis.horizontal:
1956 final double y = offset.dy + constraints.crossAxisExtent / 2.0;
1957 start = Offset(offset.dx, y);
1958 end = Offset(offset.dy + geometry!.paintExtent, y);
1959 delta = Offset(0.0, constraints.crossAxisExtent / 5.0);
1960 }
1961 for (int index = -2; index <= 2; index += 1) {
1962 paintZigZag(
1963 context.canvas,
1964 paint,
1965 start - delta * index.toDouble(),
1966 end - delta * index.toDouble(),
1967 10,
1968 10.0,
1969 );
1970 }
1971 }
1972 return true;
1973 }());
1974 }
1975
1976 @override
1977 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
1978 super.debugFillProperties(properties);
1979 properties.add(DiagnosticsProperty<SliverOverlapAbsorberHandle>('handle', handle));
1980 }
1981}
1982
1983/// The [Viewport] variant used by [NestedScrollView].
1984///
1985/// This viewport takes a [SliverOverlapAbsorberHandle] and notifies it any time
1986/// the viewport needs to recompute its layout (e.g. when it is scrolled).
1987class NestedScrollViewViewport extends Viewport {
1988 /// Creates a variant of [Viewport] that has a [SliverOverlapAbsorberHandle].
1989 NestedScrollViewViewport({
1990 super.key,
1991 super.axisDirection,
1992 super.crossAxisDirection,
1993 super.anchor,
1994 required super.offset,
1995 super.center,
1996 super.slivers,
1997 required this.handle,
1998 super.clipBehavior,
1999 });
2000
2001 /// The handle to the [SliverOverlapAbsorber] that is feeding this injector.
2002 final SliverOverlapAbsorberHandle handle;
2003
2004 @override
2005 RenderNestedScrollViewViewport createRenderObject(BuildContext context) {
2006 return RenderNestedScrollViewViewport(
2007 axisDirection: axisDirection,
2008 crossAxisDirection:
2009 crossAxisDirection ?? Viewport.getDefaultCrossAxisDirection(context, axisDirection),
2010 anchor: anchor,
2011 offset: offset,
2012 handle: handle,
2013 clipBehavior: clipBehavior,
2014 );
2015 }
2016
2017 @override
2018 void updateRenderObject(BuildContext context, RenderNestedScrollViewViewport renderObject) {
2019 renderObject
2020 ..axisDirection = axisDirection
2021 ..crossAxisDirection =
2022 crossAxisDirection ?? Viewport.getDefaultCrossAxisDirection(context, axisDirection)
2023 ..anchor = anchor
2024 ..offset = offset
2025 ..handle = handle
2026 ..clipBehavior = clipBehavior;
2027 }
2028
2029 @override
2030 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
2031 super.debugFillProperties(properties);
2032 properties.add(DiagnosticsProperty<SliverOverlapAbsorberHandle>('handle', handle));
2033 }
2034}
2035
2036/// The [RenderViewport] variant used by [NestedScrollView].
2037///
2038/// This viewport takes a [SliverOverlapAbsorberHandle] and notifies it any time
2039/// the viewport needs to recompute its layout (e.g. when it is scrolled).
2040class RenderNestedScrollViewViewport extends RenderViewport {
2041 /// Create a variant of [RenderViewport] that has a
2042 /// [SliverOverlapAbsorberHandle].
2043 RenderNestedScrollViewViewport({
2044 super.axisDirection,
2045 required super.crossAxisDirection,
2046 required super.offset,
2047 super.anchor,
2048 super.children,
2049 super.center,
2050 required SliverOverlapAbsorberHandle handle,
2051 super.clipBehavior,
2052 }) : _handle = handle;
2053
2054 /// The object to notify when [markNeedsLayout] is called.
2055 SliverOverlapAbsorberHandle get handle => _handle;
2056 SliverOverlapAbsorberHandle _handle;
2057
2058 /// Setting this will trigger notifications on the new object.
2059 set handle(SliverOverlapAbsorberHandle value) {
2060 if (handle == value) {
2061 return;
2062 }
2063 _handle = value;
2064 handle._markNeedsLayout();
2065 }
2066
2067 @override
2068 void markNeedsLayout() {
2069 handle._markNeedsLayout();
2070 super.markNeedsLayout();
2071 }
2072
2073 @override
2074 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
2075 super.debugFillProperties(properties);
2076 properties.add(DiagnosticsProperty<SliverOverlapAbsorberHandle>('handle', handle));
2077 }
2078}
2079

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com