animation_controller.dart 14.7 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' as ui show lerpDouble;
7

8
import 'package:flutter/foundation.dart';
9
import 'package:flutter/physics.dart';
10 11
import 'package:flutter/scheduler.dart';
import 'package:meta/meta.dart';
12 13

import 'animation.dart';
14
import 'curves.dart';
15 16 17
import 'forces.dart';
import 'listener_helpers.dart';

Adam Barth's avatar
Adam Barth committed
18 19 20 21 22 23
/// The direction in which an animation is running.
enum _AnimationDirection {
  /// The animation is running from beginning to end.
  forward,

  /// The animation is running backwards, from end to beginning.
24
  reverse,
Adam Barth's avatar
Adam Barth committed
25 26
}

27 28
/// A controller for an animation.
///
29 30 31 32 33 34 35 36 37 38 39
/// This class lets you perform tasks such as:
///
/// * Play an animation [forward] or in [reverse], or [stop] an animation.
/// * Set the animation to a specific [value].
/// * Define the [upperBound] and [lowerBound] values of an animation.
/// * Create a [fling] animation effect using a physics simulation.
///
/// By default, an [AnimationController] linearly produces values that range from 0.0 to 1.0, during
/// a given duration. The animation controller generates a new value whenever the device running
/// your app is ready to display a new frame (typically, this rate is around 60 values per second).
///
40 41 42 43 44 45 46
/// An AnimationController needs a [TickerProvider], which is configured using the `vsync` argument
/// on the constructor. If you are creating an AnimationController from a [State], then you can use
/// the [TickerProviderStateMixin] and [SingleTickerProviderStateMixin] classes to obtain a suitable
/// [TickerProvider]. The widget test framework [WidgetTester] object can be used as a ticker provider
/// in the context of tests. In other contexts, you will have to either pass a [TickerProvider] from
/// a higher level (e.g. indirectly from a [State] that mixes in [TickerProviderStateMixin]), or
/// create a custom [TickerProvider] subclass.
47
class AnimationController extends Animation<double>
48
  with AnimationEagerListenerMixin, AnimationLocalListenersMixin, AnimationLocalStatusListenersMixin {
49 50 51

  /// Creates an animation controller.
  ///
52 53 54 55 56 57
  /// * [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.
  /// * `vsync` is the [TickerProvider] for the current context. It can be changed by calling [resync].
58 59 60 61 62
  AnimationController({
    double value,
    this.duration,
    this.debugLabel,
    this.lowerBound: 0.0,
63 64
    this.upperBound: 1.0,
    @required TickerProvider vsync,
65
  }) {
66
    assert(upperBound >= lowerBound);
67
    assert(vsync != null);
68
    _direction = _AnimationDirection.forward;
69
    _ticker = vsync.createTicker(_tick);
70
    _internalSetValue(value ?? lowerBound);
71 72
  }

73 74
  /// Creates an animation controller with no upper or lower bound for its value.
  ///
75 76 77 78
  /// * [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]).
  /// * `vsync` is the [TickerProvider] for the current context. It can be changed by calling [resync].
79 80
  ///
  /// This constructor is most useful for animations that will be driven using a
81
  /// physics simulation, especially when the physics simulation has no
82
  /// pre-determined bounds.
83 84 85
  AnimationController.unbounded({
    double value: 0.0,
    this.duration,
86 87
    this.debugLabel,
    @required TickerProvider vsync,
88
  }) : lowerBound = double.NEGATIVE_INFINITY,
89
       upperBound = double.INFINITY {
90
    assert(value != null);
91
    assert(vsync != null);
92
    _direction = _AnimationDirection.forward;
93
    _ticker = vsync.createTicker(_tick);
94
    _internalSetValue(value);
95 96 97 98 99 100 101 102
  }

  /// 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;

103 104 105 106
  /// A label that is used in the [toString] output. Intended to aid with
  /// identifying animation controller instances in debug output.
  final String debugLabel;

107
  /// Returns an [Animated<double>] for this animation controller,
108 109 110 111 112 113 114
  /// 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;

115
  Ticker _ticker;
116 117 118 119 120 121 122 123

  /// Recreates the [Ticker] with the new [TickerProvider].
  void resync(TickerProvider vsync) {
    Ticker oldTicker = _ticker;
    _ticker = vsync.createTicker(_tick);
    _ticker.absorbTicker(oldTicker);
  }

124 125
  Simulation _simulation;

126
  /// The current value of the animation.
127
  ///
128 129 130 131 132 133
  /// Setting this value notifies all the listeners that the value
  /// changed.
  ///
  /// Setting this value also stops the controller if it is currently
  /// running; if this happens, it also notifies all the status
  /// listeners.
134
  @override
135
  double get value => _value;
136
  double _value;
137 138 139 140 141 142 143
  /// Stops the animation controller and sets the current value of the
  /// animation.
  ///
  /// The new value is clamped to the range set by [lowerBound] and [upperBound].
  ///
  /// Value listeners are notified even if this does not change the value.
  /// Status listeners are notified if the animation was previously playing.
144
  set value(double newValue) {
145
    assert(newValue != null);
146
    stop();
147
    _internalSetValue(newValue);
148
    notifyListeners();
149 150 151
    _checkStatusChanged();
  }

152 153 154 155 156 157 158 159 160 161 162 163
  void _internalSetValue(double newValue) {
    _value = newValue.clamp(lowerBound, upperBound);
    if (_value == lowerBound) {
      _status = AnimationStatus.dismissed;
    } else if (_value == upperBound) {
      _status = AnimationStatus.completed;
    } else
      _status = (_direction == _AnimationDirection.forward) ?
        AnimationStatus.forward :
        AnimationStatus.reverse;
  }

164 165
  /// The amount of time that has passed between the time the animation started and the most recent tick of the animation.
  ///
166
  /// If the controller is not animating, the last elapsed duration is null.
167 168 169
  Duration get lastElapsedDuration => _lastElapsedDuration;
  Duration _lastElapsedDuration;

170
  /// Whether this animation is currently animating in either the forward or reverse direction.
171 172 173 174 175 176
  ///
  /// This is separate from whether it is actively ticking. An animation
  /// controller's ticker might get muted, in which case the animation
  /// controller's callbacks will no longer fire even though time is continuing
  /// to pass. See [Ticker.muted] and [TickerMode].
  bool get isAnimating => _ticker.isActive;
177

Adam Barth's avatar
Adam Barth committed
178 179
  _AnimationDirection _direction;

180
  @override
181 182
  AnimationStatus get status => _status;
  AnimationStatus _status;
183 184

  /// Starts running this animation forwards (towards the end).
185 186
  ///
  /// Returns a [Future] that completes when the animation is complete.
187
  Future<Null> forward({ double from }) {
188 189 190 191 192 193 194 195 196 197
    assert(() {
      if (duration == null) {
        throw new FlutterError(
          'AnimationController.forward() called with no default Duration.\n'
          'The "duration" property should be set, either in the constructor or later, before '
          'calling the forward() function.'
        );
      }
      return true;
    });
198
    _direction = _AnimationDirection.forward;
199 200
    if (from != null)
      value = from;
Adam Barth's avatar
Adam Barth committed
201
    return animateTo(upperBound);
202 203
  }

Adam Barth's avatar
Adam Barth committed
204
  /// Starts running this animation in reverse (towards the beginning).
205 206
  ///
  /// Returns a [Future] that completes when the animation is complete.
207
  Future<Null> reverse({ double from }) {
208 209 210 211 212 213 214 215 216 217
    assert(() {
      if (duration == null) {
        throw new FlutterError(
          'AnimationController.reverse() called with no default Duration.\n'
          'The "duration" property should be set, either in the constructor or later, before '
          'calling the reverse() function.'
        );
      }
      return true;
    });
218
    _direction = _AnimationDirection.reverse;
219 220
    if (from != null)
      value = from;
Adam Barth's avatar
Adam Barth committed
221
    return animateTo(lowerBound);
222 223
  }

224
  /// Drives the animation from its current value to target.
225 226
  ///
  /// Returns a [Future] that completes when the animation is complete.
227
  Future<Null> animateTo(double target, { Duration duration, Curve curve: Curves.linear }) {
228 229
    Duration simulationDuration = duration;
    if (simulationDuration == null) {
230 231 232 233 234 235 236 237 238 239 240
      assert(() {
        if (this.duration == null) {
          throw new FlutterError(
            'AnimationController.animateTo() called with no explicit Duration and no default Duration.\n'
            'Either the "duration" argument to the animateTo() method should be provided, or the '
            '"duration" property should be set, either in the constructor or later, before '
            'calling the animateTo() function.'
          );
        }
        return true;
      });
241
      double range = upperBound - lowerBound;
242 243
      double remainingFraction = range.isFinite ? (target - _value).abs() / range : 1.0;
      simulationDuration = this.duration * remainingFraction;
244
    }
245
    stop();
246 247
    if (simulationDuration == Duration.ZERO) {
      assert(value == target);
248 249 250
      _status = (_direction == _AnimationDirection.forward) ?
        AnimationStatus.completed :
        AnimationStatus.dismissed;
251
      _checkStatusChanged();
252
      return new Future<Null>.value();
253 254
    }
    assert(simulationDuration > Duration.ZERO);
255
    assert(!isAnimating);
256
    return _startSimulation(new _InterpolationSimulation(_value, target, simulationDuration, curve));
257 258
  }

259 260
  /// Starts running this animation in the forward direction, and
  /// restarts the animation when it completes.
261 262
  ///
  /// Defaults to repeating between the lower and upper bounds.
263
  Future<Null> repeat({ double min, double max, Duration period }) {
264 265
    min ??= lowerBound;
    max ??= upperBound;
266
    period ??= duration;
267 268 269 270 271 272 273 274 275 276 277
    assert(() {
      if (duration == null) {
        throw new FlutterError(
          'AnimationController.repeat() called with no explicit Duration and default Duration.\n'
          'Either the "duration" argument to the repeat() method should be provided, or the '
          '"duration" property should be set, either in the constructor or later, before '
          'calling the repeat() function.'
        );
      }
      return true;
    });
278 279 280 281
    return animateWith(new _RepeatingSimulation(min, max, period));
  }

  /// Flings the timeline with an optional force (defaults to a critically
282 283
  /// damped spring within [lowerBound] and [upperBound]) and initial velocity.
  /// If velocity is positive, the animation will complete, otherwise it will dismiss.
284
  Future<Null> fling({ double velocity: 1.0, Force force }) {
285
    force ??= kDefaultSpringForce.copyWith(left: lowerBound, right: upperBound);
Adam Barth's avatar
Adam Barth committed
286
    _direction = velocity < 0.0 ? _AnimationDirection.reverse : _AnimationDirection.forward;
287 288 289 290
    return animateWith(force.release(value, velocity));
  }

  /// Drives the animation according to the given simulation.
291
  Future<Null> animateWith(Simulation simulation) {
292 293 294 295
    stop();
    return _startSimulation(simulation);
  }

296
  Future<Null> _startSimulation(Simulation simulation) {
297 298 299
    assert(simulation != null);
    assert(!isAnimating);
    _simulation = simulation;
300
    _lastElapsedDuration = Duration.ZERO;
301
    _value = simulation.x(0.0).clamp(lowerBound, upperBound);
302
    Future<Null> result = _ticker.start();
303 304 305
    _status = (_direction == _AnimationDirection.forward) ?
      AnimationStatus.forward :
      AnimationStatus.reverse;
306 307
    _checkStatusChanged();
    return result;
308 309
  }

310
  /// Stops running this animation.
311 312 313
  ///
  /// This does not trigger any notifications. The animation stops in its
  /// current state.
314 315
  void stop() {
    _simulation = null;
316
    _lastElapsedDuration = null;
317 318 319
    _ticker.stop();
  }

320 321
  /// Release the resources used by this object. The object is no longer usable
  /// after this method is called.
322
  @override
323
  void dispose() {
324 325
    _ticker.dispose();
    super.dispose();
326 327
  }

328
  AnimationStatus _lastReportedStatus = AnimationStatus.dismissed;
329 330
  void _checkStatusChanged() {
    AnimationStatus newStatus = status;
331 332
    if (_lastReportedStatus != newStatus) {
      _lastReportedStatus = newStatus;
333
      notifyStatusListeners(newStatus);
334
    }
335 336
  }

337
  void _tick(Duration elapsed) {
338
    _lastElapsedDuration = elapsed;
339
    double elapsedInSeconds = elapsed.inMicroseconds.toDouble() / Duration.MICROSECONDS_PER_SECOND;
340
    _value = _simulation.x(elapsedInSeconds).clamp(lowerBound, upperBound);
341 342 343 344
    if (_simulation.isDone(elapsedInSeconds)) {
      _status = (_direction == _AnimationDirection.forward) ?
        AnimationStatus.completed :
        AnimationStatus.dismissed;
345
      stop();
346
    }
347 348 349 350
    notifyListeners();
    _checkStatusChanged();
  }

351
  @override
352
  String toStringDetails() {
353
    String paused = isAnimating ? '' : '; paused';
354
    String silenced = _ticker.muted ? '; silenced' : '';
355 356
    String label = debugLabel == null ? '' : '; for $debugLabel';
    String more = '${super.toStringDetails()} ${value.toStringAsFixed(3)}';
357
    return '$more$paused$silenced$label';
358 359 360
  }
}

361 362
class _InterpolationSimulation extends Simulation {
  _InterpolationSimulation(this._begin, this._end, Duration duration, this._curve)
363 364 365 366 367 368 369 370 371 372 373
    : _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;

374
  @override
375 376 377 378 379 380 381 382 383 384 385
  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);
  }

386
  @override
387 388
  double dx(double timeInSeconds) => 1.0;

389
  @override
390 391 392
  bool isDone(double timeInSeconds) => timeInSeconds > _durationInSeconds;
}

393 394 395 396 397 398 399 400 401 402 403
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;

404
  @override
405 406 407
  double x(double timeInSeconds) {
    assert(timeInSeconds >= 0.0);
    final double t = (timeInSeconds / _periodInSeconds) % 1.0;
408
    return ui.lerpDouble(min, max, t);
409 410
  }

411
  @override
412 413
  double dx(double timeInSeconds) => 1.0;

414
  @override
415 416
  bool isDone(double timeInSeconds) => false;
}