implicit_animations.dart 13.8 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 31 32
/// 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
33

34
  EdgeInsets lerp(double t) => EdgeInsets.lerp(begin, end, t);
Adam Barth's avatar
Adam Barth committed
35 36
}

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
/// An abstract widget for building widgets that gradually change their
54
/// values over a period of time.
55
abstract class AnimatedWidgetBase extends StatefulWidget {
56
  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<AnimatedWidgetBase> 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
    _controller.duration = config.duration;
    if (_constructTweens()) {
108
      forEachTween((Tween<dynamic> tween, dynamic targetValue, TweenConstructor<dynamic> constructor) {
109 110
        _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
  bool _shouldAnimateTween(Tween<dynamic> tween, dynamic targetValue) {
135
    return targetValue != (tween.end ?? tween.begin);
136 137
  }

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

146 147
  bool _constructTweens() {
    bool shouldStartAnimation = false;
148
    forEachTween((Tween<dynamic> 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<dynamic> 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
  /// Empty space to surround the decoration.
218
  final EdgeInsets margin;
Adam Barth's avatar
Adam Barth committed
219

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

  /// 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
  BoxConstraintsTween _constraints;
  DecorationTween _decoration;
  DecorationTween _foregroundDecoration;
259 260
  EdgeInsetsTween _margin;
  EdgeInsetsTween _padding;
261 262 263 264
  Matrix4Tween _transform;
  Tween<double> _width;
  Tween<double> _height;

265
  void forEachTween(TweenVisitor<dynamic> visitor) {
266
    // TODO(ianh): Use constructor tear-offs when it becomes possible
267 268 269
    _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));
270 271
    _margin = visitor(_margin, config.margin, (dynamic value) => new EdgeInsetsTween(begin: value));
    _padding = visitor(_padding, config.padding, (dynamic value) => new EdgeInsetsTween(begin: value));
272 273 274
    _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
  Tween<double> _left;
  Tween<double> _top;
  Tween<double> _right;
  Tween<double> _bottom;
  Tween<double> _width;
  Tween<double> _height;

384
  void forEachTween(TweenVisitor<dynamic> 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');
  }
}