implicit_animations.dart 13.7 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 15 16

  BoxConstraints lerp(double t) => BoxConstraints.lerp(begin, end, t);
}

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

21 22 23 24 25 26 27
  Decoration lerp(double t) {
    if (begin == null && end == null)
      return null;
    if (end == null)
      return begin.lerpTo(end, t);
    return end.lerpFrom(begin, t);
  }
Adam Barth's avatar
Adam Barth committed
28 29
}

30
/// An interpolation between two [EdgeDims]s.
31 32
class EdgeDimsTween extends Tween<EdgeDims> {
  EdgeDimsTween({ EdgeDims begin, EdgeDims end }) : super(begin: begin, end: end);
Adam Barth's avatar
Adam Barth committed
33 34 35 36

  EdgeDims lerp(double t) => EdgeDims.lerp(begin, end, t);
}

37
/// An interpolation between two [Matrix4]s.
38 39
///
/// Currently this class works only for translations.
40 41
class Matrix4Tween extends Tween<Matrix4> {
  Matrix4Tween({ Matrix4 begin, Matrix4 end }) : super(begin: begin, end: end);
Adam Barth's avatar
Adam Barth committed
42 43 44 45 46 47 48 49 50 51 52

  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);
  }
}

53 54 55 56
/// An abstract widget for building components that gradually change their
/// values over a period of time.
abstract class AnimatedWidgetBase extends StatefulComponent {
  AnimatedWidgetBase({
Adam Barth's avatar
Adam Barth committed
57
    Key key,
58
    this.curve: Curves.linear,
Adam Barth's avatar
Adam Barth committed
59 60 61
    this.duration
  }) : super(key: key) {
    assert(curve != null);
Ian Hickson's avatar
Ian Hickson committed
62
    assert(duration != null);
Adam Barth's avatar
Adam Barth committed
63 64
  }

65
  /// The curve to apply when animating the parameters of this container.
Adam Barth's avatar
Adam Barth committed
66
  final Curve curve;
67 68

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

71
  AnimatedWidgetBaseState createState();
72 73 74 75 76

  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('duration: ${duration.inMilliseconds}ms');
  }
Adam Barth's avatar
Adam Barth committed
77 78
}

79
/// Used by [AnimatedWidgetBaseState].
80
typedef Tween<T> TweenConstructor<T>(T targetValue);
81 82

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

85
/// A base class for widgets with implicit animations.
86
abstract class AnimatedWidgetBaseState<T extends AnimatedWidgetBase> extends State<T> {
87 88
  AnimationController _controller;

89
  /// The animation driving this widget's implicit animations.
90 91
  Animation<double> get animation => _animation;
  Animation<double> _animation;
Adam Barth's avatar
Adam Barth committed
92 93 94

  void initState() {
    super.initState();
95
    _controller = new AnimationController(
96 97
      duration: config.duration,
      debugLabel: '${config.toStringShort()}'
98
    )..addListener(_handleAnimationChanged);
99
    _updateCurve();
100
    _constructTweens();
Adam Barth's avatar
Adam Barth committed
101 102
  }

103
  void didUpdateConfig(T oldConfig) {
104 105
    if (config.curve != oldConfig.curve)
      _updateCurve();
106 107 108 109 110
    _controller.duration = config.duration;
    if (_constructTweens()) {
      forEachTween((Tween tween, dynamic targetValue, TweenConstructor<T> constructor) {
        _updateTween(tween, targetValue);
        return tween;
111
      });
112 113 114
      _controller
        ..value = 0.0
        ..forward();
Adam Barth's avatar
Adam Barth committed
115 116 117
    }
  }

118 119
  void _updateCurve() {
    if (config.curve != null)
120
      _animation = new CurvedAnimation(parent: _controller, curve: config.curve);
121
    else
122
      _animation = _controller;
123 124
  }

Adam Barth's avatar
Adam Barth committed
125
  void dispose() {
126
    _controller.stop();
Adam Barth's avatar
Adam Barth committed
127 128 129
    super.dispose();
  }

130 131
  void _handleAnimationChanged() {
    setState(() { });
Adam Barth's avatar
Adam Barth committed
132 133
  }

134 135
  bool _shouldAnimateTween(Tween tween, dynamic targetValue) {
    return targetValue != (tween.end ?? tween.begin);
136 137
  }

138 139 140 141 142 143
  void _updateTween(Tween tween, dynamic targetValue) {
    if (tween == null)
      return;
    tween
      ..begin = tween.evaluate(_animation)
      ..end = targetValue;
Adam Barth's avatar
Adam Barth committed
144 145
  }

146 147 148
  bool _constructTweens() {
    bool shouldStartAnimation = false;
    forEachTween((Tween tween, dynamic targetValue, TweenConstructor<T> constructor) {
149
      if (targetValue != null) {
150 151 152
        tween ??= constructor(targetValue);
        if (_shouldAnimateTween(tween, targetValue))
          shouldStartAnimation = true;
153
      } else {
154
        tween = null;
155
      }
156
      return tween;
157
    });
158
    return shouldStartAnimation;
159
  }
Adam Barth's avatar
Adam Barth committed
160

161 162 163 164
  /// Subclasses must implement this function by running through the following
  /// steps for for each animatable facet in the class:
  ///
  /// 1. Call the visitor callback with three arguments, the first argument
165 166
  /// being the current value of the Tween<T> object that represents the
  /// tween (initially null), the second argument, of type T, being the value
167
  /// on the Widget (config) that represents the current target value of the
168
  /// tween, and the third being a callback that takes a value T (which will
169
  /// be the second argument to the visitor callback), and that returns an
170
  /// Tween<T> object for the tween, configured with the given value
171 172 173
  /// as the begin value.
  ///
  /// 2. Take the value returned from the callback, and store it. This is the
174
  /// value to use as the current value the next time that the forEachTween()
175
  /// method is called.
176
  void forEachTween(TweenVisitor visitor);
177
}
Adam Barth's avatar
Adam Barth committed
178

179 180 181 182
/// 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
183 184
/// likely want to use a subclass of [Transition] or use an
/// [AnimationController] yourself.
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204
class AnimatedContainer extends AnimatedWidgetBase {
  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
205

206
  final Widget child;
Adam Barth's avatar
Adam Barth committed
207

208 209
  /// Additional constraints to apply to the child.
  final BoxConstraints constraints;
Adam Barth's avatar
Adam Barth committed
210

211 212
  /// The decoration to paint behind the child.
  final Decoration decoration;
Adam Barth's avatar
Adam Barth committed
213

214 215
  /// The decoration to paint in front of the child.
  final Decoration foregroundDecoration;
Adam Barth's avatar
Adam Barth committed
216

217 218
  /// Empty space to surround the decoration.
  final EdgeDims margin;
Adam Barth's avatar
Adam Barth committed
219

220 221 222 223 224 225 226 227 228 229 230 231 232
  /// Empty space to inscribe inside the decoration.
  final EdgeDims padding;

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

  _AnimatedContainerState createState() => new _AnimatedContainerState();
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252

  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');
  }
253 254 255
}

class _AnimatedContainerState extends AnimatedWidgetBaseState<AnimatedContainer> {
256 257 258 259 260 261 262 263 264 265
  BoxConstraintsTween _constraints;
  DecorationTween _decoration;
  DecorationTween _foregroundDecoration;
  EdgeDimsTween _margin;
  EdgeDimsTween _padding;
  Matrix4Tween _transform;
  Tween<double> _width;
  Tween<double> _height;

  void forEachTween(TweenVisitor visitor) {
266
    // TODO(ianh): Use constructor tear-offs when it becomes possible
267 268 269 270 271 272 273 274
    _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));
    _margin = visitor(_margin, config.margin, (dynamic value) => new EdgeDimsTween(begin: value));
    _padding = visitor(_padding, config.padding, (dynamic value) => new EdgeDimsTween(begin: value));
    _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
275 276 277 278 279
  }

  Widget build(BuildContext context) {
    return new Container(
      child: config.child,
280 281 282 283 284 285 286 287
      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
288 289
    );
  }
Hixie's avatar
Hixie committed
290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309

  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
310
}
Ian Hickson's avatar
Ian Hickson committed
311 312

/// Animated version of [Positioned] which automatically transitions the child's
313
/// position over a given duration whenever the given position changes.
Ian Hickson's avatar
Ian Hickson committed
314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 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 366 367 368 369 370 371 372 373 374 375 376
///
/// Only works if it's the child of a [Stack].
class AnimatedPositioned extends AnimatedWidgetBase {
  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);

  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;

  _AnimatedPositionedState createState() => new _AnimatedPositionedState();
}

class _AnimatedPositionedState extends AnimatedWidgetBaseState<AnimatedPositioned> {
377 378 379 380 381 382 383 384
  Tween<double> _left;
  Tween<double> _top;
  Tween<double> _right;
  Tween<double> _bottom;
  Tween<double> _width;
  Tween<double> _height;

  void forEachTween(TweenVisitor visitor) {
Ian Hickson's avatar
Ian Hickson committed
385
    // TODO(ianh): Use constructor tear-offs when it becomes possible
386 387 388 389 390 391
    _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
392 393 394 395 396
  }

  Widget build(BuildContext context) {
    return new Positioned(
      child: config.child,
397 398 399 400 401 402
      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
403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421
    );
  }

  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');
  }
}