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