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