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/widgets.dart'; |
6 | /// @docImport 'package:flutter_test/flutter_test.dart'; |
7 | library; |
8 | |
9 | import 'dart:ui' as ui show lerpDouble; |
10 | |
11 | import 'package:flutter/foundation.dart'; |
12 | import 'package:flutter/physics.dart'; |
13 | import 'package:flutter/scheduler.dart'; |
14 | import 'package:flutter/semantics.dart'; |
15 | |
16 | import 'animation.dart'; |
17 | import 'curves.dart'; |
18 | import 'listener_helpers.dart'; |
19 | |
20 | export 'package:flutter/physics.dart' show Simulation, SpringDescription; |
21 | export 'package:flutter/scheduler.dart' show TickerFuture, TickerProvider; |
22 | |
23 | export 'animation.dart' show Animation, AnimationStatus; |
24 | export 'curves.dart' show Curve; |
25 | |
26 | // Examples can assume: |
27 | // late AnimationController _controller, fadeAnimationController, sizeAnimationController; |
28 | // late bool dismissed; |
29 | // void setState(VoidCallback fn) { } |
30 | |
31 | /// The direction in which an animation is running. |
32 | enum _AnimationDirection { |
33 | /// The animation is running from beginning to end. |
34 | forward, |
35 | |
36 | /// The animation is running backwards, from end to beginning. |
37 | reverse, |
38 | } |
39 | |
40 | final SpringDescription _kFlingSpringDescription = SpringDescription.withDampingRatio( |
41 | mass: 1.0, |
42 | stiffness: 500.0, |
43 | ); |
44 | |
45 | const Tolerance _kFlingTolerance = Tolerance(velocity: double.infinity, distance: 0.01); |
46 | |
47 | /// Configures how an [AnimationController] behaves when animations are |
48 | /// disabled. |
49 | /// |
50 | /// When [AccessibilityFeatures.disableAnimations] is true, the device is asking |
51 | /// Flutter to reduce or disable animations as much as possible. To honor this, |
52 | /// we reduce the duration and the corresponding number of frames for |
53 | /// animations. This enum is used to allow certain [AnimationController]s to opt |
54 | /// out of this behavior. |
55 | /// |
56 | /// For example, the [AnimationController] which controls the physics simulation |
57 | /// for a scrollable list will have [AnimationBehavior.preserve], so that when |
58 | /// a user attempts to scroll it does not jump to the end/beginning too quickly. |
59 | enum AnimationBehavior { |
60 | /// The [AnimationController] will reduce its duration when |
61 | /// [AccessibilityFeatures.disableAnimations] is true. |
62 | normal, |
63 | |
64 | /// The [AnimationController] will preserve its behavior. |
65 | /// |
66 | /// This is the default for repeating animations in order to prevent them from |
67 | /// flashing rapidly on the screen if the widget does not take the |
68 | /// [AccessibilityFeatures.disableAnimations] flag into account. |
69 | preserve, |
70 | } |
71 | |
72 | /// A controller for an animation. |
73 | /// |
74 | /// This class lets you perform tasks such as: |
75 | /// |
76 | /// * Play an animation [forward] or in [reverse], or [stop] an animation. |
77 | /// * Set the animation to a specific [value]. |
78 | /// * Define the [upperBound] and [lowerBound] values of an animation. |
79 | /// * Create a [fling] animation effect using a physics simulation. |
80 | /// |
81 | /// By default, an [AnimationController] linearly produces values that range |
82 | /// from 0.0 to 1.0, during a given duration. |
83 | /// |
84 | /// When the animation is actively animating, the animation controller generates |
85 | /// a new value each time the device running your app is ready to display a new |
86 | /// frame (typically, this rate is around 60–120 values per second). |
87 | /// If the animation controller is associated with a [State] |
88 | /// through a [TickerProvider], then its updates will be silenced when that |
89 | /// [State]'s subtree is disabled as defined by [TickerMode]; time will still |
90 | /// elapse, and methods like [forward] and [stop] can still be called and |
91 | /// will change the value, but the controller will not generate new values |
92 | /// on its own. |
93 | /// |
94 | /// ## Ticker providers |
95 | /// |
96 | /// An [AnimationController] needs a [TickerProvider], which is configured using |
97 | /// the `vsync` argument on the constructor. |
98 | /// The constructor uses the [TickerProvider] to create a [Ticker], which |
99 | /// the [AnimationController] uses to step through the animation it controls. |
100 | /// |
101 | /// For advice on obtaining a ticker provider, see [TickerProvider]. |
102 | /// Typically the relevant [State] serves as the ticker provider, |
103 | /// after applying a suitable mixin (like [SingleTickerProviderStateMixin]) |
104 | /// to cause the [State] subclass to implement [TickerProvider]. |
105 | /// |
106 | /// ## Life cycle |
107 | /// |
108 | /// An [AnimationController] should be [dispose]d when it is no longer needed. |
109 | /// This reduces the likelihood of leaks. When used with a [StatefulWidget], it |
110 | /// is common for an [AnimationController] to be created in the |
111 | /// [State.initState] method and then disposed in the [State.dispose] method. |
112 | /// |
113 | /// ## Using [Future]s with [AnimationController] |
114 | /// |
115 | /// The methods that start animations return a [TickerFuture] object which |
116 | /// completes when the animation completes successfully, and never throws an |
117 | /// error; if the animation is canceled, the future never completes. This object |
118 | /// also has a [TickerFuture.orCancel] property which returns a future that |
119 | /// completes when the animation completes successfully, and completes with an |
120 | /// error when the animation is aborted. |
121 | /// |
122 | /// This can be used to write code such as the `fadeOutAndUpdateState` method |
123 | /// below. |
124 | /// |
125 | /// {@tool snippet} |
126 | /// |
127 | /// Here is a stateful `Foo` widget. Its [State] uses the |
128 | /// [SingleTickerProviderStateMixin] to implement the necessary |
129 | /// [TickerProvider], creating its controller in the [State.initState] method |
130 | /// and disposing of it in the [State.dispose] method. The duration of the |
131 | /// controller is configured from a property in the `Foo` widget; as that |
132 | /// changes, the [State.didUpdateWidget] method is used to update the |
133 | /// controller. |
134 | /// |
135 | /// ```dart |
136 | /// class Foo extends StatefulWidget { |
137 | /// const Foo({ super.key, required this.duration }); |
138 | /// |
139 | /// final Duration duration; |
140 | /// |
141 | /// @override |
142 | /// State<Foo> createState() => _FooState(); |
143 | /// } |
144 | /// |
145 | /// class _FooState extends State<Foo> with SingleTickerProviderStateMixin { |
146 | /// late AnimationController _controller; |
147 | /// |
148 | /// @override |
149 | /// void initState() { |
150 | /// super.initState(); |
151 | /// _controller = AnimationController( |
152 | /// vsync: this, // the SingleTickerProviderStateMixin |
153 | /// duration: widget.duration, |
154 | /// ); |
155 | /// } |
156 | /// |
157 | /// @override |
158 | /// void didUpdateWidget(Foo oldWidget) { |
159 | /// super.didUpdateWidget(oldWidget); |
160 | /// _controller.duration = widget.duration; |
161 | /// } |
162 | /// |
163 | /// @override |
164 | /// void dispose() { |
165 | /// _controller.dispose(); |
166 | /// super.dispose(); |
167 | /// } |
168 | /// |
169 | /// @override |
170 | /// Widget build(BuildContext context) { |
171 | /// return Container(); // ... |
172 | /// } |
173 | /// } |
174 | /// ``` |
175 | /// {@end-tool} |
176 | /// {@tool snippet} |
177 | /// |
178 | /// The following method (for a [State] subclass) drives two animation |
179 | /// controllers using Dart's asynchronous syntax for awaiting [Future] objects: |
180 | /// |
181 | /// ```dart |
182 | /// Future<void> fadeOutAndUpdateState() async { |
183 | /// try { |
184 | /// await fadeAnimationController.forward().orCancel; |
185 | /// await sizeAnimationController.forward().orCancel; |
186 | /// setState(() { |
187 | /// dismissed = true; |
188 | /// }); |
189 | /// } on TickerCanceled { |
190 | /// // the animation got canceled, probably because we were disposed |
191 | /// } |
192 | /// } |
193 | /// ``` |
194 | /// {@end-tool} |
195 | /// |
196 | /// The assumption in the code above is that the animation controllers are being |
197 | /// disposed in the [State] subclass' override of the [State.dispose] method. |
198 | /// Since disposing the controller cancels the animation (raising a |
199 | /// [TickerCanceled] exception), the code here can skip verifying whether |
200 | /// [State.mounted] is still true at each step. (Again, this assumes that the |
201 | /// controllers are created in [State.initState] and disposed in |
202 | /// [State.dispose], as described in the previous section.) |
203 | /// |
204 | /// {@tool dartpad} |
205 | /// This example shows how to use [AnimationController] and |
206 | /// [SlideTransition] to create an animated digit like you might find |
207 | /// on an old pinball machine our your car's odometer. New digit |
208 | /// values slide into place from below, as the old value slides |
209 | /// upwards and out of view. Taps that occur while the controller is |
210 | /// already animating cause the controller's |
211 | /// [AnimationController.duration] to be reduced so that the visuals |
212 | /// don't fall behind. |
213 | /// |
214 | /// ** See code in examples/api/lib/animation/animation_controller/animated_digit.0.dart ** |
215 | /// {@end-tool} |
216 | /// |
217 | /// See also: |
218 | /// |
219 | /// * [Tween], the base class for converting an [AnimationController] to a |
220 | /// range of values of other types. |
221 | class AnimationController extends Animation<double> |
222 | with |
223 | AnimationEagerListenerMixin, |
224 | AnimationLocalListenersMixin, |
225 | AnimationLocalStatusListenersMixin { |
226 | /// Creates an animation controller. |
227 | /// |
228 | /// * `value` is the initial value of the animation. If defaults to the lower |
229 | /// bound. |
230 | /// |
231 | /// * [duration] is the length of time this animation should last. |
232 | /// |
233 | /// * [debugLabel] is a string to help identify this animation during |
234 | /// debugging (used by [toString]). |
235 | /// |
236 | /// * [lowerBound] is the smallest value this animation can obtain and the |
237 | /// value at which this animation is deemed to be dismissed. |
238 | /// |
239 | /// * [upperBound] is the largest value this animation can obtain and the |
240 | /// value at which this animation is deemed to be completed. |
241 | /// |
242 | /// * `vsync` is the required [TickerProvider] for the current context. It can |
243 | /// be changed by calling [resync]. See [TickerProvider] for advice on |
244 | /// obtaining a ticker provider. |
245 | AnimationController({ |
246 | double? value, |
247 | this.duration, |
248 | this.reverseDuration, |
249 | this.debugLabel, |
250 | this.lowerBound = 0.0, |
251 | this.upperBound = 1.0, |
252 | this.animationBehavior = AnimationBehavior.normal, |
253 | required TickerProvider vsync, |
254 | }) : assert(upperBound >= lowerBound), |
255 | _direction = _AnimationDirection.forward { |
256 | assert(debugMaybeDispatchCreated('animation' , 'AnimationController' , this)); |
257 | _ticker = vsync.createTicker(_tick); |
258 | _internalSetValue(value ?? lowerBound); |
259 | } |
260 | |
261 | /// Creates an animation controller with no upper or lower bound for its |
262 | /// value. |
263 | /// |
264 | /// * [value] is the initial value of the animation. |
265 | /// |
266 | /// * [duration] is the length of time this animation should last. |
267 | /// |
268 | /// * [debugLabel] is a string to help identify this animation during |
269 | /// debugging (used by [toString]). |
270 | /// |
271 | /// * `vsync` is the required [TickerProvider] for the current context. It can |
272 | /// be changed by calling [resync]. See [TickerProvider] for advice on |
273 | /// obtaining a ticker provider. |
274 | /// |
275 | /// This constructor is most useful for animations that will be driven using a |
276 | /// physics simulation, especially when the physics simulation has no |
277 | /// pre-determined bounds. |
278 | AnimationController.unbounded({ |
279 | double value = 0.0, |
280 | this.duration, |
281 | this.reverseDuration, |
282 | this.debugLabel, |
283 | required TickerProvider vsync, |
284 | this.animationBehavior = AnimationBehavior.preserve, |
285 | }) : lowerBound = double.negativeInfinity, |
286 | upperBound = double.infinity, |
287 | _direction = _AnimationDirection.forward { |
288 | assert(debugMaybeDispatchCreated('animation' , 'AnimationController' , this)); |
289 | _ticker = vsync.createTicker(_tick); |
290 | _internalSetValue(value); |
291 | } |
292 | |
293 | /// The value at which this animation is deemed to be dismissed. |
294 | final double lowerBound; |
295 | |
296 | /// The value at which this animation is deemed to be completed. |
297 | final double upperBound; |
298 | |
299 | /// A label that is used in the [toString] output. Intended to aid with |
300 | /// identifying animation controller instances in debug output. |
301 | final String? debugLabel; |
302 | |
303 | /// The behavior of the controller when [AccessibilityFeatures.disableAnimations] |
304 | /// is true. |
305 | /// |
306 | /// Defaults to [AnimationBehavior.normal] for the [AnimationController.new] |
307 | /// constructor, and [AnimationBehavior.preserve] for the |
308 | /// [AnimationController.unbounded] constructor. |
309 | final AnimationBehavior animationBehavior; |
310 | |
311 | /// Returns an [Animation<double>] for this animation controller, so that a |
312 | /// pointer to this object can be passed around without allowing users of that |
313 | /// pointer to mutate the [AnimationController] state. |
314 | Animation<double> get view => this; |
315 | |
316 | /// The length of time this animation should last. |
317 | /// |
318 | /// If [reverseDuration] is specified, then [duration] is only used when going |
319 | /// [forward]. Otherwise, it specifies the duration going in both directions. |
320 | Duration? duration; |
321 | |
322 | /// The length of time this animation should last when going in [reverse]. |
323 | /// |
324 | /// The value of [duration] is used if [reverseDuration] is not specified or |
325 | /// set to null. |
326 | Duration? reverseDuration; |
327 | |
328 | Ticker? _ticker; |
329 | |
330 | /// Recreates the [Ticker] with the new [TickerProvider]. |
331 | void resync(TickerProvider vsync) { |
332 | final Ticker oldTicker = _ticker!; |
333 | _ticker = vsync.createTicker(_tick); |
334 | _ticker!.absorbTicker(oldTicker); |
335 | } |
336 | |
337 | Simulation? _simulation; |
338 | |
339 | /// The current value of the animation. |
340 | /// |
341 | /// Setting this value notifies all the listeners that the value |
342 | /// changed. |
343 | /// |
344 | /// Setting this value also stops the controller if it is currently |
345 | /// running; if this happens, it also notifies all the status |
346 | /// listeners. |
347 | @override |
348 | double get value => _value; |
349 | late double _value; |
350 | |
351 | /// Stops the animation controller and sets the current value of the |
352 | /// animation. |
353 | /// |
354 | /// The new value is clamped to the range set by [lowerBound] and |
355 | /// [upperBound]. |
356 | /// |
357 | /// Value listeners are notified even if this does not change the value. |
358 | /// Status listeners are notified if the animation was previously playing. |
359 | /// |
360 | /// The most recently returned [TickerFuture], if any, is marked as having been |
361 | /// canceled, meaning the future never completes and its [TickerFuture.orCancel] |
362 | /// derivative future completes with a [TickerCanceled] error. |
363 | /// |
364 | /// See also: |
365 | /// |
366 | /// * [reset], which is equivalent to setting [value] to [lowerBound]. |
367 | /// * [stop], which aborts the animation without changing its value or status |
368 | /// and without dispatching any notifications other than completing or |
369 | /// canceling the [TickerFuture]. |
370 | /// * [forward], [reverse], [animateTo], [animateWith], [animateBackWith], |
371 | /// [fling], and [repeat], which start the animation controller. |
372 | set value(double newValue) { |
373 | stop(); |
374 | _internalSetValue(newValue); |
375 | notifyListeners(); |
376 | _checkStatusChanged(); |
377 | } |
378 | |
379 | /// Sets the controller's value to [lowerBound], stopping the animation (if |
380 | /// in progress), and resetting to its beginning point, or dismissed state. |
381 | /// |
382 | /// The most recently returned [TickerFuture], if any, is marked as having been |
383 | /// canceled, meaning the future never completes and its [TickerFuture.orCancel] |
384 | /// derivative future completes with a [TickerCanceled] error. |
385 | /// |
386 | /// See also: |
387 | /// |
388 | /// * [value], which can be explicitly set to a specific value as desired. |
389 | /// * [forward], which starts the animation in the forward direction. |
390 | /// * [stop], which aborts the animation without changing its value or status |
391 | /// and without dispatching any notifications other than completing or |
392 | /// canceling the [TickerFuture]. |
393 | void reset() { |
394 | value = lowerBound; |
395 | } |
396 | |
397 | /// The rate of change of [value] per second. |
398 | /// |
399 | /// If [isAnimating] is false, then [value] is not changing and the rate of |
400 | /// change is zero. |
401 | double get velocity { |
402 | if (!isAnimating) { |
403 | return 0.0; |
404 | } |
405 | return _simulation!.dx( |
406 | lastElapsedDuration!.inMicroseconds.toDouble() / Duration.microsecondsPerSecond, |
407 | ); |
408 | } |
409 | |
410 | void _internalSetValue(double newValue) { |
411 | _value = clampDouble(newValue, lowerBound, upperBound); |
412 | if (_value == lowerBound) { |
413 | _status = AnimationStatus.dismissed; |
414 | } else if (_value == upperBound) { |
415 | _status = AnimationStatus.completed; |
416 | } else { |
417 | _status = switch (_direction) { |
418 | _AnimationDirection.forward => AnimationStatus.forward, |
419 | _AnimationDirection.reverse => AnimationStatus.reverse, |
420 | }; |
421 | } |
422 | } |
423 | |
424 | /// The amount of time that has passed between the time the animation started |
425 | /// and the most recent tick of the animation. |
426 | /// |
427 | /// If the controller is not animating, the last elapsed duration is null. |
428 | Duration? get lastElapsedDuration => _lastElapsedDuration; |
429 | Duration? _lastElapsedDuration; |
430 | |
431 | /// Whether this animation is currently animating in either the forward or reverse direction. |
432 | /// |
433 | /// This is separate from whether it is actively ticking. An animation |
434 | /// controller's ticker might get muted, in which case the animation |
435 | /// controller's callbacks will no longer fire even though time is continuing |
436 | /// to pass. See [Ticker.muted] and [TickerMode]. |
437 | /// |
438 | /// If the animation was stopped (e.g. with [stop] or by setting a new [value]), |
439 | /// [isAnimating] will return `false` but the [status] will not change, |
440 | /// so the value of [AnimationStatus.isAnimating] might still be `true`. |
441 | @override |
442 | bool get isAnimating => _ticker != null && _ticker!.isActive; |
443 | |
444 | _AnimationDirection _direction; |
445 | |
446 | @override |
447 | AnimationStatus get status => _status; |
448 | late AnimationStatus _status; |
449 | |
450 | /// Starts running this animation forwards (towards the end). |
451 | /// |
452 | /// Returns a [TickerFuture] that completes when the animation is complete. |
453 | /// |
454 | /// If [from] is non-null, it will be set as the current [value] before running |
455 | /// the animation. |
456 | /// |
457 | /// The most recently returned [TickerFuture], if any, is marked as having been |
458 | /// canceled, meaning the future never completes and its [TickerFuture.orCancel] |
459 | /// derivative future completes with a [TickerCanceled] error. |
460 | /// |
461 | /// During the animation, [status] is reported as [AnimationStatus.forward], |
462 | /// which switches to [AnimationStatus.completed] when [upperBound] is |
463 | /// reached at the end of the animation. |
464 | TickerFuture forward({double? from}) { |
465 | assert(() { |
466 | if (duration == null) { |
467 | throw FlutterError( |
468 | 'AnimationController.forward() called with no default duration.\n' |
469 | 'The "duration" property should be set, either in the constructor or later, before ' |
470 | 'calling the forward() function.' , |
471 | ); |
472 | } |
473 | return true; |
474 | }()); |
475 | assert( |
476 | _ticker != null, |
477 | 'AnimationController.forward() called after AnimationController.dispose()\n' |
478 | 'AnimationController methods should not be used after calling dispose.' , |
479 | ); |
480 | _direction = _AnimationDirection.forward; |
481 | if (from != null) { |
482 | value = from; |
483 | } |
484 | return _animateToInternal(upperBound); |
485 | } |
486 | |
487 | /// Starts running this animation in reverse (towards the beginning). |
488 | /// |
489 | /// Returns a [TickerFuture] that completes when the animation is dismissed. |
490 | /// |
491 | /// If [from] is non-null, it will be set as the current [value] before running |
492 | /// the animation. |
493 | /// |
494 | /// The most recently returned [TickerFuture], if any, is marked as having been |
495 | /// canceled, meaning the future never completes and its [TickerFuture.orCancel] |
496 | /// derivative future completes with a [TickerCanceled] error. |
497 | /// |
498 | /// During the animation, [status] is reported as [AnimationStatus.reverse], |
499 | /// which switches to [AnimationStatus.dismissed] when [lowerBound] is |
500 | /// reached at the end of the animation. |
501 | TickerFuture reverse({double? from}) { |
502 | assert(() { |
503 | if (duration == null && reverseDuration == null) { |
504 | throw FlutterError( |
505 | 'AnimationController.reverse() called with no default duration or reverseDuration.\n' |
506 | 'The "duration" or "reverseDuration" property should be set, either in the constructor or later, before ' |
507 | 'calling the reverse() function.' , |
508 | ); |
509 | } |
510 | return true; |
511 | }()); |
512 | assert( |
513 | _ticker != null, |
514 | 'AnimationController.reverse() called after AnimationController.dispose()\n' |
515 | 'AnimationController methods should not be used after calling dispose.' , |
516 | ); |
517 | _direction = _AnimationDirection.reverse; |
518 | if (from != null) { |
519 | value = from; |
520 | } |
521 | return _animateToInternal(lowerBound); |
522 | } |
523 | |
524 | /// Toggles the direction of this animation, based on whether it [isForwardOrCompleted]. |
525 | /// |
526 | /// Specifically, this function acts the same way as [reverse] if the [status] is |
527 | /// either [AnimationStatus.forward] or [AnimationStatus.completed], and acts as |
528 | /// [forward] for [AnimationStatus.reverse] or [AnimationStatus.dismissed]. |
529 | /// |
530 | /// If [from] is non-null, it will be set as the current [value] before running |
531 | /// the animation. |
532 | /// |
533 | /// The most recently returned [TickerFuture], if any, is marked as having been |
534 | /// canceled, meaning the future never completes and its [TickerFuture.orCancel] |
535 | /// derivative future completes with a [TickerCanceled] error. |
536 | TickerFuture toggle({double? from}) { |
537 | assert(() { |
538 | Duration? duration = this.duration; |
539 | if (isForwardOrCompleted) { |
540 | duration ??= reverseDuration; |
541 | } |
542 | if (duration == null) { |
543 | throw FlutterError( |
544 | 'AnimationController.toggle() called with no default duration.\n' |
545 | 'The "duration" property should be set, either in the constructor or later, before ' |
546 | 'calling the toggle() function.' , |
547 | ); |
548 | } |
549 | return true; |
550 | }()); |
551 | assert( |
552 | _ticker != null, |
553 | 'AnimationController.toggle() called after AnimationController.dispose()\n' |
554 | 'AnimationController methods should not be used after calling dispose.' , |
555 | ); |
556 | _direction = isForwardOrCompleted ? _AnimationDirection.reverse : _AnimationDirection.forward; |
557 | if (from != null) { |
558 | value = from; |
559 | } |
560 | return _animateToInternal(switch (_direction) { |
561 | _AnimationDirection.forward => upperBound, |
562 | _AnimationDirection.reverse => lowerBound, |
563 | }); |
564 | } |
565 | |
566 | /// Drives the animation from its current value to the given target, "forward". |
567 | /// |
568 | /// Returns a [TickerFuture] that completes when the animation is complete. |
569 | /// |
570 | /// The most recently returned [TickerFuture], if any, is marked as having been |
571 | /// canceled, meaning the future never completes and its [TickerFuture.orCancel] |
572 | /// derivative future completes with a [TickerCanceled] error. |
573 | /// |
574 | /// During the animation, [status] is reported as [AnimationStatus.forward] |
575 | /// regardless of whether `target` > [value] or not. At the end of the |
576 | /// animation, when `target` is reached, [status] is reported as |
577 | /// [AnimationStatus.completed]. |
578 | /// |
579 | /// If the `target` argument is the same as the current [value] of the |
580 | /// animation, then this won't animate, and the returned [TickerFuture] will |
581 | /// be already complete. |
582 | TickerFuture animateTo(double target, {Duration? duration, Curve curve = Curves.linear}) { |
583 | assert(() { |
584 | if (this.duration == null && duration == null) { |
585 | throw FlutterError( |
586 | 'AnimationController.animateTo() called with no explicit duration and no default duration.\n' |
587 | 'Either the "duration" argument to the animateTo() method should be provided, or the ' |
588 | '"duration" property should be set, either in the constructor or later, before ' |
589 | 'calling the animateTo() function.' , |
590 | ); |
591 | } |
592 | return true; |
593 | }()); |
594 | assert( |
595 | _ticker != null, |
596 | 'AnimationController.animateTo() called after AnimationController.dispose()\n' |
597 | 'AnimationController methods should not be used after calling dispose.' , |
598 | ); |
599 | _direction = _AnimationDirection.forward; |
600 | return _animateToInternal(target, duration: duration, curve: curve); |
601 | } |
602 | |
603 | /// Drives the animation from its current value to the given target, "backward". |
604 | /// |
605 | /// Returns a [TickerFuture] that completes when the animation is complete. |
606 | /// |
607 | /// The most recently returned [TickerFuture], if any, is marked as having been |
608 | /// canceled, meaning the future never completes and its [TickerFuture.orCancel] |
609 | /// derivative future completes with a [TickerCanceled] error. |
610 | /// |
611 | /// During the animation, [status] is reported as [AnimationStatus.reverse] |
612 | /// regardless of whether `target` < [value] or not. At the end of the |
613 | /// animation, when `target` is reached, [status] is reported as |
614 | /// [AnimationStatus.dismissed]. |
615 | /// |
616 | /// If the `target` argument is the same as the current [value] of the |
617 | /// animation, then this won't animate, and the returned [TickerFuture] will |
618 | /// be already complete. |
619 | TickerFuture animateBack(double target, {Duration? duration, Curve curve = Curves.linear}) { |
620 | assert(() { |
621 | if (this.duration == null && reverseDuration == null && duration == null) { |
622 | throw FlutterError( |
623 | 'AnimationController.animateBack() called with no explicit duration and no default duration or reverseDuration.\n' |
624 | 'Either the "duration" argument to the animateBack() method should be provided, or the ' |
625 | '"duration" or "reverseDuration" property should be set, either in the constructor or later, before ' |
626 | 'calling the animateBack() function.' , |
627 | ); |
628 | } |
629 | return true; |
630 | }()); |
631 | assert( |
632 | _ticker != null, |
633 | 'AnimationController.animateBack() called after AnimationController.dispose()\n' |
634 | 'AnimationController methods should not be used after calling dispose.' , |
635 | ); |
636 | _direction = _AnimationDirection.reverse; |
637 | return _animateToInternal(target, duration: duration, curve: curve); |
638 | } |
639 | |
640 | TickerFuture _animateToInternal( |
641 | double target, { |
642 | Duration? duration, |
643 | Curve curve = Curves.linear, |
644 | }) { |
645 | final double scale = switch (animationBehavior) { |
646 | // Since the framework cannot handle zero duration animations, we run it at 5% of the normal |
647 | // duration to limit most animations to a single frame. |
648 | // Ideally, the framework would be able to handle zero duration animations, however, the common |
649 | // pattern of an eternally repeating animation might cause an endless loop if it weren't delayed |
650 | // for at least one frame. |
651 | AnimationBehavior.normal when SemanticsBinding.instance.disableAnimations => 0.05, |
652 | AnimationBehavior.normal || AnimationBehavior.preserve => 1.0, |
653 | }; |
654 | Duration? simulationDuration = duration; |
655 | if (simulationDuration == null) { |
656 | assert(!(this.duration == null && _direction == _AnimationDirection.forward)); |
657 | assert( |
658 | !(this.duration == null && |
659 | _direction == _AnimationDirection.reverse && |
660 | reverseDuration == null), |
661 | ); |
662 | final double range = upperBound - lowerBound; |
663 | final double remainingFraction = range.isFinite ? (target - _value).abs() / range : 1.0; |
664 | final Duration directionDuration = |
665 | (_direction == _AnimationDirection.reverse && reverseDuration != null) |
666 | ? reverseDuration! |
667 | : this.duration!; |
668 | simulationDuration = directionDuration * remainingFraction; |
669 | } else if (target == value) { |
670 | // Already at target, don't animate. |
671 | simulationDuration = Duration.zero; |
672 | } |
673 | stop(); |
674 | if (simulationDuration == Duration.zero) { |
675 | if (value != target) { |
676 | _value = clampDouble(target, lowerBound, upperBound); |
677 | notifyListeners(); |
678 | } |
679 | _status = |
680 | (_direction == _AnimationDirection.forward) |
681 | ? AnimationStatus.completed |
682 | : AnimationStatus.dismissed; |
683 | _checkStatusChanged(); |
684 | return TickerFuture.complete(); |
685 | } |
686 | assert(simulationDuration > Duration.zero); |
687 | assert(!isAnimating); |
688 | return _startSimulation( |
689 | _InterpolationSimulation(_value, target, simulationDuration, curve, scale), |
690 | ); |
691 | } |
692 | |
693 | /// Starts running this animation in the forward direction, and |
694 | /// restarts the animation when it completes. |
695 | /// |
696 | /// Defaults to repeating between the [lowerBound] and [upperBound] of the |
697 | /// [AnimationController] when no explicit value is set for [min] and [max]. |
698 | /// |
699 | /// With [reverse] set to true, instead of always starting over at [min] |
700 | /// the starting value will alternate between [min] and [max] values on each |
701 | /// repeat. The [status] will be reported as [AnimationStatus.reverse] when |
702 | /// the animation runs from [max] to [min]. |
703 | /// |
704 | /// Each run of the animation will have a duration of `period`. If `period` is not |
705 | /// provided, [duration] will be used instead, which has to be set before [repeat] is |
706 | /// called either in the constructor or later by using the [duration] setter. |
707 | /// |
708 | /// If a value is passed to [count], the animation will perform that many |
709 | /// iterations before stopping. Otherwise, the animation repeats indefinitely. |
710 | /// |
711 | /// Returns a [TickerFuture] that never completes, unless a [count] is specified. |
712 | /// The [TickerFuture.orCancel] future completes with an error when the animation is |
713 | /// stopped (e.g. with [stop]). |
714 | /// |
715 | /// The most recently returned [TickerFuture], if any, is marked as having been |
716 | /// canceled, meaning the future never completes and its [TickerFuture.orCancel] |
717 | /// derivative future completes with a [TickerCanceled] error. |
718 | TickerFuture repeat({ |
719 | double? min, |
720 | double? max, |
721 | bool reverse = false, |
722 | Duration? period, |
723 | int? count, |
724 | }) { |
725 | min ??= lowerBound; |
726 | max ??= upperBound; |
727 | period ??= duration; |
728 | assert(() { |
729 | if (period == null) { |
730 | throw FlutterError( |
731 | 'AnimationController.repeat() called without an explicit period and with no default Duration.\n' |
732 | 'Either the "period" argument to the repeat() method should be provided, or the ' |
733 | '"duration" property should be set, either in the constructor or later, before ' |
734 | 'calling the repeat() function.' , |
735 | ); |
736 | } |
737 | return true; |
738 | }()); |
739 | assert(max >= min); |
740 | assert(max <= upperBound && min >= lowerBound); |
741 | assert(count == null || count > 0, 'Count shall be greater than zero if not null' ); |
742 | stop(); |
743 | return _startSimulation( |
744 | _RepeatingSimulation(_value, min, max, reverse, period!, _directionSetter, count), |
745 | ); |
746 | } |
747 | |
748 | void _directionSetter(_AnimationDirection direction) { |
749 | _direction = direction; |
750 | _status = |
751 | (_direction == _AnimationDirection.forward) |
752 | ? AnimationStatus.forward |
753 | : AnimationStatus.reverse; |
754 | _checkStatusChanged(); |
755 | } |
756 | |
757 | /// Drives the animation with a spring (within [lowerBound] and [upperBound]) |
758 | /// and initial velocity. |
759 | /// |
760 | /// If velocity is positive, the animation will complete, otherwise it will |
761 | /// dismiss. The velocity is specified in units per second. If the |
762 | /// [SemanticsBinding.disableAnimations] flag is set, the velocity is somewhat |
763 | /// arbitrarily multiplied by 200. |
764 | /// |
765 | /// The [springDescription] parameter can be used to specify a custom |
766 | /// [SpringType.criticallyDamped] or [SpringType.overDamped] spring with which |
767 | /// to drive the animation. By default, a [SpringType.criticallyDamped] spring |
768 | /// is used. See [SpringDescription.withDampingRatio] for how to create a |
769 | /// suitable [SpringDescription]. |
770 | /// |
771 | /// The resulting spring simulation cannot be of type [SpringType.underDamped]; |
772 | /// such a spring would oscillate rather than fling. |
773 | /// |
774 | /// Returns a [TickerFuture] that completes when the animation is complete. |
775 | /// |
776 | /// The most recently returned [TickerFuture], if any, is marked as having been |
777 | /// canceled, meaning the future never completes and its [TickerFuture.orCancel] |
778 | /// derivative future completes with a [TickerCanceled] error. |
779 | TickerFuture fling({ |
780 | double velocity = 1.0, |
781 | SpringDescription? springDescription, |
782 | AnimationBehavior? animationBehavior, |
783 | }) { |
784 | springDescription ??= _kFlingSpringDescription; |
785 | _direction = velocity < 0.0 ? _AnimationDirection.reverse : _AnimationDirection.forward; |
786 | final double target = |
787 | velocity < 0.0 |
788 | ? lowerBound - _kFlingTolerance.distance |
789 | : upperBound + _kFlingTolerance.distance; |
790 | final AnimationBehavior behavior = animationBehavior ?? this.animationBehavior; |
791 | final double scale = switch (behavior) { |
792 | // This is arbitrary (it was chosen because it worked for the drawer widget). |
793 | AnimationBehavior.normal when SemanticsBinding.instance.disableAnimations => 200.0, |
794 | AnimationBehavior.normal || AnimationBehavior.preserve => 1.0, |
795 | }; |
796 | final SpringSimulation simulation = SpringSimulation( |
797 | springDescription, |
798 | value, |
799 | target, |
800 | velocity * scale, |
801 | )..tolerance = _kFlingTolerance; |
802 | assert( |
803 | simulation.type != SpringType.underDamped, |
804 | 'The specified spring simulation is of type SpringType.underDamped.\n' |
805 | 'An underdamped spring results in oscillation rather than a fling. ' |
806 | 'Consider specifying a different springDescription, or use animateWith() ' |
807 | 'with an explicit SpringSimulation if an underdamped spring is intentional.' , |
808 | ); |
809 | stop(); |
810 | return _startSimulation(simulation); |
811 | } |
812 | |
813 | /// Drives the animation according to the given simulation. |
814 | /// |
815 | /// {@template flutter.animation.AnimationController.animateWith} |
816 | /// The values from the simulation are clamped to the [lowerBound] and |
817 | /// [upperBound]. To avoid this, consider creating the [AnimationController] |
818 | /// using the [AnimationController.unbounded] constructor. |
819 | /// |
820 | /// Returns a [TickerFuture] that completes when the animation is complete. |
821 | /// |
822 | /// The most recently returned [TickerFuture], if any, is marked as having been |
823 | /// canceled, meaning the future never completes and its [TickerFuture.orCancel] |
824 | /// derivative future completes with a [TickerCanceled] error. |
825 | /// {@endtemplate} |
826 | /// |
827 | /// The [status] is always [AnimationStatus.forward] for the entire duration |
828 | /// of the simulation. |
829 | /// |
830 | /// See also: |
831 | /// |
832 | /// * [animateBackWith], which is like this method but the status is always |
833 | /// [AnimationStatus.reverse]. |
834 | TickerFuture animateWith(Simulation simulation) { |
835 | assert( |
836 | _ticker != null, |
837 | 'AnimationController.animateWith() called after AnimationController.dispose()\n' |
838 | 'AnimationController methods should not be used after calling dispose.' , |
839 | ); |
840 | stop(); |
841 | _direction = _AnimationDirection.forward; |
842 | return _startSimulation(simulation); |
843 | } |
844 | |
845 | /// Drives the animation according to the given simulation with a [status] of |
846 | /// [AnimationStatus.reverse]. |
847 | /// |
848 | /// {@macro flutter.animation.AnimationController.animateWith} |
849 | /// |
850 | /// The [status] is always [AnimationStatus.reverse] for the entire duration |
851 | /// of the simulation. |
852 | /// |
853 | /// See also: |
854 | /// |
855 | /// * [animateWith], which is like this method but the status is always |
856 | /// [AnimationStatus.forward]. |
857 | TickerFuture animateBackWith(Simulation simulation) { |
858 | assert( |
859 | _ticker != null, |
860 | 'AnimationController.animateWith() called after AnimationController.dispose()\n' |
861 | 'AnimationController methods should not be used after calling dispose.' , |
862 | ); |
863 | stop(); |
864 | _direction = _AnimationDirection.reverse; |
865 | return _startSimulation(simulation); |
866 | } |
867 | |
868 | TickerFuture _startSimulation(Simulation simulation) { |
869 | assert(!isAnimating); |
870 | _simulation = simulation; |
871 | _lastElapsedDuration = Duration.zero; |
872 | _value = clampDouble(simulation.x(0.0), lowerBound, upperBound); |
873 | final TickerFuture result = _ticker!.start(); |
874 | _status = |
875 | (_direction == _AnimationDirection.forward) |
876 | ? AnimationStatus.forward |
877 | : AnimationStatus.reverse; |
878 | _checkStatusChanged(); |
879 | return result; |
880 | } |
881 | |
882 | /// Stops running this animation. |
883 | /// |
884 | /// This does not trigger any notifications. The animation stops in its |
885 | /// current state. |
886 | /// |
887 | /// By default, the most recently returned [TickerFuture] is marked as having |
888 | /// been canceled, meaning the future never completes and its |
889 | /// [TickerFuture.orCancel] derivative future completes with a [TickerCanceled] |
890 | /// error. By passing the `canceled` argument with the value false, this is |
891 | /// reversed, and the futures complete successfully. |
892 | /// |
893 | /// See also: |
894 | /// |
895 | /// * [reset], which stops the animation and resets it to the [lowerBound], |
896 | /// and which does send notifications. |
897 | /// * [forward], [reverse], [animateTo], [animateWith], [fling], and [repeat], |
898 | /// which restart the animation controller. |
899 | void stop({bool canceled = true}) { |
900 | assert( |
901 | _ticker != null, |
902 | 'AnimationController.stop() called after AnimationController.dispose()\n' |
903 | 'AnimationController methods should not be used after calling dispose.' , |
904 | ); |
905 | _simulation = null; |
906 | _lastElapsedDuration = null; |
907 | _ticker!.stop(canceled: canceled); |
908 | } |
909 | |
910 | /// Release the resources used by this object. The object is no longer usable |
911 | /// after this method is called. |
912 | /// |
913 | /// The most recently returned [TickerFuture], if any, is marked as having been |
914 | /// canceled, meaning the future never completes and its [TickerFuture.orCancel] |
915 | /// derivative future completes with a [TickerCanceled] error. |
916 | @override |
917 | void dispose() { |
918 | assert(() { |
919 | if (_ticker == null) { |
920 | throw FlutterError.fromParts(<DiagnosticsNode>[ |
921 | ErrorSummary('AnimationController.dispose() called more than once.' ), |
922 | ErrorDescription('A given $runtimeType cannot be disposed more than once.\n' ), |
923 | DiagnosticsProperty<AnimationController>( |
924 | 'The following $runtimeType object was disposed multiple times' , |
925 | this, |
926 | style: DiagnosticsTreeStyle.errorProperty, |
927 | ), |
928 | ]); |
929 | } |
930 | return true; |
931 | }()); |
932 | assert(debugMaybeDispatchDisposed(this)); |
933 | _ticker!.dispose(); |
934 | _ticker = null; |
935 | clearStatusListeners(); |
936 | clearListeners(); |
937 | super.dispose(); |
938 | } |
939 | |
940 | AnimationStatus _lastReportedStatus = AnimationStatus.dismissed; |
941 | void _checkStatusChanged() { |
942 | final AnimationStatus newStatus = status; |
943 | if (_lastReportedStatus != newStatus) { |
944 | _lastReportedStatus = newStatus; |
945 | notifyStatusListeners(newStatus); |
946 | } |
947 | } |
948 | |
949 | void _tick(Duration elapsed) { |
950 | _lastElapsedDuration = elapsed; |
951 | final double elapsedInSeconds = |
952 | elapsed.inMicroseconds.toDouble() / Duration.microsecondsPerSecond; |
953 | assert(elapsedInSeconds >= 0.0); |
954 | _value = clampDouble(_simulation!.x(elapsedInSeconds), lowerBound, upperBound); |
955 | if (_simulation!.isDone(elapsedInSeconds)) { |
956 | _status = |
957 | (_direction == _AnimationDirection.forward) |
958 | ? AnimationStatus.completed |
959 | : AnimationStatus.dismissed; |
960 | stop(canceled: false); |
961 | } |
962 | notifyListeners(); |
963 | _checkStatusChanged(); |
964 | } |
965 | |
966 | @override |
967 | String toStringDetails() { |
968 | final String paused = isAnimating ? '' : '; paused' ; |
969 | final String ticker = _ticker == null ? '; DISPOSED' : (_ticker!.muted ? '; silenced' : '' ); |
970 | String label = '' ; |
971 | assert(() { |
972 | if (debugLabel != null) { |
973 | label = '; for $debugLabel' ; |
974 | } |
975 | return true; |
976 | }()); |
977 | final String more = ' ${super.toStringDetails()} ${value.toStringAsFixed(3)}' ; |
978 | return ' $more$paused$ticker$label' ; |
979 | }
|
980 | }
|
981 |
|
982 | class _InterpolationSimulation extends Simulation {
|
983 | _InterpolationSimulation(this._begin, this._end, Duration duration, this._curve, double scale)
|
984 | : assert(duration.inMicroseconds > 0),
|
985 | _durationInSeconds = (duration.inMicroseconds * scale) / Duration.microsecondsPerSecond;
|
986 |
|
987 | final double _durationInSeconds;
|
988 | final double _begin;
|
989 | final double _end;
|
990 | final Curve _curve;
|
991 |
|
992 | @override
|
993 | double x(double timeInSeconds) {
|
994 | final double t = clampDouble(timeInSeconds / _durationInSeconds, 0.0, 1.0);
|
995 | return switch (t) {
|
996 | 0.0 => _begin,
|
997 | 1.0 => _end,
|
998 | _ => _begin + (_end - _begin) * _curve.transform(t),
|
999 | };
|
1000 | }
|
1001 |
|
1002 | @override
|
1003 | double dx(double timeInSeconds) {
|
1004 | final double epsilon = tolerance.time;
|
1005 | return (x(timeInSeconds + epsilon) - x(timeInSeconds - epsilon)) / (2 * epsilon);
|
1006 | }
|
1007 |
|
1008 | @override
|
1009 | bool isDone(double timeInSeconds) => timeInSeconds > _durationInSeconds;
|
1010 | }
|
1011 |
|
1012 | typedef _DirectionSetter = void Function(_AnimationDirection direction);
|
1013 |
|
1014 | class _RepeatingSimulation extends Simulation {
|
1015 | _RepeatingSimulation(
|
1016 | double initialValue,
|
1017 | this.min,
|
1018 | this.max,
|
1019 | this.reverse,
|
1020 | Duration period,
|
1021 | this.directionSetter,
|
1022 | this.count,
|
1023 | ) : assert(count == null || count > 0, 'Count shall be greater than zero if not null' ),
|
1024 | _periodInSeconds = period.inMicroseconds / Duration.microsecondsPerSecond,
|
1025 | _initialT =
|
1026 | (max == min)
|
1027 | ? 0.0
|
1028 | : ((clampDouble(initialValue, min, max) - min) / (max - min)) *
|
1029 | (period.inMicroseconds / Duration.microsecondsPerSecond) {
|
1030 | assert(_periodInSeconds > 0.0);
|
1031 | assert(_initialT >= 0.0);
|
1032 | }
|
1033 |
|
1034 | final double min;
|
1035 | final double max;
|
1036 | final bool reverse;
|
1037 | final int? count;
|
1038 | final _DirectionSetter directionSetter;
|
1039 |
|
1040 | final double _periodInSeconds;
|
1041 | final double _initialT;
|
1042 |
|
1043 | late final double _exitTimeInSeconds = (count! * _periodInSeconds) - _initialT;
|
1044 |
|
1045 | @override
|
1046 | double x(double timeInSeconds) {
|
1047 | assert(timeInSeconds >= 0.0);
|
1048 |
|
1049 | final double totalTimeInSeconds = timeInSeconds + _initialT;
|
1050 | final double t = (totalTimeInSeconds / _periodInSeconds) % 1.0;
|
1051 | final bool isPlayingReverse = (totalTimeInSeconds ~/ _periodInSeconds).isOdd;
|
1052 |
|
1053 | if (reverse && isPlayingReverse) {
|
1054 | directionSetter(_AnimationDirection.reverse);
|
1055 | return ui.lerpDouble(max, min, t)!;
|
1056 | } else {
|
1057 | directionSetter(_AnimationDirection.forward);
|
1058 | return ui.lerpDouble(min, max, t)!;
|
1059 | }
|
1060 | }
|
1061 |
|
1062 | @override
|
1063 | double dx(double timeInSeconds) => (max - min) / _periodInSeconds;
|
1064 |
|
1065 | @override
|
1066 | bool isDone(double timeInSeconds) {
|
1067 | // if [timeInSeconds] elapsed the [_exitTimeInSeconds] && [count] is not null,
|
1068 | // consider marking the simulation as "DONE"
|
1069 | return count != null && (timeInSeconds >= _exitTimeInSeconds);
|
1070 | }
|
1071 | }
|
1072 |
|