animation_controller.dart 8.67 KB
Newer Older
1 2 3 4 5
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
6
import 'dart:ui' show VoidCallback, lerpDouble;
7 8 9 10

import 'package:newton/newton.dart';

import 'animation.dart';
11
import 'curves.dart';
12 13
import 'forces.dart';
import 'listener_helpers.dart';
14
import 'ticker.dart';
15

16 17 18 19 20 21
/// A controller for an animation.
///
/// An animation controller can drive an animation forward or backward and can
/// set the animation to a particular value. The controller also defines the
/// bounds of the animation and can drive an animation using a physics
/// simulation.
22
class AnimationController extends Animation<double>
23 24 25 26 27 28 29 30 31
  with EagerListenerMixin, LocalListenersMixin, LocalAnimationStatusListenersMixin {

  /// Creates an animation controller.
  ///
  /// * value is the initial value of the animation.
  /// * duration is the length of time this animation should last.
  /// * debugLabel is a string to help identify this animation during debugging (used by toString).
  /// * lowerBound is the smallest value this animation can obtain and the value at which this animation is deemed to be dismissed.
  /// * upperBound is the largest value this animation can obtain and the value at which this animation is deemed to be completed.
32 33 34 35 36 37 38
  AnimationController({
    double value,
    this.duration,
    this.debugLabel,
    this.lowerBound: 0.0,
    this.upperBound: 1.0
  }) {
39
    assert(upperBound >= lowerBound);
40 41
    _value = (value ?? lowerBound).clamp(lowerBound, upperBound);
    _ticker = new Ticker(_tick);
42 43
  }

44 45 46 47 48 49 50 51 52
  /// Creates an animation controller with no upper or lower bound for its value.
  ///
  /// * value is the initial value of the animation.
  /// * duration is the length of time this animation should last.
  /// * debugLabel is a string to help identify this animation during debugging (used by toString).
  ///
  /// This constructor is most useful for animations that will be driven using a
  /// physics simulation, especially when the physics simulation as no
  /// pre-determined bounds.
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
  AnimationController.unbounded({
    double value: 0.0,
    this.duration,
    this.debugLabel
  }) : lowerBound = double.NEGATIVE_INFINITY,
       upperBound = double.INFINITY,
       _value = value {
    assert(value != null);
    _ticker = new Ticker(_tick);
  }

  /// The value at which this animation is deemed to be dismissed.
  final double lowerBound;

  /// The value at which this animation is deemed to be completed.
  final double upperBound;

70 71 72 73
  /// A label that is used in the [toString] output. Intended to aid with
  /// identifying animation controller instances in debug output.
  final String debugLabel;

74
  /// Returns an [Animated<double>] for this animation controller,
75 76 77 78 79 80 81 82 83 84
  /// so that a pointer to this object can be passed around without
  /// allowing users of that pointer to mutate the AnimationController state.
  Animation<double> get view => this;

  /// The length of time this animation should last.
  Duration duration;

  AnimationDirection get direction => _direction;
  AnimationDirection _direction = AnimationDirection.forward;

85 86 87
  Ticker _ticker;
  Simulation _simulation;

88
  /// The current value of the animation.
89
  ///
90
  /// Setting this value stops the current animation.
91 92 93 94
  double get value => _value.clamp(lowerBound, upperBound);
  double _value;
  void set value(double newValue) {
    assert(newValue != null);
95
    stop();
96 97
    _value = newValue.clamp(lowerBound, upperBound);
    notifyListeners();
98 99 100 101
    _checkStatusChanged();
  }

  /// Whether this animation is currently animating in either the forward or reverse direction.
102
  bool get isAnimating => _ticker.isTicking;
103 104

  AnimationStatus get status {
105
    if (!isAnimating && value == upperBound)
106
      return AnimationStatus.completed;
107
    if (!isAnimating && value == lowerBound)
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127
      return AnimationStatus.dismissed;
    return _direction == AnimationDirection.forward ?
        AnimationStatus.forward :
        AnimationStatus.reverse;
  }

  /// Starts running this animation forwards (towards the end).
  Future forward() => play(AnimationDirection.forward);

  /// Starts running this animation in reverse (towards the beginning).
  Future reverse() => play(AnimationDirection.reverse);

  /// Starts running this animation in the given direction.
  Future play(AnimationDirection direction) {
    _direction = direction;
    return resume();
  }

  /// Resumes this animation in the most recent direction.
  Future resume() {
128
    return animateTo(_direction == AnimationDirection.forward ? upperBound : lowerBound);
129 130 131 132
  }

  /// Stops running this animation.
  void stop() {
133 134
    _simulation = null;
    _ticker.stop();
135 136
  }

137
  /// Stops running this animation.
Adam Barth's avatar
Adam Barth committed
138 139 140 141
  void dispose() {
    stop();
  }

142 143 144
  /// 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.
145
  Future fling({ double velocity: 1.0, Force force }) {
146 147
    force ??= kDefaultSpringForce;
    _direction = velocity < 0.0 ? AnimationDirection.reverse : AnimationDirection.forward;
148
    return animateWith(force.release(value, velocity));
149 150 151 152 153 154
  }

  /// Starts running this animation in the forward direction, and
  /// restarts the animation when it completes.
  Future repeat({ double min: 0.0, double max: 1.0, Duration period }) {
    period ??= duration;
155 156 157 158 159 160 161
    return animateWith(new _RepeatingSimulation(min, max, period));
  }

  /// Drives the animation according to the given simulation.
  Future animateWith(Simulation simulation) {
    stop();
    return _startSimulation(simulation);
162 163 164 165 166 167 168 169 170 171
  }

  AnimationStatus _lastStatus = AnimationStatus.dismissed;
  void _checkStatusChanged() {
    AnimationStatus currentStatus = status;
    if (currentStatus != _lastStatus)
      notifyStatusListeners(status);
    _lastStatus = currentStatus;
  }

172
  /// Drives the animation from its current value to target.
173
  Future animateTo(double target, { Duration duration, Curve curve: Curves.linear }) {
174 175 176 177 178 179 180 181
    Duration simulationDuration = duration;
    if (simulationDuration == null) {
      double range = upperBound - lowerBound;
      if (range.isFinite) {
        double remainingFraction = (target - _value).abs() / range;
        simulationDuration = this.duration * remainingFraction;
      }
    }
182
    stop();
183 184
    if (simulationDuration == Duration.ZERO) {
      assert(value == target);
185
      return new Future.value();
186 187
    }
    assert(simulationDuration > Duration.ZERO);
188
    assert(!isAnimating);
189
    return _startSimulation(new _TweenSimulation(_value, target, simulationDuration, curve));
190 191 192 193 194 195 196 197
  }

  Future _startSimulation(Simulation simulation) {
    assert(simulation != null);
    assert(!isAnimating);
    _simulation = simulation;
    _value = simulation.x(0.0);
    return _ticker.start();
198 199
  }

200 201 202 203 204
  void _tick(Duration elapsed) {
    double elapsedInSeconds = elapsed.inMicroseconds.toDouble() / Duration.MICROSECONDS_PER_SECOND;
    _value = _simulation.x(elapsedInSeconds);
    if (_simulation.isDone(elapsedInSeconds))
      stop();
205 206 207 208 209
    notifyListeners();
    _checkStatusChanged();
  }

  String toStringDetails() {
210
    String paused = isAnimating ? '' : '; paused';
211 212 213 214 215 216
    String label = debugLabel == null ? '' : '; for $debugLabel';
    String more = '${super.toStringDetails()} ${value.toStringAsFixed(3)}';
    return '$more$paused$label';
  }
}

217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245
class _TweenSimulation extends Simulation {
  _TweenSimulation(this._begin, this._end, Duration duration, this._curve)
    : _durationInSeconds = duration.inMicroseconds / Duration.MICROSECONDS_PER_SECOND {
    assert(_durationInSeconds > 0.0);
    assert(_begin != null);
    assert(_end != null);
  }

  final double _durationInSeconds;
  final double _begin;
  final double _end;
  final Curve _curve;

  double x(double timeInSeconds) {
    assert(timeInSeconds >= 0.0);
    double t = (timeInSeconds / _durationInSeconds).clamp(0.0, 1.0);
    if (t == 0.0)
      return _begin;
    else if (t == 1.0)
      return _end;
    else
      return _begin + (_end - _begin) * _curve.transform(t);
  }

  double dx(double timeInSeconds) => 1.0;

  bool isDone(double timeInSeconds) => timeInSeconds > _durationInSeconds;
}

246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266
class _RepeatingSimulation extends Simulation {
  _RepeatingSimulation(this.min, this.max, Duration period)
    : _periodInSeconds = period.inMicroseconds / Duration.MICROSECONDS_PER_SECOND {
    assert(_periodInSeconds > 0.0);
  }

  final double min;
  final double max;

  final double _periodInSeconds;

  double x(double timeInSeconds) {
    assert(timeInSeconds >= 0.0);
    final double t = (timeInSeconds / _periodInSeconds) % 1.0;
    return lerpDouble(min, max, t);
  }

  double dx(double timeInSeconds) => 1.0;

  bool isDone(double timeInSeconds) => false;
}