Commit 05c9ca91 authored by Adam Barth's avatar Adam Barth

Add dartdoc for the animation library

parent 6fc343a0
......@@ -9,14 +9,22 @@ import 'package:sky/src/animation/scheduler.dart';
const double _kSecondsPerMillisecond = 1000.0;
// TODO(abarth): Change from double to Duration.
typedef _TickerCallback(double timeStamp);
/// Calls its callback once per animation frame
class Ticker {
Ticker(Function onTick) : _onTick = onTick;
/// Constructs a ticker that will call onTick once per frame while running
Ticker(_TickerCallback onTick) : _onTick = onTick;
final Function _onTick;
final _TickerCallback _onTick;
Completer _completer;
int _animationId;
/// Start calling onTick once per animation frame
///
/// The returned future resolves once the ticker stops ticking.
Future start() {
assert(!isTicking);
_completer = new Completer();
......@@ -24,6 +32,9 @@ class Ticker {
return _completer.future;
}
/// Stop calling onTick
///
/// Causes the future returned by [start] to resolve.
void stop() {
if (!isTicking)
return;
......@@ -42,6 +53,7 @@ class Ticker {
localCompleter.complete();
}
/// Whether this ticker has scheduled a call to onTick
bool get isTicking => _completer != null;
void _tick(double timeStamp) {
......@@ -63,6 +75,7 @@ class Ticker {
}
}
/// Ticks a simulation once per frame
class AnimatedSimulation {
AnimatedSimulation(Function onTick) : _onTick = onTick {
......@@ -76,6 +89,7 @@ class AnimatedSimulation {
double _startTime;
double _value = 0.0;
/// The current value of the simulation
double get value => _value;
void set value(double newValue) {
assert(!_ticker.isTicking);
......@@ -83,6 +97,9 @@ class AnimatedSimulation {
_onTick(_value);
}
/// Start ticking the given simulation once per frame
///
/// Returns a future that resolves when the simulation stops ticking.
Future start(Simulation simulation) {
assert(simulation != null);
assert(!_ticker.isTicking);
......@@ -92,12 +109,14 @@ class AnimatedSimulation {
return _ticker.start();
}
/// Stop ticking the current simulation
void stop() {
_simulation = null;
_startTime = null;
_ticker.stop();
}
/// Whether this object is currently ticking a simulation
bool get isAnimating => _ticker.isTicking;
void _tick(double timeStamp) {
......
......@@ -6,24 +6,48 @@ import "dart:sky";
import 'package:sky/src/animation/curves.dart';
/// The direction in which an animation is running
enum Direction {
/// The animation is running from beginning to end
forward,
/// The animation is running backwards, from end to beginning
reverse
}
/// A variable that changes as an animation progresses
abstract class AnimatedVariable {
/// Update the variable to a given time in an animation that is running in the given direction
void setProgress(double t, Direction direction);
String toString();
}
///
class AnimationTiming {
AnimationTiming({this.interval, this.reverseInterval, this.curve, this.reverseCurve});
AnimationTiming({
this.interval,
this.reverseInterval,
this.curve,
this.reverseCurve
});
/// The interval during which this timing is active in the forward direction
Interval interval;
/// The interval during which this timing is active in the reverse direction
///
/// If this field is null, the timing defaules to using [interval] in both directions.
Interval reverseInterval;
/// The curve that this timing applies to the animation clock in the forward direction
Curve curve;
/// The curve that this timing applies to the animation clock in the reverse direction
///
/// If this field is null, the timing defaules to using [curve] in both directions.
Curve reverseCurve;
/// Applies this timing to the given animation clock value in the given direction
double transform(double t, Direction direction) {
Interval interval = _getInterval(direction);
if (interval != null)
......@@ -49,18 +73,26 @@ class AnimationTiming {
}
}
/// An animated variable with a concrete type
class AnimatedValue<T extends dynamic> extends AnimationTiming implements AnimatedVariable {
AnimatedValue(this.begin, { this.end, Interval interval, Curve curve, Curve reverseCurve })
: super(interval: interval, curve: curve, reverseCurve: reverseCurve) {
value = begin;
}
/// The current value of this variable
T value;
/// The value this variable has at the beginning of the animation
T begin;
/// The value this variable has at the end of the animation
T end;
/// Returns the value this variable has at the given animation clock value
T lerp(double t) => begin + (end - begin) * t;
/// Updates the value of this variable according to the given animation clock value and direction
void setProgress(double t, Direction direction) {
if (end != null) {
t = transform(t, direction);
......@@ -71,12 +103,15 @@ class AnimatedValue<T extends dynamic> extends AnimationTiming implements Animat
String toString() => 'AnimatedValue(begin=$begin, end=$end, value=$value)';
}
/// A list of animated variables
class AnimatedList extends AnimationTiming implements AnimatedVariable {
/// The list of variables contained in the list
List<AnimatedVariable> variables;
AnimatedList(this.variables, { Interval interval, Curve curve, Curve reverseCurve })
: super(interval: interval, curve: curve, reverseCurve: reverseCurve);
// Updates the value of all the variables in the list according to the given animation clock value and direction
void setProgress(double t, Direction direction) {
double adjustedTime = transform(t, direction);
for (AnimatedVariable variable in variables)
......@@ -86,6 +121,10 @@ class AnimatedList extends AnimationTiming implements AnimatedVariable {
String toString() => 'AnimatedList([$variables])';
}
/// An animated variable containing a color
///
/// This class specializes the interpolation of AnimatedValue<Color> to be
/// appropriate for colors.
class AnimatedColorValue extends AnimatedValue<Color> {
AnimatedColorValue(Color begin, { Color end, Curve curve })
: super(begin, end: end, curve: curve);
......@@ -93,6 +132,10 @@ class AnimatedColorValue extends AnimatedValue<Color> {
Color lerp(double t) => Color.lerp(begin, end, t);
}
/// An animated variable containing a rectangle
///
/// This class specializes the interpolation of AnimatedValue<Rect> to be
/// appropriate for rectangles.
class AnimatedRect extends AnimatedValue<Rect> {
AnimatedRect(Rect begin, { Rect end, Curve curve })
: super(begin, end: end, curve: curve);
......
......@@ -8,50 +8,62 @@ import 'package:sky/src/animation/animated_value.dart';
import 'package:sky/src/animation/forces.dart';
import 'package:sky/src/animation/timeline.dart';
/// The status of an animation
enum AnimationStatus {
dismissed, // stoped at 0
forward, // animating from 0 => 1
reverse, // animating from 1 => 0
completed, // stopped at 1
/// The animation is stopped at the beginning
dismissed,
/// The animation is running from beginning to end
forward,
/// The animation is running backwards, from end to beginning
reverse,
/// The animation is stopped at the end
completed,
}
// This class manages a "performance" - a collection of values that change
// based on a timeline. For example, a performance may handle an animation
// of a menu opening by sliding and fading in (changing Y value and opacity)
// over .5 seconds. The performance can move forwards (present) or backwards
// (dismiss). A consumer may also take direct control of the timeline by
// manipulating |progress|, or |fling| the timeline causing a physics-based
// simulation to take over the progression.
/// A collection of values that animated based on a timeline
///
/// For example, a performance may handle an animation of a menu opening by
/// sliding and fading in (changing Y value and opacity) over .5 seconds. The
/// performance can move forwards (present) or backwards (dismiss). A consumer
/// may also take direct control of the timeline by manipulating [progress], or
/// [fling] the timeline causing a physics-based simulation to take over the
/// progression.
class AnimationPerformance {
AnimationPerformance({AnimatedVariable variable, this.duration}) :
_variable = variable {
_timeline = new Timeline(_tick);
}
AnimatedVariable _variable;
/// The length of time this performance should last
Duration duration;
/// The variable being updated by this performance
AnimatedVariable get variable => _variable;
void set variable(AnimatedVariable v) { _variable = v; }
void set variable(AnimatedVariable variable) { _variable = variable; }
AnimatedVariable _variable;
// Advances from 0 to 1. On each tick, we'll update our variable's values.
Timeline _timeline;
Timeline get timeline => _timeline;
Direction _direction;
Direction get direction => _direction;
// This controls which curve we use for variables with different curves in
// the forward/reverse directions. Curve direction is only reset when we hit
// 0 or 1, to avoid discontinuities.
/// The direction used to select the current curve
///
/// Curve direction is only reset when we hit the beginning or the end of the
/// timeline to avoid discontinuities in the value of the variable.
Direction _curveDirection;
Direction get curveDirection => _curveDirection;
/// If non-null, animate with this timing instead of a linear timing
AnimationTiming timing;
// If non-null, animate with this force instead of a tween animation.
/// If non-null, animate with this force instead of a zero-to-one timeline.
Force attachedForce;
/// Add a variable to this animation
///
/// If there are no attached variables, this variable becomes the value of
/// [variable]. Otherwise, all the variables are stored in an [AnimatedList].
void addVariable(AnimatedVariable newVariable) {
if (variable == null) {
variable = newVariable;
......@@ -62,70 +74,92 @@ class AnimationPerformance {
}
}
double get progress => timeline.value;
/// The progress of this performance along the timeline
///
/// Note: Setting this value stops the current animation.
double get progress => _timeline.value;
void set progress(double t) {
// TODO(mpcomplete): should this affect |direction|?
stop();
timeline.value = t.clamp(0.0, 1.0);
_timeline.value = t.clamp(0.0, 1.0);
_checkStatusChanged();
}
double get curvedProgress {
return timing != null ? timing.transform(progress, curveDirection) : progress;
double get _curvedProgress {
return timing != null ? timing.transform(progress, _curveDirection) : progress;
}
/// Whether this animation is stopped at the beginning
bool get isDismissed => status == AnimationStatus.dismissed;
/// Whether this animation is stopped at the end
bool get isCompleted => status == AnimationStatus.completed;
bool get isAnimating => timeline.isAnimating;
/// Whether this animation is currently animating in either the forward or reverse direction
bool get isAnimating => _timeline.isAnimating;
/// The current status of this animation
AnimationStatus get status {
if (!isAnimating && progress == 1.0)
return AnimationStatus.completed;
if (!isAnimating && progress == 0.0)
return AnimationStatus.dismissed;
return direction == Direction.forward ?
return _direction == Direction.forward ?
AnimationStatus.forward :
AnimationStatus.reverse;
}
/// Update the given varaible according to the current progress of this performance
void updateVariable(AnimatedVariable variable) {
variable.setProgress(curvedProgress, curveDirection);
variable.setProgress(_curvedProgress, _curveDirection);
}
/// Start running this animation in the given direction
Future play([Direction direction = Direction.forward]) {
_direction = direction;
return resume();
}
/// Start running this animation forwards (towards the end)
Future forward() => play(Direction.forward);
/// Start running this animation in reverse (towards the beginning)
Future reverse() => play(Direction.reverse);
/// Start running this animation in the most recently direction
Future resume() {
if (attachedForce != null) {
return fling(velocity: _direction == Direction.forward ? 1.0 : -1.0,
force: attachedForce);
}
return _animateTo(direction == Direction.forward ? 1.0 : 0.0);
return _animateTo(_direction == Direction.forward ? 1.0 : 0.0);
}
/// Stop running this animation
void stop() {
timeline.stop();
_timeline.stop();
}
// Flings the timeline with an optional force (defaults to a critically
// damped spring) and initial velocity. If velocity is positive, the
// animation will complete, otherwise it will dismiss.
/// Start running this animation according to the given physical parameters
///
/// Flings the timeline with an optional force (defaults to a critically
/// damped spring) and initial velocity. If velocity is positive, the
/// animation will complete, otherwise it will dismiss.
Future fling({double velocity: 1.0, Force force}) {
if (force == null)
force = kDefaultSpringForce;
_direction = velocity < 0.0 ? Direction.reverse : Direction.forward;
return timeline.fling(force.release(progress, velocity));
return _timeline.fling(force.release(progress, velocity));
}
final List<Function> _listeners = new List<Function>();
/// Calls the listener every time the progress of this performance changes
void addListener(Function listener) {
_listeners.add(listener);
}
/// Stop calling the listener every time the progress of this performance changes
void removeListener(Function listener) {
_listeners.remove(listener);
}
......@@ -138,10 +172,12 @@ class AnimationPerformance {
final List<Function> _statusListeners = new List<Function>();
/// Calls listener every time the status of this performance changes
void addStatusListener(Function listener) {
_statusListeners.add(listener);
}
/// Stops calling the listener every time the status of this performance changes
void removeStatusListener(Function listener) {
_statusListeners.remove(listener);
}
......@@ -160,28 +196,28 @@ class AnimationPerformance {
void _updateCurveDirection() {
if (status != _lastStatus) {
if (_lastStatus == AnimationStatus.dismissed || _lastStatus == AnimationStatus.completed)
_curveDirection = direction;
_curveDirection = _direction;
}
}
Future _animateTo(double target) {
Duration remainingDuration = duration * (target - timeline.value).abs();
timeline.stop();
Duration remainingDuration = duration * (target - _timeline.value).abs();
_timeline.stop();
if (remainingDuration == Duration.ZERO)
return new Future.value();
return timeline.animateTo(target, duration: remainingDuration);
return _timeline.animateTo(target, duration: remainingDuration);
}
void _tick(double t) {
_updateCurveDirection();
if (variable != null)
variable.setProgress(curvedProgress, curveDirection);
variable.setProgress(_curvedProgress, _curveDirection);
_notifyListeners();
_checkStatusChanged();
}
}
// Simple helper class for an animation with a single value.
/// An animation performance with an animated variable with a concrete type
class ValueAnimation<T> extends AnimationPerformance {
ValueAnimation({AnimatedValue<T> variable, Duration duration}) :
super(variable: variable, duration: duration);
......
......@@ -11,10 +11,17 @@ double _evaluateCubic(double a, double b, double m) {
const double _kCubicErrorBound = 0.001;
/// A mapping of the unit interval to the unit interval
///
/// A curve must map 0.0 to 0.0 and 1.0 to 1.0.
abstract class Curve {
/// Return the value of the curve at point t
///
/// The value of t must be between 0.0 and 1.0, inclusive.
double transform(double t);
}
/// The idenity map over the unit interval
class Linear implements Curve {
const Linear();
......@@ -23,8 +30,12 @@ class Linear implements Curve {
}
}
/// A curve that is initially 0.0, then linear, then 1.0
class Interval implements Curve {
/// The smallest value for which this interval is 0.0
final double start;
/// The smallest value for which this interval is 1.0
final double end;
Interval(this.start, this.end) {
......@@ -39,6 +50,7 @@ class Interval implements Curve {
}
}
/// A cubic polynomial mapping of the unit interval
class Cubic implements Curve {
final double a;
final double b;
......@@ -79,6 +91,7 @@ double _bounce(double t) {
return 7.5625 * t * t + 0.984375;
}
/// An oscillating curve that grows in magnitude
class BounceInCurve implements Curve {
const BounceInCurve();
......@@ -87,6 +100,7 @@ class BounceInCurve implements Curve {
}
}
/// An oscillating curve that shrink in magnitude
class BounceOutCurve implements Curve {
const BounceOutCurve();
......@@ -95,6 +109,7 @@ class BounceOutCurve implements Curve {
}
}
/// An oscillating curve that first grows and then shrink in magnitude
class BounceInOutCurve implements Curve {
const BounceInOutCurve();
......@@ -106,6 +121,7 @@ class BounceInOutCurve implements Curve {
}
}
/// An oscillating curve that grows in magnitude while overshootings its bounds
class ElasticInCurve implements Curve {
const ElasticInCurve([this.period = 0.4]);
final double period;
......@@ -117,6 +133,7 @@ class ElasticInCurve implements Curve {
}
}
/// An oscillating curve that shrinks in magnitude while overshootings its bounds
class ElasticOutCurve implements Curve {
const ElasticOutCurve([this.period = 0.4]);
final double period;
......@@ -127,6 +144,7 @@ class ElasticOutCurve implements Curve {
}
}
/// An oscillating curve that grows and then shrinks in magnitude while overshootings its bounds
class ElasticInOutCurve implements Curve {
const ElasticInOutCurve([this.period = 0.4]);
final double period;
......@@ -141,14 +159,35 @@ class ElasticInOutCurve implements Curve {
}
}
/// A linear animation curve
const Linear linear = const Linear();
/// A cubic animation cuve that speeds up quickly and ends slowly
const Cubic ease = const Cubic(0.25, 0.1, 0.25, 1.0);
/// A cubic animation cuve that starts slowly and ends quickly
const Cubic easeIn = const Cubic(0.42, 0.0, 1.0, 1.0);
/// A cubic animation cuve that starts quickly and ends slowly
const Cubic easeOut = const Cubic(0.0, 0.0, 0.58, 1.0);
/// A cubic animation cuve that starts slowly, speeds up, and then and ends slowly
const Cubic easeInOut = const Cubic(0.42, 0.0, 0.58, 1.0);
/// An oscillating curve that grows in magnitude
const BounceInCurve bounceIn = const BounceInCurve();
/// An oscillating curve that first grows and then shrink in magnitude
const BounceOutCurve bounceOut = const BounceOutCurve();
/// An oscillating curve that first grows and then shrink in magnitude
const BounceInOutCurve bounceInOut = const BounceInOutCurve();
/// An oscillating curve that grows in magnitude while overshootings its bounds
const ElasticInCurve elasticIn = const ElasticInCurve();
/// An oscillating curve that shrinks in magnitude while overshootings its bounds
const ElasticOutCurve elasticOut = const ElasticOutCurve();
/// An oscillating curve that grows and then shrinks in magnitude while overshootings its bounds
const ElasticInOutCurve elasticInOut = const ElasticInOutCurve();
......@@ -4,26 +4,31 @@
import 'package:newton/newton.dart';
// Base class for creating Simulations for the animation Timeline.
/// A factory for simulations
abstract class Force {
const Force();
Simulation release(double position, double velocity);
}
/// A factory for spring-based physics simulations
class SpringForce extends Force {
const SpringForce(this.spring, { this.left: 0.0, this.right: 1.0 });
/// The description of the spring to be used in the created simulations
final SpringDescription spring;
// Where to put the spring's resting point when releasing left or right,
// respectively.
/// Where to put the spring's resting point when releasing left
final double left;
/// Where to put the spring's resting point when releasing right
final double right;
// We overshoot the target by this distance, but stop the simulation when
// the spring gets within this distance (regardless of how fast it's moving).
// This causes the spring to settle a bit faster than it otherwise would.
/// How pricely to terminate the simulation
///
/// We overshoot the target by this distance, but stop the simulation when
/// the spring gets within this distance (regardless of how fast it's moving).
/// This causes the spring to settle a bit faster than it otherwise would.
static const Tolerance tolerance = const Tolerance(
velocity: double.INFINITY,
distance: 0.01
......@@ -43,4 +48,5 @@ final SpringDescription _kDefaultSpringDesc = new SpringDescription.withDampingR
ratio: 1.0
);
/// A spring force with reasonable default values
final SpringForce kDefaultSpringForce = new SpringForce(_kDefaultSpringDesc);
......@@ -9,24 +9,36 @@ import 'package:newton/newton.dart';
const double _kSecondsPerMillisecond = 1000.0;
const double _kScrollDrag = 0.025;
/// An interface for controlling the behavior of scrollable widgets
abstract class ScrollBehavior {
/// A simulation to run to determine the scroll offset
///
/// Called when the user stops scrolling at a given position with a given
/// instantaneous velocity.
Simulation release(double position, double velocity) => null;
// Returns the new scroll offset.
/// The new scroll offset to use when the user attempts to scroll from the given offset by the given delta
double applyCurve(double scrollOffset, double scrollDelta);
}
/// A scroll behavior for a scrollable widget with linear extent
abstract class ExtentScrollBehavior extends ScrollBehavior {
ExtentScrollBehavior({ double contentExtent: 0.0, double containerExtent: 0.0 })
: _contentExtent = contentExtent, _containerExtent = containerExtent;
double _contentExtent;
/// The linear extent of the content inside the scrollable widget
double get contentExtent => _contentExtent;
double _contentExtent;
double _containerExtent;
/// The linear extent of the exterior of the scrollable widget
double get containerExtent => _containerExtent;
double _containerExtent;
/// Returns the new scrollOffset.
/// Update either content or container extent (or both)
///
/// The scrollOffset parameter is the scroll offset of the widget before the
/// change in extent. Returns the new scroll offset of the widget after the
/// change in extent.
double updateExtents({
double contentExtent,
double containerExtent,
......@@ -39,10 +51,14 @@ abstract class ExtentScrollBehavior extends ScrollBehavior {
return scrollOffset.clamp(minScrollOffset, maxScrollOffset);
}
/// The minimum value the scroll offset can obtain
double get minScrollOffset;
/// The maximum value the scroll offset can obatin
double get maxScrollOffset;
}
/// A scroll behavior that prevents the user from exeeding scroll bounds
class BoundedBehavior extends ExtentScrollBehavior {
BoundedBehavior({ double contentExtent: 0.0, double containerExtent: 0.0 })
: super(contentExtent: contentExtent, containerExtent: containerExtent);
......@@ -55,6 +71,7 @@ class BoundedBehavior extends ExtentScrollBehavior {
}
}
/// A scroll behavior that does not prevent the user from exeeding scroll bounds
class UnboundedBehavior extends ExtentScrollBehavior {
UnboundedBehavior({ double contentExtent: 0.0, double containerExtent: 0.0 })
: super(contentExtent: contentExtent, containerExtent: containerExtent);
......@@ -74,19 +91,20 @@ class UnboundedBehavior extends ExtentScrollBehavior {
}
}
Simulation createDefaultScrollSimulation(double position, double velocity, double minScrollOffset, double maxScrollOffset) {
Simulation _createDefaultScrollSimulation(double position, double velocity, double minScrollOffset, double maxScrollOffset) {
double velocityPerSecond = velocity * _kSecondsPerMillisecond;
SpringDescription spring = new SpringDescription.withDampingRatio(
mass: 1.0, springConstant: 170.0, ratio: 1.1);
return new ScrollSimulation(position, velocityPerSecond, minScrollOffset, maxScrollOffset, spring, _kScrollDrag);
}
/// A scroll behavior that lets the user scroll beyond the scroll bounds with some resistance
class OverscrollBehavior extends BoundedBehavior {
OverscrollBehavior({ double contentExtent: 0.0, double containerExtent: 0.0 })
: super(contentExtent: contentExtent, containerExtent: containerExtent);
Simulation release(double position, double velocity) {
return createDefaultScrollSimulation(position, velocity, minScrollOffset, maxScrollOffset);
return _createDefaultScrollSimulation(position, velocity, minScrollOffset, maxScrollOffset);
}
double applyCurve(double scrollOffset, double scrollDelta) {
......@@ -106,6 +124,7 @@ class OverscrollBehavior extends BoundedBehavior {
}
}
/// A scroll behavior that lets the user scroll beyond the scroll bounds only when the bounds are disjoint
class OverscrollWhenScrollableBehavior extends OverscrollBehavior {
bool get isScrollable => contentExtent > containerExtent;
......
......@@ -8,10 +8,14 @@ import 'package:newton/newton.dart';
import 'package:sky/src/animation/animated_simulation.dart';
// Simple simulation that linearly varies from |begin| to |end| over |duration|.
/// A simulation that linearly varies from [begin] to [end] over [duration]
class TweenSimulation extends Simulation {
final double _durationInSeconds;
/// The initial value of the simulation
final double begin;
/// The terminal value of the simulation
final double end;
TweenSimulation(Duration duration, this.begin, this.end) :
......@@ -32,14 +36,15 @@ class TweenSimulation extends Simulation {
bool isDone(double timeInSeconds) => timeInSeconds > _durationInSeconds;
}
/// A timeline for an animation
class Timeline {
Timeline(Function onTick) : _onTick = onTick {
_animation = new AnimatedSimulation(_tick);
Timeline(Function onTick) {
_animation = new AnimatedSimulation(onTick);
}
final Function _onTick;
AnimatedSimulation _animation;
/// The current value of the timeline
double get value => _animation.value.clamp(0.0, 1.0);
void set value(double newValue) {
assert(newValue != null && newValue >= 0.0 && newValue <= 1.0);
......@@ -47,6 +52,7 @@ class Timeline {
_animation.value = newValue;
}
/// Whether the timeline is currently animating
bool get isAnimating => _animation.isAnimating;
Future _start({
......@@ -59,22 +65,23 @@ class Timeline {
return _animation.start(new TweenSimulation(duration, begin, end));
}
/// Animate value of the timeline to the given target over the given duration
///
/// Returns a future that resolves when the timeline stops animating,
/// typically when the timeline arives at the target value.
Future animateTo(double target, { Duration duration }) {
assert(duration > Duration.ZERO);
return _start(duration: duration, begin: value, end: target);
}
/// Stop animating the timeline
void stop() {
_animation.stop();
}
// Give |simulation| control over the timeline.
// Gives the given simulation control over the timeline
Future fling(Simulation simulation) {
stop();
return _animation.start(simulation);
}
void _tick(double newValue) {
_onTick(value);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment