implicit_animations.dart 14.1 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
/// An abstract widget for building widgets that gradually change their
52
/// values over a period of time.
53 54
abstract class ImplicitlyAnimatedWidget extends StatefulWidget {
  ImplicitlyAnimatedWidget({
Adam Barth's avatar
Adam Barth committed
55
    Key key,
56
    this.curve: Curves.linear,
Adam Barth's avatar
Adam Barth committed
57 58 59
    this.duration
  }) : super(key: key) {
    assert(curve != null);
Ian Hickson's avatar
Ian Hickson committed
60
    assert(duration != null);
Adam Barth's avatar
Adam Barth committed
61 62
  }

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

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

69
  @override
70
  AnimatedWidgetBaseState<ImplicitlyAnimatedWidget> createState();
71

72
  @override
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 ImplicitlyAnimatedWidget> 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
  @override
Adam Barth's avatar
Adam Barth committed
94 95
  void initState() {
    super.initState();
96
    _controller = new AnimationController(
97 98
      duration: config.duration,
      debugLabel: '${config.toStringShort()}'
99
    )..addListener(_handleAnimationChanged);
100
    _updateCurve();
101
    _constructTweens();
Adam Barth's avatar
Adam Barth committed
102 103
  }

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

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

127
  @override
Adam Barth's avatar
Adam Barth committed
128
  void dispose() {
129
    _controller.stop();
Adam Barth's avatar
Adam Barth committed
130 131 132
    super.dispose();
  }

133 134
  void _handleAnimationChanged() {
    setState(() { });
Adam Barth's avatar
Adam Barth committed
135 136
  }

137
  bool _shouldAnimateTween(Tween<dynamic> tween, dynamic targetValue) {
138
    return targetValue != (tween.end ?? tween.begin);
139 140
  }

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

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

164 165 166 167
  /// 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
168 169
  /// being the current value of the Tween<T> object that represents the
  /// tween (initially null), the second argument, of type T, being the value
170
  /// on the Widget (config) that represents the current target value of the
171
  /// tween, and the third being a callback that takes a value T (which will
172
  /// be the second argument to the visitor callback), and that returns an
173
  /// Tween<T> object for the tween, configured with the given value
174 175 176
  /// as the begin value.
  ///
  /// 2. Take the value returned from the callback, and store it. This is the
177
  /// value to use as the current value the next time that the forEachTween()
178
  /// method is called.
179
  void forEachTween(TweenVisitor<dynamic> visitor);
180
}
Adam Barth's avatar
Adam Barth committed
181

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

209
  /// The widget below this widget in the tree.
210
  final Widget child;
Adam Barth's avatar
Adam Barth committed
211

212 213
  /// Additional constraints to apply to the child.
  final BoxConstraints constraints;
Adam Barth's avatar
Adam Barth committed
214

215 216
  /// The decoration to paint behind the child.
  final Decoration decoration;
Adam Barth's avatar
Adam Barth committed
217

218 219
  /// The decoration to paint in front of the child.
  final Decoration foregroundDecoration;
Adam Barth's avatar
Adam Barth committed
220

221
  /// Empty space to surround the decoration.
222
  final EdgeInsets margin;
Adam Barth's avatar
Adam Barth committed
223

224
  /// Empty space to inscribe inside the decoration.
225
  final EdgeInsets padding;
226 227 228 229 230 231 232 233 234 235

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

236
  @override
237
  _AnimatedContainerState createState() => new _AnimatedContainerState();
238

239
  @override
240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258
  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');
  }
259 260 261
}

class _AnimatedContainerState extends AnimatedWidgetBaseState<AnimatedContainer> {
262 263 264
  BoxConstraintsTween _constraints;
  DecorationTween _decoration;
  DecorationTween _foregroundDecoration;
265 266
  EdgeInsetsTween _margin;
  EdgeInsetsTween _padding;
267 268 269 270
  Matrix4Tween _transform;
  Tween<double> _width;
  Tween<double> _height;

271
  @override
272
  void forEachTween(TweenVisitor<dynamic> visitor) {
273
    // TODO(ianh): Use constructor tear-offs when it becomes possible
274 275 276
    _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));
277 278
    _margin = visitor(_margin, config.margin, (dynamic value) => new EdgeInsetsTween(begin: value));
    _padding = visitor(_padding, config.padding, (dynamic value) => new EdgeInsetsTween(begin: value));
279 280 281
    _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
282 283
  }

284
  @override
Adam Barth's avatar
Adam Barth committed
285 286 287
  Widget build(BuildContext context) {
    return new Container(
      child: config.child,
288 289 290 291 292 293 294 295
      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
296 297
    );
  }
Hixie's avatar
Hixie committed
298

299
  @override
Hixie's avatar
Hixie committed
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318
  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
319
}
Ian Hickson's avatar
Ian Hickson committed
320 321

/// Animated version of [Positioned] which automatically transitions the child's
322
/// position over a given duration whenever the given position changes.
Ian Hickson's avatar
Ian Hickson committed
323 324
///
/// Only works if it's the child of a [Stack].
325
class AnimatedPositioned extends ImplicitlyAnimatedWidget {
Ian Hickson's avatar
Ian Hickson committed
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
  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);

356
  /// The widget below this widget in the tree.
Ian Hickson's avatar
Ian Hickson committed
357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382
  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;

383
  @override
Ian Hickson's avatar
Ian Hickson committed
384 385 386 387
  _AnimatedPositionedState createState() => new _AnimatedPositionedState();
}

class _AnimatedPositionedState extends AnimatedWidgetBaseState<AnimatedPositioned> {
388 389 390 391 392 393 394
  Tween<double> _left;
  Tween<double> _top;
  Tween<double> _right;
  Tween<double> _bottom;
  Tween<double> _width;
  Tween<double> _height;

395
  @override
396
  void forEachTween(TweenVisitor<dynamic> visitor) {
Ian Hickson's avatar
Ian Hickson committed
397
    // TODO(ianh): Use constructor tear-offs when it becomes possible
398 399 400 401 402 403
    _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
404 405
  }

406
  @override
Ian Hickson's avatar
Ian Hickson committed
407 408 409
  Widget build(BuildContext context) {
    return new Positioned(
      child: config.child,
410 411 412 413 414 415
      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
416 417 418
    );
  }

419
  @override
Ian Hickson's avatar
Ian Hickson committed
420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435
  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');
  }
}