implicit_animations.dart 19.6 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
import 'basic.dart';
6
import 'container.dart';
7
import 'framework.dart';
Adam Barth's avatar
Adam Barth committed
8

9
import 'package:meta/meta.dart';
Adam Barth's avatar
Adam Barth committed
10 11
import 'package:vector_math/vector_math_64.dart';

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

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

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

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

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

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

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

54
  @override
Adam Barth's avatar
Adam Barth committed
55
  Matrix4 lerp(double t) {
56 57
    // 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
58 59 60 61 62 63 64
    Vector3 beginT = begin.getTranslation();
    Vector3 endT = end.getTranslation();
    Vector3 lerpT = beginT*(1.0-t) + endT*t;
    return new Matrix4.identity()..translate(lerpT);
  }
}

65 66 67 68
/// 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> {
69 70 71
  /// Creates a text style tween.
  ///
  /// The [begin] and [end] arguments must not be null.
72 73 74 75 76 77
  TextStyleTween({ TextStyle begin, TextStyle end }) : super(begin: begin, end: end);

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

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

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

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

99
  @override
100
  AnimatedWidgetBaseState<ImplicitlyAnimatedWidget> createState();
101

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

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

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

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

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

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

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

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

157
  @override
Adam Barth's avatar
Adam Barth committed
158
  void dispose() {
159
    _controller.stop();
Adam Barth's avatar
Adam Barth committed
160 161 162
    super.dispose();
  }

163 164
  void _handleAnimationChanged() {
    setState(() { });
Adam Barth's avatar
Adam Barth committed
165 166
  }

167
  bool _shouldAnimateTween(Tween<dynamic> tween, dynamic targetValue) {
168
    return targetValue != (tween.end ?? tween.begin);
169 170
  }

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

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

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

212 213 214 215
/// 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
216 217
/// likely want to use a subclass of [Transition] or use an
/// [AnimationController] yourself.
218
class AnimatedContainer extends ImplicitlyAnimatedWidget {
219 220 221
  /// Creates a container that animates its parameters implicitly.
  ///
  /// The [curve] and [duration] arguments must not be null.
222 223 224 225 226 227 228 229 230 231 232 233
  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,
234
    @required Duration duration
235 236 237 238 239 240
  }) : super(key: key, curve: curve, duration: duration) {
    assert(decoration == null || decoration.debugAssertValid());
    assert(foregroundDecoration == null || foregroundDecoration.debugAssertValid());
    assert(margin == null || margin.isNonNegative);
    assert(padding == null || padding.isNonNegative);
  }
Adam Barth's avatar
Adam Barth committed
241

242
  /// The widget below this widget in the tree.
243
  final Widget child;
Adam Barth's avatar
Adam Barth committed
244

245 246
  /// Additional constraints to apply to the child.
  final BoxConstraints constraints;
Adam Barth's avatar
Adam Barth committed
247

248 249
  /// The decoration to paint behind the child.
  final Decoration decoration;
Adam Barth's avatar
Adam Barth committed
250

251 252
  /// The decoration to paint in front of the child.
  final Decoration foregroundDecoration;
Adam Barth's avatar
Adam Barth committed
253

254
  /// Empty space to surround the decoration.
255
  final EdgeInsets margin;
Adam Barth's avatar
Adam Barth committed
256

257
  /// Empty space to inscribe inside the decoration.
258
  final EdgeInsets padding;
259 260 261 262 263 264 265 266 267 268

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

269
  @override
270
  _AnimatedContainerState createState() => new _AnimatedContainerState();
271

272
  @override
273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291
  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');
  }
292 293 294
}

class _AnimatedContainerState extends AnimatedWidgetBaseState<AnimatedContainer> {
295 296 297
  BoxConstraintsTween _constraints;
  DecorationTween _decoration;
  DecorationTween _foregroundDecoration;
298 299
  EdgeInsetsTween _margin;
  EdgeInsetsTween _padding;
300 301 302 303
  Matrix4Tween _transform;
  Tween<double> _width;
  Tween<double> _height;

304
  @override
305
  void forEachTween(TweenVisitor<dynamic> visitor) {
306
    // TODO(ianh): Use constructor tear-offs when it becomes possible
307 308 309
    _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));
310 311
    _margin = visitor(_margin, config.margin, (dynamic value) => new EdgeInsetsTween(begin: value));
    _padding = visitor(_padding, config.padding, (dynamic value) => new EdgeInsetsTween(begin: value));
312 313 314
    _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
315 316
  }

317
  @override
Adam Barth's avatar
Adam Barth committed
318 319 320
  Widget build(BuildContext context) {
    return new Container(
      child: config.child,
321 322 323 324 325 326 327 328
      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
329 330
    );
  }
Hixie's avatar
Hixie committed
331

332
  @override
Hixie's avatar
Hixie committed
333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351
  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
352
}
Ian Hickson's avatar
Ian Hickson committed
353 354

/// Animated version of [Positioned] which automatically transitions the child's
355
/// position over a given duration whenever the given position changes.
Ian Hickson's avatar
Ian Hickson committed
356 357
///
/// Only works if it's the child of a [Stack].
358
class AnimatedPositioned extends ImplicitlyAnimatedWidget {
359 360 361 362 363 364 365 366
  /// 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
367 368 369 370 371 372 373 374 375 376
  AnimatedPositioned({
    Key key,
    this.child,
    this.left,
    this.top,
    this.right,
    this.bottom,
    this.width,
    this.height,
    Curve curve: Curves.linear,
377
    @required Duration duration
Ian Hickson's avatar
Ian Hickson committed
378 379 380 381 382
  }) : super(key: key, curve: curve, duration: duration) {
    assert(left == null || right == null || width == null);
    assert(top == null || bottom == null || height == null);
  }

383 384 385
  /// 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
386 387 388 389 390
  AnimatedPositioned.fromRect({
    Key key,
    this.child,
    Rect rect,
    Curve curve: Curves.linear,
391
    @required Duration duration
Ian Hickson's avatar
Ian Hickson committed
392 393 394 395 396 397 398 399
  }) : left = rect.left,
       top = rect.top,
       width = rect.width,
       height = rect.height,
       right = null,
       bottom = null,
       super(key: key, curve: curve, duration: duration);

400
  /// The widget below this widget in the tree.
Ian Hickson's avatar
Ian Hickson committed
401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426
  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;

427
  @override
Ian Hickson's avatar
Ian Hickson committed
428
  _AnimatedPositionedState createState() => new _AnimatedPositionedState();
Ian Hickson's avatar
Ian Hickson committed
429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445

  @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
446 447 448
}

class _AnimatedPositionedState extends AnimatedWidgetBaseState<AnimatedPositioned> {
449 450 451 452 453 454 455
  Tween<double> _left;
  Tween<double> _top;
  Tween<double> _right;
  Tween<double> _bottom;
  Tween<double> _width;
  Tween<double> _height;

456
  @override
457
  void forEachTween(TweenVisitor<dynamic> visitor) {
Ian Hickson's avatar
Ian Hickson committed
458
    // TODO(ianh): Use constructor tear-offs when it becomes possible
459 460 461 462 463 464
    _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
465 466
  }

467
  @override
Ian Hickson's avatar
Ian Hickson committed
468 469 470
  Widget build(BuildContext context) {
    return new Positioned(
      child: config.child,
471 472 473 474 475 476
      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
477 478 479
    );
  }

480
  @override
Ian Hickson's avatar
Ian Hickson committed
481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496
  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
497 498 499 500 501 502

/// 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 {
503 504 505 506
  /// 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
507 508 509 510 511
  AnimatedOpacity({
    Key key,
    this.child,
    this.opacity,
    Curve curve: Curves.linear,
512
    @required Duration duration
Ian Hickson's avatar
Ian Hickson committed
513 514 515 516 517 518 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
  }) : 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
    );
  }
}
555 556 557 558 559 560

/// 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 {
561 562 563
  /// Creates a widget that animates the default text style implicitly.
  ///
  /// The [child], [style], [curve], and [duration] arguments must not be null.
564 565
  AnimatedDefaultTextStyle({
    Key key,
566 567
    @required this.child,
    @required this.style,
568
    Curve curve: Curves.linear,
569
    @required Duration duration
570 571 572 573 574 575 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
  }) : 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) {
604
    return new DefaultTextStyle(
605 606 607 608 609
      style: _style.evaluate(animation),
      child: config.child
    );
  }
}