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'; |
10 | library; |
11 | |
12 | import 'dart:math' as math; |
13 | |
14 | import 'package:flutter/foundation.dart'; |
15 | import 'package:flutter/gestures.dart'; |
16 | import 'package:flutter/rendering.dart'; |
17 | import 'package:flutter/scheduler.dart'; |
18 | |
19 | import 'basic.dart'; |
20 | import 'framework.dart'; |
21 | import 'primary_scroll_controller.dart'; |
22 | import 'scroll_activity.dart'; |
23 | import 'scroll_configuration.dart'; |
24 | import 'scroll_context.dart'; |
25 | import 'scroll_controller.dart'; |
26 | import 'scroll_metrics.dart'; |
27 | import 'scroll_physics.dart'; |
28 | import 'scroll_position.dart'; |
29 | import 'scroll_view.dart'; |
30 | import 'sliver_fill.dart'; |
31 | import '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. |
39 | typedef 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]. |
183 | class 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} |
393 | class 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 | |
522 | class _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 | |
557 | class _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 | |
566 | class _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 | |
612 | typedef _NestedScrollActivityGetter = ScrollActivity Function(_NestedScrollPosition position); |
613 | |
614 | class _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 | |
1143 | class _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. |
1203 | class _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 | |
1491 | enum _NestedBallisticScrollActivityMode { outer, inner, independent } |
1492 | |
1493 | class _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 | |
1523 | class _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. |
1608 | class 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. |
1685 | class 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]. |
1721 | class 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. |
1834 | class 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. |
1870 | class 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). |
1987 | class 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). |
2040 | class 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 |
Definitions
- NestedScrollView
- NestedScrollView
- sliverOverlapAbsorberHandleFor
- _buildSlivers
- createState
- NestedScrollViewState
- innerController
- outerController
- initState
- didChangeDependencies
- didUpdateWidget
- dispose
- _handleHasScrolledBodyChanged
- build
- _NestedScrollViewCustomScrollView
- _NestedScrollViewCustomScrollView
- buildViewport
- _InheritedNestedScrollView
- _InheritedNestedScrollView
- updateShouldNotify
- _NestedScrollMetrics
- _NestedScrollMetrics
- copyWith
- _NestedScrollCoordinator
- _NestedScrollCoordinator
- outOfRange
- _outerPosition
- _innerPositions
- canScrollBody
- hasScrolledBody
- updateShadow
- userScrollDirection
- updateUserScrollDirection
- beginActivity
- axisDirection
- _createIdleScrollActivity
- goIdle
- goBallistic
- createOuterBallisticScrollActivity
- createInnerBallisticScrollActivity
- _getMetrics
- unnestOffset
- nestOffset
- updateCanDrag
- animateTo
- jumpTo
- pointerScroll
- setPixels
- hold
- cancel
- drag
- applyUserOffset
- setParent
- updateParent
- dispose
- toString
- _NestedScrollController
- _NestedScrollController
- createScrollPosition
- attach
- detach
- _scheduleUpdateShadow
- nestedPositions
- _NestedScrollPosition
- _NestedScrollPosition
- vsync
- setParent
- axisDirection
- absorb
- restoreScrollOffset
- applyClampedDragUpdate
- applyFullDragUpdate
- applyClampedPointerSignalUpdate
- userScrollDirection
- createDrivenScrollActivity
- applyUserOffset
- goIdle
- goBallistic
- createBallisticScrollActivity
- animateTo
- jumpTo
- pointerScroll
- jumpToWithoutSettling
- localJumpTo
- applyNewDimensions
- updateCanDrag
- hold
- drag
- _NestedBallisticScrollActivityMode
- _NestedInnerBallisticScrollActivity
- _NestedInnerBallisticScrollActivity
- delegate
- resetActivity
- applyNewDimensions
- applyMoveTo
- _NestedOuterBallisticScrollActivity
- _NestedOuterBallisticScrollActivity
- delegate
- resetActivity
- applyNewDimensions
- applyMoveTo
- toString
- SliverOverlapAbsorberHandle
- SliverOverlapAbsorberHandle
- layoutExtent
- scrollExtent
- _setExtents
- _markNeedsLayout
- toString
- SliverOverlapAbsorber
- SliverOverlapAbsorber
- createRenderObject
- updateRenderObject
- debugFillProperties
- RenderSliverOverlapAbsorber
- RenderSliverOverlapAbsorber
- handle
- handle
- attach
- detach
- performLayout
- applyPaintTransform
- hitTestChildren
- paint
- debugFillProperties
- SliverOverlapInjector
- SliverOverlapInjector
- createRenderObject
- updateRenderObject
- debugFillProperties
- RenderSliverOverlapInjector
- RenderSliverOverlapInjector
- handle
- handle
- attach
- detach
- performLayout
- debugPaint
- debugFillProperties
- NestedScrollViewViewport
- NestedScrollViewViewport
- createRenderObject
- updateRenderObject
- debugFillProperties
- RenderNestedScrollViewViewport
- RenderNestedScrollViewViewport
- handle
- handle
- markNeedsLayout
Learn more about Flutter for embedded and desktop on industrialflutter.com