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 | /// @docImport 'package:flutter_test/flutter_test.dart'; |
7 | /// |
8 | /// @docImport 'primary_scroll_controller.dart'; |
9 | /// @docImport 'scroll_configuration.dart'; |
10 | /// @docImport 'scroll_view.dart'; |
11 | /// @docImport 'scrollable.dart'; |
12 | /// @docImport 'single_child_scroll_view.dart'; |
13 | /// @docImport 'viewport.dart'; |
14 | library; |
15 | |
16 | import 'dart:math' as math; |
17 | |
18 | import 'package:collection/collection.dart'; |
19 | import 'package:flutter/foundation.dart'; |
20 | import 'package:flutter/gestures.dart'; |
21 | |
22 | import 'basic.dart'; |
23 | import 'binding.dart'; |
24 | import 'framework.dart'; |
25 | import 'inherited_notifier.dart'; |
26 | import 'layout_builder.dart'; |
27 | import 'notification_listener.dart'; |
28 | import 'scroll_activity.dart'; |
29 | import 'scroll_context.dart'; |
30 | import 'scroll_controller.dart'; |
31 | import 'scroll_notification.dart'; |
32 | import 'scroll_physics.dart'; |
33 | import 'scroll_position.dart'; |
34 | import 'scroll_position_with_single_context.dart'; |
35 | import 'scroll_simulation.dart'; |
36 | import 'value_listenable_builder.dart'; |
37 | |
38 | /// The signature of a method that provides a [BuildContext] and |
39 | /// [ScrollController] for building a widget that may overflow the draggable |
40 | /// [Axis] of the containing [DraggableScrollableSheet]. |
41 | /// |
42 | /// Users should apply the [scrollController] to a [ScrollView] subclass, such |
43 | /// as a [SingleChildScrollView], [ListView] or [GridView], to have the whole |
44 | /// sheet be draggable. |
45 | typedef ScrollableWidgetBuilder = |
46 | Widget Function(BuildContext context, ScrollController scrollController); |
47 | |
48 | /// Controls a [DraggableScrollableSheet]. |
49 | /// |
50 | /// Draggable scrollable controllers are typically stored as member variables in |
51 | /// [State] objects and are reused in each [State.build]. Controllers can only |
52 | /// be used to control one sheet at a time. A controller can be reused with a |
53 | /// new sheet if the previous sheet has been disposed. |
54 | /// |
55 | /// The controller's methods cannot be used until after the controller has been |
56 | /// passed into a [DraggableScrollableSheet] and the sheet has run initState. |
57 | /// |
58 | /// A [DraggableScrollableController] is a [Listenable]. It notifies its |
59 | /// listeners whenever an attached sheet changes sizes. It does not notify its |
60 | /// listeners when a sheet is first attached or when an attached sheet's |
61 | /// parameters change without affecting the sheet's current size. It does not |
62 | /// fire when [pixels] changes without [size] changing. For example, if the |
63 | /// constraints provided to an attached sheet change. |
64 | class DraggableScrollableController extends ChangeNotifier { |
65 | /// Creates a controller for [DraggableScrollableSheet]. |
66 | DraggableScrollableController() { |
67 | if (kFlutterMemoryAllocationsEnabled) { |
68 | ChangeNotifier.maybeDispatchObjectCreation(this); |
69 | } |
70 | } |
71 | |
72 | _DraggableScrollableSheetScrollController? _attachedController; |
73 | final Set<AnimationController> _animationControllers = <AnimationController>{}; |
74 | |
75 | /// Get the current size (as a fraction of the parent height) of the attached sheet. |
76 | double get size { |
77 | _assertAttached(); |
78 | return _attachedController!.extent.currentSize; |
79 | } |
80 | |
81 | /// Get the current pixel height of the attached sheet. |
82 | double get pixels { |
83 | _assertAttached(); |
84 | return _attachedController!.extent.currentPixels; |
85 | } |
86 | |
87 | /// Convert a sheet's size (fractional value of parent container height) to pixels. |
88 | double sizeToPixels(double size) { |
89 | _assertAttached(); |
90 | return _attachedController!.extent.sizeToPixels(size); |
91 | } |
92 | |
93 | /// Returns Whether any [DraggableScrollableController] objects have attached themselves to the |
94 | /// [DraggableScrollableSheet]. |
95 | /// |
96 | /// If this is false, then members that interact with the [ScrollPosition], |
97 | /// such as [sizeToPixels], [size], [animateTo], and [jumpTo], must not be |
98 | /// called. |
99 | bool get isAttached => _attachedController != null && _attachedController!.hasClients; |
100 | |
101 | /// Convert a sheet's pixel height to size (fractional value of parent container height). |
102 | double pixelsToSize(double pixels) { |
103 | _assertAttached(); |
104 | return _attachedController!.extent.pixelsToSize(pixels); |
105 | } |
106 | |
107 | /// Animates the attached sheet from its current size to the given [size], a |
108 | /// fractional value of the parent container's height. |
109 | /// |
110 | /// Any active sheet animation is canceled. If the sheet's internal scrollable |
111 | /// is currently animating (e.g. responding to a user fling), that animation is |
112 | /// canceled as well. |
113 | /// |
114 | /// An animation will be interrupted whenever the user attempts to scroll |
115 | /// manually, whenever another activity is started, or when the sheet hits its |
116 | /// max or min size (e.g. if you animate to 1 but the max size is .8, the |
117 | /// animation will stop playing when it reaches .8). |
118 | /// |
119 | /// The duration must not be zero. To jump to a particular value without an |
120 | /// animation, use [jumpTo]. |
121 | /// |
122 | /// The sheet will not snap after calling [animateTo] even if [DraggableScrollableSheet.snap] |
123 | /// is true. Snapping only occurs after user drags. |
124 | /// |
125 | /// When calling [animateTo] in widget tests, `await`ing the returned |
126 | /// [Future] may cause the test to hang and timeout. Instead, use |
127 | /// [WidgetTester.pumpAndSettle]. |
128 | Future<void> animateTo(double size, {required Duration duration, required Curve curve}) async { |
129 | _assertAttached(); |
130 | assert(size >= 0 && size <= 1); |
131 | assert(duration != Duration.zero); |
132 | final AnimationController animationController = AnimationController.unbounded( |
133 | vsync: _attachedController!.position.context.vsync, |
134 | value: _attachedController!.extent.currentSize, |
135 | ); |
136 | _animationControllers.add(animationController); |
137 | _attachedController!.position.goIdle(); |
138 | // This disables any snapping until the next user interaction with the sheet. |
139 | _attachedController!.extent.hasDragged = false; |
140 | _attachedController!.extent.hasChanged = true; |
141 | _attachedController!.extent.startActivity( |
142 | onCanceled: () { |
143 | // Don't stop the controller if it's already finished and may have been disposed. |
144 | if (animationController.isAnimating) { |
145 | animationController.stop(); |
146 | } |
147 | }, |
148 | ); |
149 | animationController.addListener(() { |
150 | _attachedController!.extent.updateSize( |
151 | animationController.value, |
152 | _attachedController!.position.context.notificationContext!, |
153 | ); |
154 | }); |
155 | await animationController.animateTo( |
156 | clampDouble(size, _attachedController!.extent.minSize, _attachedController!.extent.maxSize), |
157 | duration: duration, |
158 | curve: curve, |
159 | ); |
160 | } |
161 | |
162 | /// Jumps the attached sheet from its current size to the given [size], a |
163 | /// fractional value of the parent container's height. |
164 | /// |
165 | /// If [size] is outside of a the attached sheet's min or max child size, |
166 | /// [jumpTo] will jump the sheet to the nearest valid size instead. |
167 | /// |
168 | /// Any active sheet animation is canceled. If the sheet's inner scrollable |
169 | /// is currently animating (e.g. responding to a user fling), that animation is |
170 | /// canceled as well. |
171 | /// |
172 | /// The sheet will not snap after calling [jumpTo] even if [DraggableScrollableSheet.snap] |
173 | /// is true. Snapping only occurs after user drags. |
174 | void jumpTo(double size) { |
175 | _assertAttached(); |
176 | assert(size >= 0 && size <= 1); |
177 | // Call start activity to interrupt any other playing activities. |
178 | _attachedController!.extent.startActivity(onCanceled: () {}); |
179 | _attachedController!.position.goIdle(); |
180 | _attachedController!.extent.hasDragged = false; |
181 | _attachedController!.extent.hasChanged = true; |
182 | _attachedController!.extent.updateSize( |
183 | size, |
184 | _attachedController!.position.context.notificationContext!, |
185 | ); |
186 | } |
187 | |
188 | /// Reset the attached sheet to its initial size (see: [DraggableScrollableSheet.initialChildSize]). |
189 | void reset() { |
190 | _assertAttached(); |
191 | _attachedController!.reset(); |
192 | } |
193 | |
194 | void _assertAttached() { |
195 | assert( |
196 | isAttached, |
197 | 'DraggableScrollableController is not attached to a sheet. A DraggableScrollableController ' |
198 | 'must be used in a DraggableScrollableSheet before any of its methods are called.', |
199 | ); |
200 | } |
201 | |
202 | void _attach(_DraggableScrollableSheetScrollController scrollController) { |
203 | assert( |
204 | _attachedController == null, |
205 | 'Draggable scrollable controller is already attached to a sheet.', |
206 | ); |
207 | _attachedController = scrollController; |
208 | _attachedController!.extent._currentSize.addListener(notifyListeners); |
209 | _attachedController!.onPositionDetached = _disposeAnimationControllers; |
210 | } |
211 | |
212 | void _onExtentReplaced(_DraggableSheetExtent previousExtent) { |
213 | // When the extent has been replaced, the old extent is already disposed and |
214 | // the controller will point to a new extent. We have to add our listener to |
215 | // the new extent. |
216 | _attachedController!.extent._currentSize.addListener(notifyListeners); |
217 | if (previousExtent.currentSize != _attachedController!.extent.currentSize) { |
218 | // The listener won't fire for a change in size between two extent |
219 | // objects so we have to fire it manually here. |
220 | notifyListeners(); |
221 | } |
222 | } |
223 | |
224 | void _detach({bool disposeExtent = false}) { |
225 | if (disposeExtent) { |
226 | _attachedController?.extent.dispose(); |
227 | } else { |
228 | _attachedController?.extent._currentSize.removeListener(notifyListeners); |
229 | } |
230 | _disposeAnimationControllers(); |
231 | _attachedController = null; |
232 | } |
233 | |
234 | void _disposeAnimationControllers() { |
235 | for (final AnimationController animationController in _animationControllers) { |
236 | animationController.dispose(); |
237 | } |
238 | _animationControllers.clear(); |
239 | } |
240 | } |
241 | |
242 | /// A container for a [Scrollable] that responds to drag gestures by resizing |
243 | /// the scrollable until a limit is reached, and then scrolling. |
244 | /// |
245 | /// {@youtube 560 315 https://www.youtube.com/watch?v=Hgw819mL_78} |
246 | /// |
247 | /// This widget can be dragged along the vertical axis between its |
248 | /// [minChildSize], which defaults to `0.25` and [maxChildSize], which defaults |
249 | /// to `1.0`. These sizes are percentages of the height of the parent container. |
250 | /// |
251 | /// The widget coordinates resizing and scrolling of the widget returned by |
252 | /// builder as the user drags along the horizontal axis. |
253 | /// |
254 | /// The widget will initially be displayed at its initialChildSize which |
255 | /// defaults to `0.5`, meaning half the height of its parent. Dragging will work |
256 | /// between the range of minChildSize and maxChildSize (as percentages of the |
257 | /// parent container's height) as long as the builder creates a widget which |
258 | /// uses the provided [ScrollController]. If the widget created by the |
259 | /// [ScrollableWidgetBuilder] does not use the provided [ScrollController], the |
260 | /// sheet will remain at the initialChildSize. |
261 | /// |
262 | /// By default, the widget will stay at whatever size the user drags it to. To |
263 | /// make the widget snap to specific sizes whenever they lift their finger |
264 | /// during a drag, set [snap] to `true`. The sheet will snap between |
265 | /// [minChildSize] and [maxChildSize]. Use [snapSizes] to add more sizes for |
266 | /// the sheet to snap between. |
267 | /// |
268 | /// The snapping effect is only applied on user drags. Programmatically |
269 | /// manipulating the sheet size via [DraggableScrollableController.animateTo] or |
270 | /// [DraggableScrollableController.jumpTo] will ignore [snap] and [snapSizes]. |
271 | /// |
272 | /// By default, the widget will expand its non-occupied area to fill available |
273 | /// space in the parent. If this is not desired, e.g. because the parent wants |
274 | /// to position sheet based on the space it is taking, the [expand] property |
275 | /// may be set to false. |
276 | /// |
277 | /// {@tool dartpad} |
278 | /// |
279 | /// This is a sample widget which shows a [ListView] that has 25 [ListTile]s. |
280 | /// It starts out as taking up half the body of the [Scaffold], and can be |
281 | /// dragged up to the full height of the scaffold or down to 25% of the height |
282 | /// of the scaffold. Upon reaching full height, the list contents will be |
283 | /// scrolled up or down, until they reach the top of the list again and the user |
284 | /// drags the sheet back down. |
285 | /// |
286 | /// On desktop and web running on desktop platforms, dragging to scroll with a mouse is disabled by default |
287 | /// to align with the natural behavior found in other desktop applications. |
288 | /// |
289 | /// This behavior is dictated by the [ScrollBehavior], and can be changed by adding |
290 | /// [PointerDeviceKind.mouse] to [ScrollBehavior.dragDevices]. |
291 | /// For more info on this, please refer to https://docs.flutter.dev/release/breaking-changes/default-scroll-behavior-drag |
292 | /// |
293 | /// Alternatively, this example illustrates how to add a drag handle for desktop applications. |
294 | /// |
295 | /// ** See code in examples/api/lib/widgets/draggable_scrollable_sheet/draggable_scrollable_sheet.0.dart ** |
296 | /// {@end-tool} |
297 | class DraggableScrollableSheet extends StatefulWidget { |
298 | /// Creates a widget that can be dragged and scrolled in a single gesture. |
299 | const DraggableScrollableSheet({ |
300 | super.key, |
301 | this.initialChildSize = 0.5, |
302 | this.minChildSize = 0.25, |
303 | this.maxChildSize = 1.0, |
304 | this.expand = true, |
305 | this.snap = false, |
306 | this.snapSizes, |
307 | this.snapAnimationDuration, |
308 | this.controller, |
309 | this.shouldCloseOnMinExtent = true, |
310 | required this.builder, |
311 | }) : assert(minChildSize >= 0.0), |
312 | assert(maxChildSize <= 1.0), |
313 | assert(minChildSize <= initialChildSize), |
314 | assert(initialChildSize <= maxChildSize), |
315 | assert(snapAnimationDuration == null || snapAnimationDuration > Duration.zero); |
316 | |
317 | /// The initial fractional value of the parent container's height to use when |
318 | /// displaying the widget. |
319 | /// |
320 | /// Rebuilding the sheet with a new [initialChildSize] will only move |
321 | /// the sheet to the new value if the sheet has not yet been dragged since it |
322 | /// was first built or since the last call to [DraggableScrollableActuator.reset]. |
323 | /// |
324 | /// The default value is `0.5`. |
325 | final double initialChildSize; |
326 | |
327 | /// The minimum fractional value of the parent container's height to use when |
328 | /// displaying the widget. |
329 | /// |
330 | /// The default value is `0.25`. |
331 | final double minChildSize; |
332 | |
333 | /// The maximum fractional value of the parent container's height to use when |
334 | /// displaying the widget. |
335 | /// |
336 | /// The default value is `1.0`. |
337 | final double maxChildSize; |
338 | |
339 | /// Whether the widget should expand to fill the available space in its parent |
340 | /// or not. |
341 | /// |
342 | /// In most cases, this should be true. However, in the case of a parent |
343 | /// widget that will position this one based on its desired size (such as a |
344 | /// [Center]), this should be set to false. |
345 | /// |
346 | /// The default value is true. |
347 | final bool expand; |
348 | |
349 | /// Whether the widget should snap between [snapSizes] when the user lifts |
350 | /// their finger during a drag. |
351 | /// |
352 | /// If the user's finger was still moving when they lifted it, the widget will |
353 | /// snap to the next snap size (see [snapSizes]) in the direction of the drag. |
354 | /// If their finger was still, the widget will snap to the nearest snap size. |
355 | /// |
356 | /// Snapping is not applied when the sheet is programmatically moved by |
357 | /// calling [DraggableScrollableController.animateTo] or [DraggableScrollableController.jumpTo]. |
358 | /// |
359 | /// Rebuilding the sheet with snap newly enabled will immediately trigger a |
360 | /// snap unless the sheet has not yet been dragged away from |
361 | /// [initialChildSize] since first being built or since the last call to |
362 | /// [DraggableScrollableActuator.reset]. |
363 | final bool snap; |
364 | |
365 | /// A list of target sizes that the widget should snap to. |
366 | /// |
367 | /// Snap sizes are fractional values of the parent container's height. They |
368 | /// must be listed in increasing order and be between [minChildSize] and |
369 | /// [maxChildSize]. |
370 | /// |
371 | /// The [minChildSize] and [maxChildSize] are implicitly included in snap |
372 | /// sizes and do not need to be specified here. For example, `snapSizes = [.5]` |
373 | /// will result in a sheet that snaps between [minChildSize], `.5`, and |
374 | /// [maxChildSize]. |
375 | /// |
376 | /// Any modifications to the [snapSizes] list will not take effect until the |
377 | /// `build` function containing this widget is run again. |
378 | /// |
379 | /// Rebuilding with a modified or new list will trigger a snap unless the |
380 | /// sheet has not yet been dragged away from [initialChildSize] since first |
381 | /// being built or since the last call to [DraggableScrollableActuator.reset]. |
382 | final List<double>? snapSizes; |
383 | |
384 | /// Defines a duration for the snap animations. |
385 | /// |
386 | /// If it's not set, then the animation duration is the distance to the snap |
387 | /// target divided by the velocity of the widget. |
388 | final Duration? snapAnimationDuration; |
389 | |
390 | /// A controller that can be used to programmatically control this sheet. |
391 | final DraggableScrollableController? controller; |
392 | |
393 | /// Whether the sheet, when dragged (or flung) to its minimum size, should |
394 | /// cause its parent sheet to close. |
395 | /// |
396 | /// Set on emitted [DraggableScrollableNotification]s. It is up to parent |
397 | /// classes to properly read and handle this value. |
398 | final bool shouldCloseOnMinExtent; |
399 | |
400 | /// The builder that creates a child to display in this widget, which will |
401 | /// use the provided [ScrollController] to enable dragging and scrolling |
402 | /// of the contents. |
403 | final ScrollableWidgetBuilder builder; |
404 | |
405 | @override |
406 | State<DraggableScrollableSheet> createState() => _DraggableScrollableSheetState(); |
407 | } |
408 | |
409 | /// A [Notification] related to the extent, which is the size, and scroll |
410 | /// offset, which is the position of the child list, of the |
411 | /// [DraggableScrollableSheet]. |
412 | /// |
413 | /// [DraggableScrollableSheet] widgets notify their ancestors when the size of |
414 | /// the sheet changes. When the extent of the sheet changes via a drag, |
415 | /// this notification bubbles up through the tree, which means a given |
416 | /// [NotificationListener] will receive notifications for all descendant |
417 | /// [DraggableScrollableSheet] widgets. To focus on notifications from the |
418 | /// nearest [DraggableScrollableSheet] descendant, check that the [depth] |
419 | /// property of the notification is zero. |
420 | /// |
421 | /// When an extent notification is received by a [NotificationListener], the |
422 | /// listener will already have completed build and layout, and it is therefore |
423 | /// too late for that widget to call [State.setState]. Any attempt to adjust the |
424 | /// build or layout based on an extent notification would result in a layout |
425 | /// that lagged one frame behind, which is a poor user experience. Extent |
426 | /// notifications are used primarily to drive animations. The [Scaffold] widget |
427 | /// listens for extent notifications and responds by driving animations for the |
428 | /// [FloatingActionButton] as the bottom sheet scrolls up. |
429 | class DraggableScrollableNotification extends Notification with ViewportNotificationMixin { |
430 | /// Creates a notification that the extent of a [DraggableScrollableSheet] has |
431 | /// changed. |
432 | /// |
433 | /// All parameters are required. The [minExtent] must be >= 0. The [maxExtent] |
434 | /// must be <= 1.0. The [extent] must be between [minExtent] and [maxExtent]. |
435 | DraggableScrollableNotification({ |
436 | required this.extent, |
437 | required this.minExtent, |
438 | required this.maxExtent, |
439 | required this.initialExtent, |
440 | required this.context, |
441 | this.shouldCloseOnMinExtent = true, |
442 | }) : assert(0.0 <= minExtent), |
443 | assert(maxExtent <= 1.0), |
444 | assert(minExtent <= extent), |
445 | assert(minExtent <= initialExtent), |
446 | assert(extent <= maxExtent), |
447 | assert(initialExtent <= maxExtent); |
448 | |
449 | /// The current value of the extent, between [minExtent] and [maxExtent]. |
450 | final double extent; |
451 | |
452 | /// The minimum value of [extent], which is >= 0. |
453 | final double minExtent; |
454 | |
455 | /// The maximum value of [extent]. |
456 | final double maxExtent; |
457 | |
458 | /// The initially requested value for [extent]. |
459 | final double initialExtent; |
460 | |
461 | /// The build context of the widget that fired this notification. |
462 | /// |
463 | /// This can be used to find the sheet's render objects to determine the size |
464 | /// of the viewport, for instance. A listener can only assume this context |
465 | /// is live when it first gets the notification. |
466 | final BuildContext context; |
467 | |
468 | /// Whether the widget that fired this notification, when dragged (or flung) |
469 | /// to minExtent, should cause its parent sheet to close. |
470 | /// |
471 | /// It is up to parent classes to properly read and handle this value. |
472 | final bool shouldCloseOnMinExtent; |
473 | |
474 | @override |
475 | void debugFillDescription(List<String> description) { |
476 | super.debugFillDescription(description); |
477 | description.add( |
478 | 'minExtent:$minExtent , extent:$extent , maxExtent:$maxExtent , initialExtent:$initialExtent ', |
479 | ); |
480 | } |
481 | } |
482 | |
483 | /// Manages state between [_DraggableScrollableSheetState], |
484 | /// [_DraggableScrollableSheetScrollController], and |
485 | /// [_DraggableScrollableSheetScrollPosition]. |
486 | /// |
487 | /// The State knows the pixels available along the axis the widget wants to |
488 | /// scroll, but expects to get a fraction of those pixels to render the sheet. |
489 | /// |
490 | /// The ScrollPosition knows the number of pixels a user wants to move the sheet. |
491 | /// |
492 | /// The [currentSize] will never be null. |
493 | /// The [availablePixels] will never be null, but may be `double.infinity`. |
494 | class _DraggableSheetExtent { |
495 | _DraggableSheetExtent({ |
496 | required this.minSize, |
497 | required this.maxSize, |
498 | required this.snap, |
499 | required this.snapSizes, |
500 | required this.initialSize, |
501 | this.snapAnimationDuration, |
502 | ValueNotifier<double>? currentSize, |
503 | bool? hasDragged, |
504 | bool? hasChanged, |
505 | this.shouldCloseOnMinExtent = true, |
506 | }) : assert(minSize >= 0), |
507 | assert(maxSize <= 1), |
508 | assert(minSize <= initialSize), |
509 | assert(initialSize <= maxSize), |
510 | _currentSize = currentSize ?? ValueNotifier<double>(initialSize), |
511 | availablePixels = double.infinity, |
512 | hasDragged = hasDragged ?? false, |
513 | hasChanged = hasChanged ?? false { |
514 | assert(debugMaybeDispatchCreated('widgets', '_DraggableSheetExtent', this)); |
515 | } |
516 | |
517 | VoidCallback? _cancelActivity; |
518 | |
519 | final double minSize; |
520 | final double maxSize; |
521 | final bool snap; |
522 | final List<double> snapSizes; |
523 | final Duration? snapAnimationDuration; |
524 | final double initialSize; |
525 | final bool shouldCloseOnMinExtent; |
526 | final ValueNotifier<double> _currentSize; |
527 | double availablePixels; |
528 | |
529 | // Used to disable snapping until the user has dragged on the sheet. |
530 | bool hasDragged; |
531 | |
532 | // Used to determine if the sheet should move to a new initial size when it |
533 | // changes. |
534 | // We need both `hasChanged` and `hasDragged` to achieve the following |
535 | // behavior: |
536 | // 1. The sheet should only snap following user drags (as opposed to |
537 | // programmatic sheet changes). See docs for `animateTo` and `jumpTo`. |
538 | // 2. The sheet should move to a new initial child size on rebuild iff the |
539 | // sheet has not changed, either by drag or programmatic control. See |
540 | // docs for `initialChildSize`. |
541 | bool hasChanged; |
542 | |
543 | bool get isAtMin => minSize >= _currentSize.value; |
544 | bool get isAtMax => maxSize <= _currentSize.value; |
545 | |
546 | double get currentSize => _currentSize.value; |
547 | double get currentPixels => sizeToPixels(_currentSize.value); |
548 | |
549 | List<double> get pixelSnapSizes => snapSizes.map(sizeToPixels).toList(); |
550 | |
551 | /// Start an activity that affects the sheet and register a cancel call back |
552 | /// that will be called if another activity starts. |
553 | /// |
554 | /// The `onCanceled` callback will get called even if the subsequent activity |
555 | /// started after this one finished, so `onCanceled` must be safe to call at |
556 | /// any time. |
557 | void startActivity({required VoidCallback onCanceled}) { |
558 | _cancelActivity?.call(); |
559 | _cancelActivity = onCanceled; |
560 | } |
561 | |
562 | /// The scroll position gets inputs in terms of pixels, but the size is |
563 | /// expected to be expressed as a number between 0..1. |
564 | /// |
565 | /// This should only be called to respond to a user drag. To update the |
566 | /// size in response to a programmatic call, use [updateSize] directly. |
567 | void addPixelDelta(double delta, BuildContext context) { |
568 | // Stop any playing sheet animations. |
569 | _cancelActivity?.call(); |
570 | _cancelActivity = null; |
571 | // The user has interacted with the sheet, set `hasDragged` to true so that |
572 | // we'll snap if applicable. |
573 | hasDragged = true; |
574 | hasChanged = true; |
575 | if (availablePixels == 0) { |
576 | return; |
577 | } |
578 | updateSize(currentSize + pixelsToSize(delta), context); |
579 | } |
580 | |
581 | /// Set the size to the new value. [newSize] should be a number between |
582 | /// [minSize] and [maxSize]. |
583 | /// |
584 | /// This can be triggered by a programmatic (e.g. controller triggered) change |
585 | /// or a user drag. |
586 | void updateSize(double newSize, BuildContext context) { |
587 | final double clampedSize = clampDouble(newSize, minSize, maxSize); |
588 | if (_currentSize.value == clampedSize) { |
589 | return; |
590 | } |
591 | _currentSize.value = clampedSize; |
592 | DraggableScrollableNotification( |
593 | minExtent: minSize, |
594 | maxExtent: maxSize, |
595 | extent: currentSize, |
596 | initialExtent: initialSize, |
597 | context: context, |
598 | shouldCloseOnMinExtent: shouldCloseOnMinExtent, |
599 | ).dispatch(context); |
600 | } |
601 | |
602 | double pixelsToSize(double pixels) { |
603 | return pixels / availablePixels * maxSize; |
604 | } |
605 | |
606 | double sizeToPixels(double size) { |
607 | return size / maxSize * availablePixels; |
608 | } |
609 | |
610 | void dispose() { |
611 | assert(debugMaybeDispatchDisposed(this)); |
612 | _currentSize.dispose(); |
613 | } |
614 | |
615 | _DraggableSheetExtent copyWith({ |
616 | required double minSize, |
617 | required double maxSize, |
618 | required bool snap, |
619 | required List<double> snapSizes, |
620 | required double initialSize, |
621 | required Duration? snapAnimationDuration, |
622 | required bool shouldCloseOnMinExtent, |
623 | }) { |
624 | return _DraggableSheetExtent( |
625 | minSize: minSize, |
626 | maxSize: maxSize, |
627 | snap: snap, |
628 | snapSizes: snapSizes, |
629 | snapAnimationDuration: snapAnimationDuration, |
630 | initialSize: initialSize, |
631 | // Set the current size to the possibly updated initial size if the sheet |
632 | // hasn't changed yet. |
633 | currentSize: ValueNotifier<double>( |
634 | hasChanged ? clampDouble(_currentSize.value, minSize, maxSize) : initialSize, |
635 | ), |
636 | hasDragged: hasDragged, |
637 | hasChanged: hasChanged, |
638 | shouldCloseOnMinExtent: shouldCloseOnMinExtent, |
639 | ); |
640 | } |
641 | } |
642 | |
643 | class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> { |
644 | late _DraggableScrollableSheetScrollController _scrollController; |
645 | late _DraggableSheetExtent _extent; |
646 | |
647 | @override |
648 | void initState() { |
649 | super.initState(); |
650 | _extent = _DraggableSheetExtent( |
651 | minSize: widget.minChildSize, |
652 | maxSize: widget.maxChildSize, |
653 | snap: widget.snap, |
654 | snapSizes: _impliedSnapSizes(), |
655 | snapAnimationDuration: widget.snapAnimationDuration, |
656 | initialSize: widget.initialChildSize, |
657 | shouldCloseOnMinExtent: widget.shouldCloseOnMinExtent, |
658 | ); |
659 | _scrollController = _DraggableScrollableSheetScrollController(extent: _extent); |
660 | widget.controller?._attach(_scrollController); |
661 | } |
662 | |
663 | List<double> _impliedSnapSizes() { |
664 | for (int index = 0; index < (widget.snapSizes?.length ?? 0); index += 1) { |
665 | final double snapSize = widget.snapSizes![index]; |
666 | assert( |
667 | snapSize >= widget.minChildSize && snapSize <= widget.maxChildSize, |
668 | '${_snapSizeErrorMessage(index)} \nSnap sizes must be between `minChildSize` and `maxChildSize`. ', |
669 | ); |
670 | assert( |
671 | index == 0 || snapSize > widget.snapSizes![index - 1], |
672 | '${_snapSizeErrorMessage(index)} \nSnap sizes must be in ascending order. ', |
673 | ); |
674 | } |
675 | // Ensure the snap sizes start and end with the min and max child sizes. |
676 | if (widget.snapSizes == null || widget.snapSizes!.isEmpty) { |
677 | return <double>[widget.minChildSize, widget.maxChildSize]; |
678 | } |
679 | return <double>[ |
680 | if (widget.snapSizes!.first != widget.minChildSize) widget.minChildSize, |
681 | ...widget.snapSizes!, |
682 | if (widget.snapSizes!.last != widget.maxChildSize) widget.maxChildSize, |
683 | ]; |
684 | } |
685 | |
686 | @override |
687 | void didUpdateWidget(covariant DraggableScrollableSheet oldWidget) { |
688 | super.didUpdateWidget(oldWidget); |
689 | if (widget.controller != oldWidget.controller) { |
690 | oldWidget.controller?._detach(); |
691 | widget.controller?._attach(_scrollController); |
692 | } |
693 | _replaceExtent(oldWidget); |
694 | } |
695 | |
696 | @override |
697 | void didChangeDependencies() { |
698 | super.didChangeDependencies(); |
699 | if (_InheritedResetNotifier.shouldReset(context)) { |
700 | _scrollController.reset(); |
701 | } |
702 | } |
703 | |
704 | @override |
705 | Widget build(BuildContext context) { |
706 | return ValueListenableBuilder<double>( |
707 | valueListenable: _extent._currentSize, |
708 | builder: |
709 | (BuildContext context, double currentSize, Widget? child) => LayoutBuilder( |
710 | builder: (BuildContext context, BoxConstraints constraints) { |
711 | _extent.availablePixels = widget.maxChildSize * constraints.biggest.height; |
712 | final Widget sheet = FractionallySizedBox( |
713 | heightFactor: currentSize, |
714 | alignment: Alignment.bottomCenter, |
715 | child: child, |
716 | ); |
717 | return widget.expand ? SizedBox.expand(child: sheet) : sheet; |
718 | }, |
719 | ), |
720 | child: widget.builder(context, _scrollController), |
721 | ); |
722 | } |
723 | |
724 | @override |
725 | void dispose() { |
726 | if (widget.controller == null) { |
727 | _extent.dispose(); |
728 | } else { |
729 | widget.controller!._detach(disposeExtent: true); |
730 | } |
731 | _scrollController.dispose(); |
732 | super.dispose(); |
733 | } |
734 | |
735 | void _replaceExtent(covariant DraggableScrollableSheet oldWidget) { |
736 | final _DraggableSheetExtent previousExtent = _extent; |
737 | _extent = previousExtent.copyWith( |
738 | minSize: widget.minChildSize, |
739 | maxSize: widget.maxChildSize, |
740 | snap: widget.snap, |
741 | snapSizes: _impliedSnapSizes(), |
742 | snapAnimationDuration: widget.snapAnimationDuration, |
743 | initialSize: widget.initialChildSize, |
744 | shouldCloseOnMinExtent: widget.shouldCloseOnMinExtent, |
745 | ); |
746 | // Modify the existing scroll controller instead of replacing it so that |
747 | // developers listening to the controller do not have to rebuild their listeners. |
748 | _scrollController.extent = _extent; |
749 | // If an external facing controller was provided, let it know that the |
750 | // extent has been replaced. |
751 | widget.controller?._onExtentReplaced(previousExtent); |
752 | previousExtent.dispose(); |
753 | if (widget.snap && |
754 | (widget.snap != oldWidget.snap || widget.snapSizes != oldWidget.snapSizes) && |
755 | _scrollController.hasClients) { |
756 | // Trigger a snap in case snap or snapSizes has changed and there is a |
757 | // scroll position currently attached. We put this in a post frame |
758 | // callback so that `build` can update `_extent.availablePixels` before |
759 | // this runs-we can't use the previous extent's available pixels as it may |
760 | // have changed when the widget was updated. |
761 | WidgetsBinding.instance.addPostFrameCallback((Duration timeStamp) { |
762 | for (int index = 0; index < _scrollController.positions.length; index++) { |
763 | final _DraggableScrollableSheetScrollPosition position = |
764 | _scrollController.positions.elementAt(index) |
765 | as _DraggableScrollableSheetScrollPosition; |
766 | position.goBallistic(0); |
767 | } |
768 | }, debugLabel: 'DraggableScrollableSheet.snap'); |
769 | } |
770 | } |
771 | |
772 | String _snapSizeErrorMessage(int invalidIndex) { |
773 | final List<String> snapSizesWithIndicator = |
774 | widget.snapSizes!.asMap().keys.map((int index) { |
775 | final String snapSizeString = widget.snapSizes![index].toString(); |
776 | if (index == invalidIndex) { |
777 | return '>>>$snapSizeString <<<'; |
778 | } |
779 | return snapSizeString; |
780 | }).toList(); |
781 | return "Invalid snapSize '${widget.snapSizes![invalidIndex]} ' at index$invalidIndex of:\n" |
782 | '$snapSizesWithIndicator '; |
783 | } |
784 | } |
785 | |
786 | /// A [ScrollController] suitable for use in a [ScrollableWidgetBuilder] created |
787 | /// by a [DraggableScrollableSheet]. |
788 | /// |
789 | /// If a [DraggableScrollableSheet] contains content that is exceeds the height |
790 | /// of its container, this controller will allow the sheet to both be dragged to |
791 | /// fill the container and then scroll the child content. |
792 | /// |
793 | /// See also: |
794 | /// |
795 | /// * [_DraggableScrollableSheetScrollPosition], which manages the positioning logic for |
796 | /// this controller. |
797 | /// * [PrimaryScrollController], which can be used to establish a |
798 | /// [_DraggableScrollableSheetScrollController] as the primary controller for |
799 | /// descendants. |
800 | class _DraggableScrollableSheetScrollController extends ScrollController { |
801 | _DraggableScrollableSheetScrollController({required this.extent}); |
802 | |
803 | _DraggableSheetExtent extent; |
804 | VoidCallback? onPositionDetached; |
805 | |
806 | @override |
807 | _DraggableScrollableSheetScrollPosition createScrollPosition( |
808 | ScrollPhysics physics, |
809 | ScrollContext context, |
810 | ScrollPosition? oldPosition, |
811 | ) { |
812 | return _DraggableScrollableSheetScrollPosition( |
813 | physics: physics.applyTo(const AlwaysScrollableScrollPhysics()), |
814 | context: context, |
815 | oldPosition: oldPosition, |
816 | getExtent: () => extent, |
817 | ); |
818 | } |
819 | |
820 | @override |
821 | void debugFillDescription(List<String> description) { |
822 | super.debugFillDescription(description); |
823 | description.add('extent:$extent '); |
824 | } |
825 | |
826 | @override |
827 | _DraggableScrollableSheetScrollPosition get position => |
828 | super.position as _DraggableScrollableSheetScrollPosition; |
829 | |
830 | void reset() { |
831 | extent._cancelActivity?.call(); |
832 | extent.hasDragged = false; |
833 | extent.hasChanged = false; |
834 | // jumpTo can result in trying to replace semantics during build. |
835 | // Just animate really fast. |
836 | // Avoid doing it at all if the offset is already 0.0. |
837 | if (offset != 0.0) { |
838 | animateTo(0.0, duration: const Duration(milliseconds: 1), curve: Curves.linear); |
839 | } |
840 | extent.updateSize(extent.initialSize, position.context.notificationContext!); |
841 | } |
842 | |
843 | @override |
844 | void detach(ScrollPosition position) { |
845 | onPositionDetached?.call(); |
846 | super.detach(position); |
847 | } |
848 | } |
849 | |
850 | /// A scroll position that manages scroll activities for |
851 | /// [_DraggableScrollableSheetScrollController]. |
852 | /// |
853 | /// This class is a concrete subclass of [ScrollPosition] logic that handles a |
854 | /// single [ScrollContext], such as a [Scrollable]. An instance of this class |
855 | /// manages [ScrollActivity] instances, which changes the |
856 | /// [_DraggableSheetExtent.currentSize] or visible content offset in the |
857 | /// [Scrollable]'s [Viewport] |
858 | /// |
859 | /// See also: |
860 | /// |
861 | /// * [_DraggableScrollableSheetScrollController], which uses this as its [ScrollPosition]. |
862 | class _DraggableScrollableSheetScrollPosition extends ScrollPositionWithSingleContext { |
863 | _DraggableScrollableSheetScrollPosition({ |
864 | required super.physics, |
865 | required super.context, |
866 | super.oldPosition, |
867 | required this.getExtent, |
868 | }); |
869 | |
870 | VoidCallback? _dragCancelCallback; |
871 | final _DraggableSheetExtent Function() getExtent; |
872 | final Set<AnimationController> _ballisticControllers = <AnimationController>{}; |
873 | bool get listShouldScroll => pixels > 0.0; |
874 | |
875 | _DraggableSheetExtent get extent => getExtent(); |
876 | |
877 | @override |
878 | void absorb(ScrollPosition other) { |
879 | super.absorb(other); |
880 | assert(_dragCancelCallback == null); |
881 | |
882 | if (other is! _DraggableScrollableSheetScrollPosition) { |
883 | return; |
884 | } |
885 | |
886 | if (other._dragCancelCallback != null) { |
887 | _dragCancelCallback = other._dragCancelCallback; |
888 | other._dragCancelCallback = null; |
889 | } |
890 | } |
891 | |
892 | @override |
893 | void beginActivity(ScrollActivity? newActivity) { |
894 | // Cancel the running ballistic simulations |
895 | for (final AnimationController ballisticController in _ballisticControllers) { |
896 | ballisticController.stop(); |
897 | } |
898 | super.beginActivity(newActivity); |
899 | } |
900 | |
901 | @override |
902 | void applyUserOffset(double delta) { |
903 | if (!listShouldScroll && |
904 | (!(extent.isAtMin || extent.isAtMax) || |
905 | (extent.isAtMin && delta < 0) || |
906 | (extent.isAtMax && delta > 0))) { |
907 | extent.addPixelDelta(-delta, context.notificationContext!); |
908 | } else { |
909 | super.applyUserOffset(delta); |
910 | } |
911 | } |
912 | |
913 | // Checks if the sheet's current size is close to a snap size, returning the |
914 | // snap size if so; returns null otherwise. |
915 | double? _getCurrentSnapSize() { |
916 | return extent.snapSizes.firstWhereOrNull((double snapSize) { |
917 | return (extent.currentSize - snapSize).abs() <= |
918 | extent.pixelsToSize(physics.toleranceFor(this).distance); |
919 | }); |
920 | } |
921 | |
922 | bool _isAtSnapSize() => _getCurrentSnapSize() != null; |
923 | |
924 | bool _shouldSnap() => extent.snap && extent.hasDragged && !_isAtSnapSize(); |
925 | |
926 | @override |
927 | void dispose() { |
928 | for (final AnimationController ballisticController in _ballisticControllers) { |
929 | ballisticController.dispose(); |
930 | } |
931 | _ballisticControllers.clear(); |
932 | super.dispose(); |
933 | } |
934 | |
935 | @override |
936 | void goBallistic(double velocity) { |
937 | if ((velocity == 0.0 && !_shouldSnap()) || |
938 | (velocity < 0.0 && listShouldScroll) || |
939 | (velocity > 0.0 && extent.isAtMax)) { |
940 | super.goBallistic(velocity); |
941 | return; |
942 | } |
943 | // Scrollable expects that we will dispose of its current _dragCancelCallback |
944 | _dragCancelCallback?.call(); |
945 | _dragCancelCallback = null; |
946 | |
947 | late final Simulation simulation; |
948 | if (extent.snap) { |
949 | // Snap is enabled, simulate snapping instead of clamping scroll. |
950 | simulation = _SnappingSimulation( |
951 | position: extent.currentPixels, |
952 | initialVelocity: velocity, |
953 | pixelSnapSize: extent.pixelSnapSizes, |
954 | snapAnimationDuration: extent.snapAnimationDuration, |
955 | tolerance: physics.toleranceFor(this), |
956 | ); |
957 | } else { |
958 | // The iOS bouncing simulation just isn't right here - once we delegate |
959 | // the ballistic back to the ScrollView, it will use the right simulation. |
960 | simulation = ClampingScrollSimulation( |
961 | // Run the simulation in terms of pixels, not extent. |
962 | position: extent.currentPixels, |
963 | velocity: velocity, |
964 | tolerance: physics.toleranceFor(this), |
965 | ); |
966 | } |
967 | |
968 | final AnimationController ballisticController = AnimationController.unbounded( |
969 | debugLabel: objectRuntimeType(this, '_DraggableScrollableSheetPosition'), |
970 | vsync: context.vsync, |
971 | ); |
972 | _ballisticControllers.add(ballisticController); |
973 | |
974 | double lastPosition = extent.currentPixels; |
975 | void tick() { |
976 | final double delta = ballisticController.value - lastPosition; |
977 | lastPosition = ballisticController.value; |
978 | extent.addPixelDelta(delta, context.notificationContext!); |
979 | if ((velocity > 0 && extent.isAtMax) || (velocity < 0 && extent.isAtMin)) { |
980 | // Make sure we pass along enough velocity to keep scrolling - otherwise |
981 | // we just "bounce" off the top making it look like the list doesn't |
982 | // have more to scroll. |
983 | velocity = |
984 | ballisticController.velocity + |
985 | (physics.toleranceFor(this).velocity * ballisticController.velocity.sign); |
986 | super.goBallistic(velocity); |
987 | ballisticController.stop(); |
988 | } else if (ballisticController.isCompleted) { |
989 | // Update the extent value after the snap animation completes to |
990 | // avoid rounding errors that could prevent the sheet from closing when |
991 | // it reaches minSize. |
992 | final double? snapSize = _getCurrentSnapSize(); |
993 | if (snapSize != null) { |
994 | extent.updateSize(snapSize, context.notificationContext!); |
995 | } |
996 | super.goBallistic(0); |
997 | } |
998 | } |
999 | |
1000 | ballisticController |
1001 | ..addListener(tick) |
1002 | ..animateWith(simulation).whenCompleteOrCancel(() { |
1003 | if (_ballisticControllers.contains(ballisticController)) { |
1004 | _ballisticControllers.remove(ballisticController); |
1005 | ballisticController.dispose(); |
1006 | } |
1007 | }); |
1008 | } |
1009 | |
1010 | @override |
1011 | Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) { |
1012 | // Save this so we can call it later if we have to [goBallistic] on our own. |
1013 | _dragCancelCallback = dragCancelCallback; |
1014 | return super.drag(details, dragCancelCallback); |
1015 | } |
1016 | } |
1017 | |
1018 | /// A widget that can notify a descendent [DraggableScrollableSheet] that it |
1019 | /// should reset its position to the initial state. |
1020 | /// |
1021 | /// The [Scaffold] uses this widget to notify a persistent bottom sheet that |
1022 | /// the user has tapped back if the sheet has started to cover more of the body |
1023 | /// than when at its initial position. This is important for users of assistive |
1024 | /// technology, where dragging may be difficult to communicate. |
1025 | /// |
1026 | /// This is just a wrapper on top of [DraggableScrollableController]. It is |
1027 | /// primarily useful for controlling a sheet in a part of the widget tree that |
1028 | /// the current code does not control (e.g. library code trying to affect a sheet |
1029 | /// in library users' code). Generally, it's easier to control the sheet |
1030 | /// directly by creating a controller and passing the controller to the sheet in |
1031 | /// its constructor (see [DraggableScrollableSheet.controller]). |
1032 | class DraggableScrollableActuator extends StatefulWidget { |
1033 | /// Creates a widget that can notify descendent [DraggableScrollableSheet]s |
1034 | /// to reset to their initial position. |
1035 | /// |
1036 | /// The [child] parameter is required. |
1037 | const DraggableScrollableActuator({super.key, required this.child}); |
1038 | |
1039 | /// This child's [DraggableScrollableSheet] descendant will be reset when the |
1040 | /// [reset] method is applied to a context that includes it. |
1041 | final Widget child; |
1042 | |
1043 | /// Notifies any descendant [DraggableScrollableSheet] that it should reset |
1044 | /// to its initial position. |
1045 | /// |
1046 | /// Returns `true` if a [DraggableScrollableActuator] is available and |
1047 | /// some [DraggableScrollableSheet] is listening for updates, `false` |
1048 | /// otherwise. |
1049 | static bool reset(BuildContext context) { |
1050 | final _InheritedResetNotifier? notifier = |
1051 | context.dependOnInheritedWidgetOfExactType<_InheritedResetNotifier>(); |
1052 | return notifier?._sendReset() ?? false; |
1053 | } |
1054 | |
1055 | @override |
1056 | State<DraggableScrollableActuator> createState() => _DraggableScrollableActuatorState(); |
1057 | } |
1058 | |
1059 | class _DraggableScrollableActuatorState extends State<DraggableScrollableActuator> { |
1060 | final _ResetNotifier _notifier = _ResetNotifier(); |
1061 | |
1062 | @override |
1063 | Widget build(BuildContext context) { |
1064 | return _InheritedResetNotifier(notifier: _notifier, child: widget.child); |
1065 | } |
1066 | |
1067 | @override |
1068 | void dispose() { |
1069 | _notifier.dispose(); |
1070 | super.dispose(); |
1071 | } |
1072 | } |
1073 | |
1074 | /// A [ChangeNotifier] to use with [_InheritedResetNotifier] to notify |
1075 | /// descendants that they should reset to initial state. |
1076 | class _ResetNotifier extends ChangeNotifier { |
1077 | _ResetNotifier() { |
1078 | if (kFlutterMemoryAllocationsEnabled) { |
1079 | ChangeNotifier.maybeDispatchObjectCreation(this); |
1080 | } |
1081 | } |
1082 | |
1083 | /// Whether someone called [sendReset] or not. |
1084 | /// |
1085 | /// This flag should be reset after checking it. |
1086 | bool _wasCalled = false; |
1087 | |
1088 | /// Fires a reset notification to descendants. |
1089 | /// |
1090 | /// Returns false if there are no listeners. |
1091 | bool sendReset() { |
1092 | if (!hasListeners) { |
1093 | return false; |
1094 | } |
1095 | _wasCalled = true; |
1096 | notifyListeners(); |
1097 | return true; |
1098 | } |
1099 | } |
1100 | |
1101 | class _InheritedResetNotifier extends InheritedNotifier<_ResetNotifier> { |
1102 | /// Creates an [InheritedNotifier] that the [DraggableScrollableSheet] will |
1103 | /// listen to for an indication that it should reset itself back to [DraggableScrollableSheet.initialChildSize]. |
1104 | const _InheritedResetNotifier({required super.child, required _ResetNotifier super.notifier}); |
1105 | |
1106 | bool _sendReset() => notifier!.sendReset(); |
1107 | |
1108 | /// Specifies whether the [DraggableScrollableSheet] should reset to its |
1109 | /// initial position. |
1110 | /// |
1111 | /// Returns true if the notifier requested a reset, false otherwise. |
1112 | static bool shouldReset(BuildContext context) { |
1113 | final InheritedWidget? widget = |
1114 | context.dependOnInheritedWidgetOfExactType<_InheritedResetNotifier>(); |
1115 | if (widget == null) { |
1116 | return false; |
1117 | } |
1118 | assert(widget is _InheritedResetNotifier); |
1119 | final _InheritedResetNotifier inheritedNotifier = widget as _InheritedResetNotifier; |
1120 | final bool wasCalled = inheritedNotifier.notifier!._wasCalled; |
1121 | inheritedNotifier.notifier!._wasCalled = false; |
1122 | return wasCalled; |
1123 | } |
1124 | } |
1125 | |
1126 | class _SnappingSimulation extends Simulation { |
1127 | _SnappingSimulation({ |
1128 | required this.position, |
1129 | required double initialVelocity, |
1130 | required List<double> pixelSnapSize, |
1131 | Duration? snapAnimationDuration, |
1132 | super.tolerance, |
1133 | }) { |
1134 | _pixelSnapSize = _getSnapSize(initialVelocity, pixelSnapSize); |
1135 | |
1136 | if (snapAnimationDuration != null && snapAnimationDuration.inMilliseconds > 0) { |
1137 | velocity = (_pixelSnapSize - position) * 1000 / snapAnimationDuration.inMilliseconds; |
1138 | } |
1139 | // Check the direction of the target instead of the sign of the velocity because |
1140 | // we may snap in the opposite direction of velocity if velocity is very low. |
1141 | else if (_pixelSnapSize < position) { |
1142 | velocity = math.min(-minimumSpeed, initialVelocity); |
1143 | } else { |
1144 | velocity = math.max(minimumSpeed, initialVelocity); |
1145 | } |
1146 | } |
1147 | |
1148 | final double position; |
1149 | late final double velocity; |
1150 | |
1151 | // A minimum speed to snap at. Used to ensure that the snapping animation |
1152 | // does not play too slowly. |
1153 | static const double minimumSpeed = 1600.0; |
1154 | |
1155 | late final double _pixelSnapSize; |
1156 | |
1157 | @override |
1158 | double dx(double time) { |
1159 | if (isDone(time)) { |
1160 | return 0; |
1161 | } |
1162 | return velocity; |
1163 | } |
1164 | |
1165 | @override |
1166 | bool isDone(double time) { |
1167 | return x(time) == _pixelSnapSize; |
1168 | } |
1169 | |
1170 | @override |
1171 | double x(double time) { |
1172 | final double newPosition = position + velocity * time; |
1173 | if ((velocity >= 0 && newPosition > _pixelSnapSize) || |
1174 | (velocity < 0 && newPosition < _pixelSnapSize)) { |
1175 | // We're passed the snap size, return it instead. |
1176 | return _pixelSnapSize; |
1177 | } |
1178 | return newPosition; |
1179 | } |
1180 | |
1181 | // Find the two closest snap sizes to the position. If the velocity is |
1182 | // non-zero, select the size in the velocity's direction. Otherwise, |
1183 | // the nearest snap size. |
1184 | double _getSnapSize(double initialVelocity, List<double> pixelSnapSizes) { |
1185 | final int indexOfNextSize = pixelSnapSizes.indexWhere((double size) => size >= position); |
1186 | if (indexOfNextSize == 0) { |
1187 | return pixelSnapSizes.first; |
1188 | } |
1189 | final double nextSize = pixelSnapSizes[indexOfNextSize]; |
1190 | final double previousSize = pixelSnapSizes[indexOfNextSize - 1]; |
1191 | if (initialVelocity.abs() <= tolerance.velocity) { |
1192 | // If velocity is zero, snap to the nearest snap size with the minimum velocity. |
1193 | if (position - previousSize < nextSize - position) { |
1194 | return previousSize; |
1195 | } else { |
1196 | return nextSize; |
1197 | } |
1198 | } |
1199 | // Snap forward or backward depending on current velocity. |
1200 | if (initialVelocity < 0.0) { |
1201 | return pixelSnapSizes[indexOfNextSize - 1]; |
1202 | } |
1203 | return pixelSnapSizes[indexOfNextSize]; |
1204 | } |
1205 | } |
1206 |
Definitions
- DraggableScrollableController
- DraggableScrollableController
- size
- pixels
- sizeToPixels
- isAttached
- pixelsToSize
- animateTo
- jumpTo
- reset
- _assertAttached
- _attach
- _onExtentReplaced
- _detach
- _disposeAnimationControllers
- DraggableScrollableSheet
- DraggableScrollableSheet
- createState
- DraggableScrollableNotification
- DraggableScrollableNotification
- debugFillDescription
- _DraggableSheetExtent
- _DraggableSheetExtent
- isAtMin
- isAtMax
- currentSize
- currentPixels
- pixelSnapSizes
- startActivity
- addPixelDelta
- updateSize
- pixelsToSize
- sizeToPixels
- dispose
- copyWith
- _DraggableScrollableSheetState
- initState
- _impliedSnapSizes
- didUpdateWidget
- didChangeDependencies
- build
- dispose
- _replaceExtent
- _snapSizeErrorMessage
- _DraggableScrollableSheetScrollController
- _DraggableScrollableSheetScrollController
- createScrollPosition
- debugFillDescription
- position
- reset
- detach
- _DraggableScrollableSheetScrollPosition
- _DraggableScrollableSheetScrollPosition
- listShouldScroll
- extent
- absorb
- beginActivity
- applyUserOffset
- _getCurrentSnapSize
- _isAtSnapSize
- _shouldSnap
- dispose
- goBallistic
- tick
- drag
- DraggableScrollableActuator
- DraggableScrollableActuator
- reset
- createState
- _DraggableScrollableActuatorState
- build
- dispose
- _ResetNotifier
- _ResetNotifier
- sendReset
- _InheritedResetNotifier
- _InheritedResetNotifier
- _sendReset
- shouldReset
- _SnappingSimulation
- _SnappingSimulation
- dx
- isDone
- x
Learn more about Flutter for embedded and desktop on industrialflutter.com