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

Provided by KDAB

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