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

import 'package:vector_math/vector_math_64.dart';

10
/// An interpolation between two [BoxConstraint]s.
11 12
class BoxConstraintsTween extends Tween<BoxConstraints> {
  BoxConstraintsTween({ BoxConstraints begin, BoxConstraints end }) : super(begin: begin, end: end);
Adam Barth's avatar
Adam Barth committed
13

14
  @override
Adam Barth's avatar
Adam Barth committed
15 16 17
  BoxConstraints lerp(double t) => BoxConstraints.lerp(begin, end, t);
}

18
/// An interpolation between two [Decoration]s.
19 20
class DecorationTween extends Tween<Decoration> {
  DecorationTween({ Decoration begin, Decoration end }) : super(begin: begin, end: end);
Adam Barth's avatar
Adam Barth committed
21

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

26 27 28
/// An interpolation between two [EdgeInsets]s.
class EdgeInsetsTween extends Tween<EdgeInsets> {
  EdgeInsetsTween({ EdgeInsets begin, EdgeInsets end }) : super(begin: begin, end: end);
Adam Barth's avatar
Adam Barth committed
29

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

34
/// An interpolation between two [Matrix4]s.
35 36
///
/// Currently this class works only for translations.
37 38
class Matrix4Tween extends Tween<Matrix4> {
  Matrix4Tween({ Matrix4 begin, Matrix4 end }) : super(begin: begin, end: end);
Adam Barth's avatar
Adam Barth committed
39

40
  @override
Adam Barth's avatar
Adam Barth committed
41 42 43 44 45 46 47 48 49 50
  Matrix4 lerp(double t) {
    // TODO(mpcomplete): Animate the full matrix. Will animating the cells
    // separately work?
    Vector3 beginT = begin.getTranslation();
    Vector3 endT = end.getTranslation();
    Vector3 lerpT = beginT*(1.0-t) + endT*t;
    return new Matrix4.identity()..translate(lerpT);
  }
}

51 52 53 54 55 56 57 58 59 60
/// 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> {
  TextStyleTween({ TextStyle begin, TextStyle end }) : super(begin: begin, end: end);

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

61
/// An abstract widget for building widgets that gradually change their
62
/// values over a period of time.
63 64
abstract class ImplicitlyAnimatedWidget extends StatefulWidget {
  ImplicitlyAnimatedWidget({
Adam Barth's avatar
Adam Barth committed
65
    Key key,
66
    this.curve: Curves.linear,
Adam Barth's avatar
Adam Barth committed
67 68 69
    this.duration
  }) : super(key: key) {
    assert(curve != null);
Ian Hickson's avatar
Ian Hickson committed
70
    assert(duration != null);
Adam Barth's avatar
Adam Barth committed
71 72
  }

73
  /// The curve to apply when animating the parameters of this container.
Adam Barth's avatar
Adam Barth committed
74
  final Curve curve;
75 76

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

79
  @override
80
  AnimatedWidgetBaseState<ImplicitlyAnimatedWidget> createState();
81

82
  @override
83 84 85 86
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('duration: ${duration.inMilliseconds}ms');
  }
Adam Barth's avatar
Adam Barth committed
87 88
}

89
/// Used by [AnimatedWidgetBaseState].
90
typedef Tween<T> TweenConstructor<T>(T targetValue);
91 92

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

95
/// A base class for widgets with implicit animations.
96
abstract class AnimatedWidgetBaseState<T extends ImplicitlyAnimatedWidget> extends State<T> {
97 98
  AnimationController _controller;

99
  /// The animation driving this widget's implicit animations.
100 101
  Animation<double> get animation => _animation;
  Animation<double> _animation;
Adam Barth's avatar
Adam Barth committed
102

103
  @override
Adam Barth's avatar
Adam Barth committed
104 105
  void initState() {
    super.initState();
106
    _controller = new AnimationController(
107 108
      duration: config.duration,
      debugLabel: '${config.toStringShort()}'
109
    )..addListener(_handleAnimationChanged);
110
    _updateCurve();
111
    _constructTweens();
Adam Barth's avatar
Adam Barth committed
112 113
  }

114
  @override
115
  void didUpdateConfig(T oldConfig) {
116 117
    if (config.curve != oldConfig.curve)
      _updateCurve();
118 119
    _controller.duration = config.duration;
    if (_constructTweens()) {
120
      forEachTween((Tween<dynamic> tween, dynamic targetValue, TweenConstructor<dynamic> constructor) {
121 122
        _updateTween(tween, targetValue);
        return tween;
123
      });
124 125 126
      _controller
        ..value = 0.0
        ..forward();
Adam Barth's avatar
Adam Barth committed
127 128 129
    }
  }

130 131
  void _updateCurve() {
    if (config.curve != null)
132
      _animation = new CurvedAnimation(parent: _controller, curve: config.curve);
133
    else
134
      _animation = _controller;
135 136
  }

137
  @override
Adam Barth's avatar
Adam Barth committed
138
  void dispose() {
139
    _controller.stop();
Adam Barth's avatar
Adam Barth committed
140 141 142
    super.dispose();
  }

143 144
  void _handleAnimationChanged() {
    setState(() { });
Adam Barth's avatar
Adam Barth committed
145 146
  }

147
  bool _shouldAnimateTween(Tween<dynamic> tween, dynamic targetValue) {
148
    return targetValue != (tween.end ?? tween.begin);
149 150
  }

151
  void _updateTween(Tween<dynamic> tween, dynamic targetValue) {
152 153 154 155 156
    if (tween == null)
      return;
    tween
      ..begin = tween.evaluate(_animation)
      ..end = targetValue;
Adam Barth's avatar
Adam Barth committed
157 158
  }

159 160
  bool _constructTweens() {
    bool shouldStartAnimation = false;
161
    forEachTween((Tween<dynamic> tween, dynamic targetValue, TweenConstructor<T> constructor) {
162
      if (targetValue != null) {
163 164 165
        tween ??= constructor(targetValue);
        if (_shouldAnimateTween(tween, targetValue))
          shouldStartAnimation = true;
166
      } else {
167
        tween = null;
168
      }
169
      return tween;
170
    });
171
    return shouldStartAnimation;
172
  }
Adam Barth's avatar
Adam Barth committed
173

174
  /// Subclasses must implement this function by running through the following
175
  /// steps for each animatable facet in the class:
176 177
  ///
  /// 1. Call the visitor callback with three arguments, the first argument
178 179
  /// being the current value of the Tween<T> object that represents the
  /// tween (initially null), the second argument, of type T, being the value
180
  /// on the Widget (config) that represents the current target value of the
181
  /// tween, and the third being a callback that takes a value T (which will
182
  /// be the second argument to the visitor callback), and that returns an
183
  /// Tween<T> object for the tween, configured with the given value
184 185 186
  /// as the begin value.
  ///
  /// 2. Take the value returned from the callback, and store it. This is the
187
  /// value to use as the current value the next time that the forEachTween()
188
  /// method is called.
189
  void forEachTween(TweenVisitor<dynamic> visitor);
190
}
Adam Barth's avatar
Adam Barth committed
191

192 193 194 195
/// 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
196 197
/// likely want to use a subclass of [Transition] or use an
/// [AnimationController] yourself.
198
class AnimatedContainer extends ImplicitlyAnimatedWidget {
199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217
  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,
    Duration duration
  }) : 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
218

219
  /// The widget below this widget in the tree.
220
  final Widget child;
Adam Barth's avatar
Adam Barth committed
221

222 223
  /// Additional constraints to apply to the child.
  final BoxConstraints constraints;
Adam Barth's avatar
Adam Barth committed
224

225 226
  /// The decoration to paint behind the child.
  final Decoration decoration;
Adam Barth's avatar
Adam Barth committed
227

228 229
  /// The decoration to paint in front of the child.
  final Decoration foregroundDecoration;
Adam Barth's avatar
Adam Barth committed
230

231
  /// Empty space to surround the decoration.
232
  final EdgeInsets margin;
Adam Barth's avatar
Adam Barth committed
233

234
  /// Empty space to inscribe inside the decoration.
235
  final EdgeInsets padding;
236 237 238 239 240 241 242 243 244 245

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

246
  @override
247
  _AnimatedContainerState createState() => new _AnimatedContainerState();
248

249
  @override
250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268
  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');
  }
269 270 271
}

class _AnimatedContainerState extends AnimatedWidgetBaseState<AnimatedContainer> {
272 273 274
  BoxConstraintsTween _constraints;
  DecorationTween _decoration;
  DecorationTween _foregroundDecoration;
275 276
  EdgeInsetsTween _margin;
  EdgeInsetsTween _padding;
277 278 279 280
  Matrix4Tween _transform;
  Tween<double> _width;
  Tween<double> _height;

281
  @override
282
  void forEachTween(TweenVisitor<dynamic> visitor) {
283
    // TODO(ianh): Use constructor tear-offs when it becomes possible
284 285 286
    _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));
287 288
    _margin = visitor(_margin, config.margin, (dynamic value) => new EdgeInsetsTween(begin: value));
    _padding = visitor(_padding, config.padding, (dynamic value) => new EdgeInsetsTween(begin: value));
289 290 291
    _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
292 293
  }

294
  @override
Adam Barth's avatar
Adam Barth committed
295 296 297
  Widget build(BuildContext context) {
    return new Container(
      child: config.child,
298 299 300 301 302 303 304 305
      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
306 307
    );
  }
Hixie's avatar
Hixie committed
308

309
  @override
Hixie's avatar
Hixie committed
310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328
  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
329
}
Ian Hickson's avatar
Ian Hickson committed
330 331

/// Animated version of [Positioned] which automatically transitions the child's
332
/// position over a given duration whenever the given position changes.
Ian Hickson's avatar
Ian Hickson committed
333 334
///
/// Only works if it's the child of a [Stack].
335
class AnimatedPositioned extends ImplicitlyAnimatedWidget {
Ian Hickson's avatar
Ian Hickson committed
336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365
  AnimatedPositioned({
    Key key,
    this.child,
    this.left,
    this.top,
    this.right,
    this.bottom,
    this.width,
    this.height,
    Curve curve: Curves.linear,
    Duration duration
  }) : super(key: key, curve: curve, duration: duration) {
    assert(left == null || right == null || width == null);
    assert(top == null || bottom == null || height == null);
  }

  AnimatedPositioned.fromRect({
    Key key,
    this.child,
    Rect rect,
    Curve curve: Curves.linear,
    Duration duration
  }) : left = rect.left,
       top = rect.top,
       width = rect.width,
       height = rect.height,
       right = null,
       bottom = null,
       super(key: key, curve: curve, duration: duration);

366
  /// The widget below this widget in the tree.
Ian Hickson's avatar
Ian Hickson committed
367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392
  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;

393
  @override
Ian Hickson's avatar
Ian Hickson committed
394
  _AnimatedPositionedState createState() => new _AnimatedPositionedState();
Ian Hickson's avatar
Ian Hickson committed
395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411

  @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
412 413 414
}

class _AnimatedPositionedState extends AnimatedWidgetBaseState<AnimatedPositioned> {
415 416 417 418 419 420 421
  Tween<double> _left;
  Tween<double> _top;
  Tween<double> _right;
  Tween<double> _bottom;
  Tween<double> _width;
  Tween<double> _height;

422
  @override
423
  void forEachTween(TweenVisitor<dynamic> visitor) {
Ian Hickson's avatar
Ian Hickson committed
424
    // TODO(ianh): Use constructor tear-offs when it becomes possible
425 426 427 428 429 430
    _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
431 432
  }

433
  @override
Ian Hickson's avatar
Ian Hickson committed
434 435 436
  Widget build(BuildContext context) {
    return new Positioned(
      child: config.child,
437 438 439 440 441 442
      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
443 444 445
    );
  }

446
  @override
Ian Hickson's avatar
Ian Hickson committed
447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462
  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
463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516

/// 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 {
  AnimatedOpacity({
    Key key,
    this.child,
    this.opacity,
    Curve curve: Curves.linear,
    Duration duration
  }) : 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
    );
  }
}
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 555 556 557 558 559 560 561 562

/// 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 {
  AnimatedDefaultTextStyle({
    Key key,
    this.child,
    this.style,
    Curve curve: Curves.linear,
    Duration duration
  }) : 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) {
563
    return new DefaultTextStyle(
564 565 566 567 568
      style: _style.evaluate(animation),
      child: config.child
    );
  }
}