implicit_animations.dart 19.9 KB
Newer Older
Adam Barth's avatar
Adam Barth committed
1 2 3 4
// Copyright 2015 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.

5 6 7
import 'package:meta/meta.dart';
import 'package:vector_math/vector_math_64.dart';

8
import 'basic.dart';
9
import 'container.dart';
10
import 'framework.dart';
11
import 'text.dart';
12
import 'ticker_provider.dart';
Adam Barth's avatar
Adam Barth committed
13

14
/// An interpolation between two [BoxConstraint]s.
15
class BoxConstraintsTween extends Tween<BoxConstraints> {
16 17 18
  /// Creates a box constraints tween.
  ///
  /// The [begin] and [end] arguments must not be null.
19
  BoxConstraintsTween({ BoxConstraints begin, BoxConstraints end }) : super(begin: begin, end: end);
Adam Barth's avatar
Adam Barth committed
20

21
  @override
Adam Barth's avatar
Adam Barth committed
22 23 24
  BoxConstraints lerp(double t) => BoxConstraints.lerp(begin, end, t);
}

25
/// An interpolation between two [Decoration]s.
26
class DecorationTween extends Tween<Decoration> {
27 28 29
  /// Creates a decoration tween.
  ///
  /// The [begin] and [end] arguments must not be null.
30
  DecorationTween({ Decoration begin, Decoration end }) : super(begin: begin, end: end);
Adam Barth's avatar
Adam Barth committed
31

32
  @override
33
  Decoration lerp(double t) => Decoration.lerp(begin, end, t);
Adam Barth's avatar
Adam Barth committed
34 35
}

36 37
/// An interpolation between two [EdgeInsets]s.
class EdgeInsetsTween extends Tween<EdgeInsets> {
38 39 40
  /// Creates an edge insets tween.
  ///
  /// The [begin] and [end] arguments must not be null.
41
  EdgeInsetsTween({ EdgeInsets begin, EdgeInsets end }) : super(begin: begin, end: end);
Adam Barth's avatar
Adam Barth committed
42

43
  @override
44
  EdgeInsets lerp(double t) => EdgeInsets.lerp(begin, end, t);
Adam Barth's avatar
Adam Barth committed
45 46
}

47
/// An interpolation between two [Matrix4]s.
48 49
///
/// Currently this class works only for translations.
50
class Matrix4Tween extends Tween<Matrix4> {
51 52 53
  /// Creates a [Matrix4] tween.
  ///
  /// The [begin] and [end] arguments must not be null.
54
  Matrix4Tween({ Matrix4 begin, Matrix4 end }) : super(begin: begin, end: end);
Adam Barth's avatar
Adam Barth committed
55

56
  @override
Adam Barth's avatar
Adam Barth committed
57
  Matrix4 lerp(double t) {
58 59
    // TODO(abarth): We should use [Matrix4.decompose] and animate the
    // decomposed parameters instead of just animating the translation.
Adam Barth's avatar
Adam Barth committed
60 61 62 63 64 65 66
    Vector3 beginT = begin.getTranslation();
    Vector3 endT = end.getTranslation();
    Vector3 lerpT = beginT*(1.0-t) + endT*t;
    return new Matrix4.identity()..translate(lerpT);
  }
}

67 68 69 70
/// An interpolation between two [TextStyle]s.
///
/// This will not work well if the styles don't set the same fields.
class TextStyleTween extends Tween<TextStyle> {
71 72 73
  /// Creates a text style tween.
  ///
  /// The [begin] and [end] arguments must not be null.
74 75 76 77 78 79
  TextStyleTween({ TextStyle begin, TextStyle end }) : super(begin: begin, end: end);

  @override
  TextStyle lerp(double t) => TextStyle.lerp(begin, end, t);
}

80
/// An abstract widget for building widgets that gradually change their
81
/// values over a period of time.
82
abstract class ImplicitlyAnimatedWidget extends StatefulWidget {
83 84 85
  /// Initializes fields for subclasses.
  ///
  /// The [curve] and [duration] arguments must not be null.
86
  ImplicitlyAnimatedWidget({
Adam Barth's avatar
Adam Barth committed
87
    Key key,
88
    this.curve: Curves.linear,
89
    @required this.duration
Adam Barth's avatar
Adam Barth committed
90 91
  }) : super(key: key) {
    assert(curve != null);
Ian Hickson's avatar
Ian Hickson committed
92
    assert(duration != null);
Adam Barth's avatar
Adam Barth committed
93 94
  }

95
  /// The curve to apply when animating the parameters of this container.
Adam Barth's avatar
Adam Barth committed
96
  final Curve curve;
97 98

  /// The duration over which to animate the parameters of this container.
Adam Barth's avatar
Adam Barth committed
99 100
  final Duration duration;

101
  @override
102
  AnimatedWidgetBaseState<ImplicitlyAnimatedWidget> createState();
103

104
  @override
105 106 107 108
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('duration: ${duration.inMilliseconds}ms');
  }
Adam Barth's avatar
Adam Barth committed
109 110
}

111
/// Used by [AnimatedWidgetBaseState].
112
typedef Tween<T> TweenConstructor<T>(T targetValue);
113 114

/// Used by [AnimatedWidgetBaseState].
115
typedef Tween<T> TweenVisitor<T>(Tween<T> tween, T targetValue, TweenConstructor<T> constructor);
Adam Barth's avatar
Adam Barth committed
116

117
/// A base class for widgets with implicit animations.
118
abstract class AnimatedWidgetBaseState<T extends ImplicitlyAnimatedWidget> extends State<T> with SingleTickerProviderStateMixin {
119 120
  AnimationController _controller;

121
  /// The animation driving this widget's implicit animations.
122 123
  Animation<double> get animation => _animation;
  Animation<double> _animation;
Adam Barth's avatar
Adam Barth committed
124

125
  @override
Adam Barth's avatar
Adam Barth committed
126 127
  void initState() {
    super.initState();
128
    _controller = new AnimationController(
129
      duration: config.duration,
130 131
      debugLabel: '${config.toStringShort()}',
      vsync: this,
132
    )..addListener(_handleAnimationChanged);
133
    _updateCurve();
134
    _constructTweens();
Adam Barth's avatar
Adam Barth committed
135 136
  }

137
  @override
138
  void didUpdateConfig(T oldConfig) {
139 140
    if (config.curve != oldConfig.curve)
      _updateCurve();
141 142
    _controller.duration = config.duration;
    if (_constructTweens()) {
143
      forEachTween((Tween<dynamic> tween, dynamic targetValue, TweenConstructor<dynamic> constructor) {
144 145
        _updateTween(tween, targetValue);
        return tween;
146
      });
147 148 149
      _controller
        ..value = 0.0
        ..forward();
Adam Barth's avatar
Adam Barth committed
150 151 152
    }
  }

153 154
  void _updateCurve() {
    if (config.curve != null)
155
      _animation = new CurvedAnimation(parent: _controller, curve: config.curve);
156
    else
157
      _animation = _controller;
158 159
  }

160
  @override
Adam Barth's avatar
Adam Barth committed
161
  void dispose() {
162
    _controller.dispose();
Adam Barth's avatar
Adam Barth committed
163 164 165
    super.dispose();
  }

166 167
  void _handleAnimationChanged() {
    setState(() { });
Adam Barth's avatar
Adam Barth committed
168 169
  }

170
  bool _shouldAnimateTween(Tween<dynamic> tween, dynamic targetValue) {
171
    return targetValue != (tween.end ?? tween.begin);
172 173
  }

174
  void _updateTween(Tween<dynamic> tween, dynamic targetValue) {
175 176 177 178 179
    if (tween == null)
      return;
    tween
      ..begin = tween.evaluate(_animation)
      ..end = targetValue;
Adam Barth's avatar
Adam Barth committed
180 181
  }

182 183
  bool _constructTweens() {
    bool shouldStartAnimation = false;
184
    forEachTween((Tween<dynamic> tween, dynamic targetValue, TweenConstructor<dynamic> constructor) {
185
      if (targetValue != null) {
186 187 188
        tween ??= constructor(targetValue);
        if (_shouldAnimateTween(tween, targetValue))
          shouldStartAnimation = true;
189
      } else {
190
        tween = null;
191
      }
192
      return tween;
193
    });
194
    return shouldStartAnimation;
195
  }
Adam Barth's avatar
Adam Barth committed
196

197
  /// Subclasses must implement this function by running through the following
198
  /// steps for each animatable facet in the class:
199 200
  ///
  /// 1. Call the visitor callback with three arguments, the first argument
201 202
  /// being the current value of the Tween<T> object that represents the
  /// tween (initially null), the second argument, of type T, being the value
203
  /// on the Widget (config) that represents the current target value of the
204
  /// tween, and the third being a callback that takes a value T (which will
205
  /// be the second argument to the visitor callback), and that returns an
206
  /// Tween<T> object for the tween, configured with the given value
207 208 209
  /// as the begin value.
  ///
  /// 2. Take the value returned from the callback, and store it. This is the
210
  /// value to use as the current value the next time that the forEachTween()
211
  /// method is called.
212
  void forEachTween(TweenVisitor<dynamic> visitor);
213
}
Adam Barth's avatar
Adam Barth committed
214

215 216 217 218
/// A container that gradually changes its values over a period of time.
///
/// This class is useful for generating simple implicit transitions between
/// different parameters to [Container]. For more complex animations, you'll
219 220
/// likely want to use a subclass of [Transition] or use an
/// [AnimationController] yourself.
221 222
///
/// Properties that are null are not animated.
223
class AnimatedContainer extends ImplicitlyAnimatedWidget {
224 225 226
  /// Creates a container that animates its parameters implicitly.
  ///
  /// The [curve] and [duration] arguments must not be null.
227 228 229 230 231 232 233 234 235 236 237 238
  AnimatedContainer({
    Key key,
    this.child,
    this.constraints,
    this.decoration,
    this.foregroundDecoration,
    this.margin,
    this.padding,
    this.transform,
    this.width,
    this.height,
    Curve curve: Curves.linear,
239
    @required Duration duration
240
  }) : super(key: key, curve: curve, duration: duration) {
241 242
    assert(decoration == null || decoration.debugAssertIsValid());
    assert(foregroundDecoration == null || foregroundDecoration.debugAssertIsValid());
243 244
    assert(margin == null || margin.isNonNegative);
    assert(padding == null || padding.isNonNegative);
245
    assert(constraints == null || constraints.debugAssertIsValid());
246
  }
Adam Barth's avatar
Adam Barth committed
247

248
  /// The widget below this widget in the tree.
249
  final Widget child;
Adam Barth's avatar
Adam Barth committed
250

251 252
  /// Additional constraints to apply to the child.
  final BoxConstraints constraints;
Adam Barth's avatar
Adam Barth committed
253

254 255
  /// The decoration to paint behind the child.
  final Decoration decoration;
Adam Barth's avatar
Adam Barth committed
256

257 258
  /// The decoration to paint in front of the child.
  final Decoration foregroundDecoration;
Adam Barth's avatar
Adam Barth committed
259

260
  /// Empty space to surround the decoration.
261
  final EdgeInsets margin;
Adam Barth's avatar
Adam Barth committed
262

263
  /// Empty space to inscribe inside the decoration.
264
  final EdgeInsets padding;
265 266 267 268 269 270 271 272 273 274

  /// The transformation matrix to apply before painting the container.
  final Matrix4 transform;

  /// If non-null, requires the decoration to have this width.
  final double width;

  /// If non-null, requires the decoration to have this height.
  final double height;

275
  @override
276
  _AnimatedContainerState createState() => new _AnimatedContainerState();
277

278
  @override
279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    if (constraints != null)
      description.add('$constraints');
    if (decoration != null)
      description.add('has background');
    if (foregroundDecoration != null)
      description.add('has foreground');
    if (margin != null)
      description.add('margin: $margin');
    if (padding != null)
      description.add('padding: $padding');
    if (transform != null)
      description.add('has transform');
    if (width != null)
      description.add('width: $width');
    if (height != null)
      description.add('height: $height');
  }
298 299 300
}

class _AnimatedContainerState extends AnimatedWidgetBaseState<AnimatedContainer> {
301 302 303
  BoxConstraintsTween _constraints;
  DecorationTween _decoration;
  DecorationTween _foregroundDecoration;
304 305
  EdgeInsetsTween _margin;
  EdgeInsetsTween _padding;
306 307 308 309
  Matrix4Tween _transform;
  Tween<double> _width;
  Tween<double> _height;

310
  @override
311
  void forEachTween(TweenVisitor<dynamic> visitor) {
312
    // TODO(ianh): Use constructor tear-offs when it becomes possible
313 314 315
    _constraints = visitor(_constraints, config.constraints, (dynamic value) => new BoxConstraintsTween(begin: value));
    _decoration = visitor(_decoration, config.decoration, (dynamic value) => new DecorationTween(begin: value));
    _foregroundDecoration = visitor(_foregroundDecoration, config.foregroundDecoration, (dynamic value) => new DecorationTween(begin: value));
316 317
    _margin = visitor(_margin, config.margin, (dynamic value) => new EdgeInsetsTween(begin: value));
    _padding = visitor(_padding, config.padding, (dynamic value) => new EdgeInsetsTween(begin: value));
318 319 320
    _transform = visitor(_transform, config.transform, (dynamic value) => new Matrix4Tween(begin: value));
    _width = visitor(_width, config.width, (dynamic value) => new Tween<double>(begin: value));
    _height = visitor(_height, config.height, (dynamic value) => new Tween<double>(begin: value));
Adam Barth's avatar
Adam Barth committed
321 322
  }

323
  @override
Adam Barth's avatar
Adam Barth committed
324 325 326
  Widget build(BuildContext context) {
    return new Container(
      child: config.child,
327 328 329 330 331 332 333 334
      constraints: _constraints?.evaluate(animation),
      decoration: _decoration?.evaluate(animation),
      foregroundDecoration: _foregroundDecoration?.evaluate(animation),
      margin: _margin?.evaluate(animation),
      padding: _padding?.evaluate(animation),
      transform: _transform?.evaluate(animation),
      width: _width?.evaluate(animation),
      height: _height?.evaluate(animation)
Adam Barth's avatar
Adam Barth committed
335 336
    );
  }
Hixie's avatar
Hixie committed
337

338
  @override
Hixie's avatar
Hixie committed
339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    if (_constraints != null)
      description.add('has constraints');
    if (_decoration != null)
      description.add('has background');
    if (_foregroundDecoration != null)
      description.add('has foreground');
    if (_margin != null)
      description.add('has margin');
    if (_padding != null)
      description.add('has padding');
    if (_transform != null)
      description.add('has transform');
    if (_width != null)
      description.add('has width');
    if (_height != null)
      description.add('has height');
  }
Adam Barth's avatar
Adam Barth committed
358
}
Ian Hickson's avatar
Ian Hickson committed
359 360

/// Animated version of [Positioned] which automatically transitions the child's
361
/// position over a given duration whenever the given position changes.
Ian Hickson's avatar
Ian Hickson committed
362 363
///
/// Only works if it's the child of a [Stack].
364
class AnimatedPositioned extends ImplicitlyAnimatedWidget {
365 366 367 368 369 370 371 372
  /// Creates a widget that animates its position implicitly.
  ///
  /// Only two out of the three horizontal values ([left], [right],
  /// [width]), and only two out of the three vertical values ([top],
  /// [bottom], [height]), can be set. In each case, at least one of
  /// the three must be null.
  ///
  /// The [curve] and [duration] arguments must not be null.
Ian Hickson's avatar
Ian Hickson committed
373 374 375 376 377 378 379 380 381 382
  AnimatedPositioned({
    Key key,
    this.child,
    this.left,
    this.top,
    this.right,
    this.bottom,
    this.width,
    this.height,
    Curve curve: Curves.linear,
383
    @required Duration duration
Ian Hickson's avatar
Ian Hickson committed
384 385 386 387 388
  }) : super(key: key, curve: curve, duration: duration) {
    assert(left == null || right == null || width == null);
    assert(top == null || bottom == null || height == null);
  }

389 390 391
  /// Creates a widget that animates the rectangle it occupies implicitly.
  ///
  /// The [curve] and [duration] arguments must not be null.
Ian Hickson's avatar
Ian Hickson committed
392 393 394 395 396
  AnimatedPositioned.fromRect({
    Key key,
    this.child,
    Rect rect,
    Curve curve: Curves.linear,
397
    @required Duration duration
Ian Hickson's avatar
Ian Hickson committed
398 399 400 401 402 403 404 405
  }) : left = rect.left,
       top = rect.top,
       width = rect.width,
       height = rect.height,
       right = null,
       bottom = null,
       super(key: key, curve: curve, duration: duration);

406
  /// The widget below this widget in the tree.
Ian Hickson's avatar
Ian Hickson committed
407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432
  final Widget child;

  /// The offset of the child's left edge from the left of the stack.
  final double left;

  /// The offset of the child's top edge from the top of the stack.
  final double top;

  /// The offset of the child's right edge from the right of the stack.
  final double right;

  /// The offset of the child's bottom edge from the bottom of the stack.
  final double bottom;

  /// The child's width.
  ///
  /// Only two out of the three horizontal values (left, right, width) can be
  /// set. The third must be null.
  final double width;

  /// The child's height.
  ///
  /// Only two out of the three vertical values (top, bottom, height) can be
  /// set. The third must be null.
  final double height;

433
  @override
Ian Hickson's avatar
Ian Hickson committed
434
  _AnimatedPositionedState createState() => new _AnimatedPositionedState();
Ian Hickson's avatar
Ian Hickson committed
435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    if (left != null)
      description.add('left: $left');
    if (top != null)
      description.add('top: $top');
    if (right != null)
      description.add('right: $right');
    if (bottom != null)
      description.add('bottom: $bottom');
    if (width != null)
      description.add('width: $width');
    if (height != null)
      description.add('height: $height');
  }
Ian Hickson's avatar
Ian Hickson committed
452 453 454
}

class _AnimatedPositionedState extends AnimatedWidgetBaseState<AnimatedPositioned> {
455 456 457 458 459 460 461
  Tween<double> _left;
  Tween<double> _top;
  Tween<double> _right;
  Tween<double> _bottom;
  Tween<double> _width;
  Tween<double> _height;

462
  @override
463
  void forEachTween(TweenVisitor<dynamic> visitor) {
Ian Hickson's avatar
Ian Hickson committed
464
    // TODO(ianh): Use constructor tear-offs when it becomes possible
465 466 467 468 469 470
    _left = visitor(_left, config.left, (dynamic value) => new Tween<double>(begin: value));
    _top = visitor(_top, config.top, (dynamic value) => new Tween<double>(begin: value));
    _right = visitor(_right, config.right, (dynamic value) => new Tween<double>(begin: value));
    _bottom = visitor(_bottom, config.bottom, (dynamic value) => new Tween<double>(begin: value));
    _width = visitor(_width, config.width, (dynamic value) => new Tween<double>(begin: value));
    _height = visitor(_height, config.height, (dynamic value) => new Tween<double>(begin: value));
Ian Hickson's avatar
Ian Hickson committed
471 472
  }

473
  @override
Ian Hickson's avatar
Ian Hickson committed
474 475 476
  Widget build(BuildContext context) {
    return new Positioned(
      child: config.child,
477 478 479 480 481 482
      left: _left?.evaluate(animation),
      top: _top?.evaluate(animation),
      right: _right?.evaluate(animation),
      bottom: _bottom?.evaluate(animation),
      width: _width?.evaluate(animation),
      height: _height?.evaluate(animation)
Ian Hickson's avatar
Ian Hickson committed
483 484 485
    );
  }

486
  @override
Ian Hickson's avatar
Ian Hickson committed
487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    if (_left != null)
      description.add('has left');
    if (_top != null)
      description.add('has top');
    if (_right != null)
      description.add('has right');
    if (_bottom != null)
      description.add('has bottom');
    if (_width != null)
      description.add('has width');
    if (_height != null)
      description.add('has height');
  }
}
Ian Hickson's avatar
Ian Hickson committed
503 504 505 506 507 508

/// Animated version of [Opacity] which automatically transitions the child's
/// opacity over a given duration whenever the given opacity changes.
///
/// Animating an opacity is relatively expensive.
class AnimatedOpacity extends ImplicitlyAnimatedWidget {
509 510 511 512
  /// Creates a widget that animates its opacity implicitly.
  ///
  /// The [opacity] argument must not be null and must be between 0.0 and 1.0,
  /// inclusive. The [curve] and [duration] arguments must not be null.
Ian Hickson's avatar
Ian Hickson committed
513 514 515 516 517
  AnimatedOpacity({
    Key key,
    this.child,
    this.opacity,
    Curve curve: Curves.linear,
518
    @required Duration duration
Ian Hickson's avatar
Ian Hickson committed
519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560
  }) : super(key: key, curve: curve, duration: duration) {
    assert(opacity != null && opacity >= 0.0 && opacity <= 1.0);
  }

  /// The widget below this widget in the tree.
  final Widget child;

  /// The target opacity.
  ///
  /// An opacity of 1.0 is fully opaque. An opacity of 0.0 is fully transparent
  /// (i.e., invisible).
  ///
  /// The opacity must not be null.
  final double opacity;

  @override
  _AnimatedOpacityState createState() => new _AnimatedOpacityState();

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('opacity: $opacity');
  }
}

class _AnimatedOpacityState extends AnimatedWidgetBaseState<AnimatedOpacity> {
  Tween<double> _opacity;

  @override
  void forEachTween(TweenVisitor<dynamic> visitor) {
    // TODO(ianh): Use constructor tear-offs when it becomes possible
    _opacity = visitor(_opacity, config.opacity, (dynamic value) => new Tween<double>(begin: value));
  }

  @override
  Widget build(BuildContext context) {
    return new Opacity(
      opacity: _opacity.evaluate(animation),
      child: config.child
    );
  }
}
561 562 563 564 565 566

/// Animated version of [DefaultTextStyle] which automatically
/// transitions the default text style (the text style to apply to
/// descendant [Text] widgets without explicit style) over a given
/// duration whenever the given style changes.
class AnimatedDefaultTextStyle extends ImplicitlyAnimatedWidget {
567 568 569
  /// Creates a widget that animates the default text style implicitly.
  ///
  /// The [child], [style], [curve], and [duration] arguments must not be null.
570 571
  AnimatedDefaultTextStyle({
    Key key,
572 573
    @required this.child,
    @required this.style,
574
    Curve curve: Curves.linear,
575
    @required Duration duration
576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609
  }) : super(key: key, curve: curve, duration: duration) {
    assert(style != null);
    assert(child != null);
  }

  /// The widget below this widget in the tree.
  final Widget child;

  /// The target text style.
  ///
  /// The text style must not be null.
  final TextStyle style;

  @override
  _AnimatedDefaultTextStyleState createState() => new _AnimatedDefaultTextStyleState();

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    '$style'.split('\n').forEach(description.add);
  }
}

class _AnimatedDefaultTextStyleState extends AnimatedWidgetBaseState<AnimatedDefaultTextStyle> {
  TextStyleTween _style;

  @override
  void forEachTween(TweenVisitor<dynamic> visitor) {
    // TODO(ianh): Use constructor tear-offs when it becomes possible
    _style = visitor(_style, config.style, (dynamic value) => new TextStyleTween(begin: value));
  }

  @override
  Widget build(BuildContext context) {
610
    return new DefaultTextStyle(
611 612 613 614 615
      style: _style.evaluate(animation),
      child: config.child
    );
  }
}