implicit_animations.dart 14.4 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 'package:flutter/animation.dart';
6 7 8

import 'basic.dart';
import 'framework.dart';
Adam Barth's avatar
Adam Barth committed
9 10 11

import 'package:vector_math/vector_math_64.dart';

12
/// An animated value that interpolates [BoxConstraint]s.
Adam Barth's avatar
Adam Barth committed
13
class AnimatedBoxConstraintsValue extends AnimatedValue<BoxConstraints> {
14 15
  AnimatedBoxConstraintsValue(BoxConstraints begin, { BoxConstraints end, Curve curve, Curve reverseCurve })
    : super(begin, end: end, curve: curve, reverseCurve: reverseCurve);
Adam Barth's avatar
Adam Barth committed
16 17 18 19

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

20
/// An animated value that interpolates [Decoration]s.
21 22
class AnimatedDecorationValue extends AnimatedValue<Decoration> {
  AnimatedDecorationValue(Decoration begin, { Decoration end, Curve curve, Curve reverseCurve })
23
    : super(begin, end: end, curve: curve, reverseCurve: reverseCurve);
Adam Barth's avatar
Adam Barth committed
24

25 26 27 28 29 30 31
  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
32 33
}

34
/// An animated value that interpolates [EdgeDims].
Adam Barth's avatar
Adam Barth committed
35
class AnimatedEdgeDimsValue extends AnimatedValue<EdgeDims> {
36 37
  AnimatedEdgeDimsValue(EdgeDims begin, { EdgeDims end, Curve curve, Curve reverseCurve })
    : super(begin, end: end, curve: curve, reverseCurve: reverseCurve);
Adam Barth's avatar
Adam Barth committed
38 39 40 41

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

42 43 44
/// An animated value that interpolates [Matrix4]s.
///
/// Currently this class works only for translations.
Adam Barth's avatar
Adam Barth committed
45
class AnimatedMatrix4Value extends AnimatedValue<Matrix4> {
46 47
  AnimatedMatrix4Value(Matrix4 begin, { Matrix4 end, Curve curve, Curve reverseCurve })
    : super(begin, end: end, curve: curve, reverseCurve: reverseCurve);
Adam Barth's avatar
Adam Barth committed
48 49 50 51 52 53 54 55 56 57 58

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

59 60 61 62
/// 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
63
    Key key,
64
    this.curve: Curves.linear,
Adam Barth's avatar
Adam Barth committed
65 66 67
    this.duration
  }) : super(key: key) {
    assert(curve != null);
Ian Hickson's avatar
Ian Hickson committed
68
    assert(duration != null);
Adam Barth's avatar
Adam Barth committed
69 70
  }

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

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

77
  AnimatedWidgetBaseState createState();
78 79 80 81 82

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

85 86
typedef AnimatedValue<T> VariableConstructor<T>(T targetValue);
typedef AnimatedValue<T> VariableVisitor<T>(AnimatedValue<T> variable, T targetValue, VariableConstructor<T> constructor);
Adam Barth's avatar
Adam Barth committed
87

88
abstract class AnimatedWidgetBaseState<T extends AnimatedWidgetBase> extends State<T> {
89 90
  Performance _performanceController;
  PerformanceView _performance;
Adam Barth's avatar
Adam Barth committed
91 92 93

  void initState() {
    super.initState();
94 95 96 97 98
    _performanceController = new Performance(
      duration: config.duration,
      debugLabel: '${config.toStringShort()}'
    );
    _updateCurve();
Adam Barth's avatar
Adam Barth committed
99 100 101
    _configAllVariables();
  }

102
  void didUpdateConfig(T oldConfig) {
103 104 105
    if (config.curve != oldConfig.curve)
      _updateCurve();
    _performanceController.duration = config.duration;
Adam Barth's avatar
Adam Barth committed
106
    if (_configAllVariables()) {
107 108 109
      forEachVariable((AnimatedValue variable, dynamic targetValue, VariableConstructor<T> constructor) {
        _updateBeginValue(variable); return variable;
      });
110 111
      _performanceController.progress = 0.0;
      _performanceController.play();
Adam Barth's avatar
Adam Barth committed
112 113 114
    }
  }

115 116 117 118 119 120 121 122 123
  void _updateCurve() {
    _performance?.removeListener(_updateAllVariables);
    if (config.curve != null)
      _performance = new CurvedPerformance(_performanceController, curve: config.curve);
    else
      _performance = _performanceController;
    _performance.addListener(_updateAllVariables);
  }

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

129
  void _updateVariable(Animatable variable) {
Adam Barth's avatar
Adam Barth committed
130 131 132 133 134 135
    if (variable != null)
      _performance.updateVariable(variable);
  }

  void _updateAllVariables() {
    setState(() {
136 137 138
      forEachVariable((AnimatedValue variable, dynamic targetValue, VariableConstructor<T> constructor) {
        _updateVariable(variable); return variable;
      });
Adam Barth's avatar
Adam Barth committed
139 140 141
    });
  }

142
  bool _updateEndValue(AnimatedValue variable, dynamic targetValue) {
143 144
    if (targetValue == variable.end)
      return false;
Adam Barth's avatar
Adam Barth committed
145
    variable.end = targetValue;
146 147 148 149 150
    return true;
  }

  void _updateBeginValue(AnimatedValue variable) {
    variable?.begin = variable.value;
Adam Barth's avatar
Adam Barth committed
151 152 153
  }

  bool _configAllVariables() {
154
    bool startAnimation = false;
155 156 157 158 159 160 161 162 163 164 165 166
    forEachVariable((AnimatedValue variable, dynamic targetValue, VariableConstructor<T> constructor) { 
      if (targetValue != null) {
        variable ??= constructor(targetValue);
        if (_updateEndValue(variable, targetValue))
          startAnimation = true;
      } else {
        variable = null;
      }
      return variable;
    });
    return startAnimation;
  }
Adam Barth's avatar
Adam Barth committed
167

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

186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211
/// 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
/// likely want to use a subclass of [Transition] or control a [Performance]
/// yourself.
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
212

213
  final Widget child;
Adam Barth's avatar
Adam Barth committed
214

215 216
  /// Additional constraints to apply to the child.
  final BoxConstraints constraints;
Adam Barth's avatar
Adam Barth committed
217

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

221 222
  /// The decoration to paint in front of the child.
  final Decoration foregroundDecoration;
Adam Barth's avatar
Adam Barth committed
223

224 225
  /// Empty space to surround the decoration.
  final EdgeDims margin;
Adam Barth's avatar
Adam Barth committed
226

227 228 229 230 231 232 233 234 235 236 237 238 239
  /// 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();
240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259

  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');
  }
260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281
}

class _AnimatedContainerState extends AnimatedWidgetBaseState<AnimatedContainer> {
  AnimatedBoxConstraintsValue _constraints;
  AnimatedDecorationValue _decoration;
  AnimatedDecorationValue _foregroundDecoration;
  AnimatedEdgeDimsValue _margin;
  AnimatedEdgeDimsValue _padding;
  AnimatedMatrix4Value _transform;
  AnimatedValue<double> _width;
  AnimatedValue<double> _height;

  void forEachVariable(VariableVisitor visitor) {
    // TODO(ianh): Use constructor tear-offs when it becomes possible
    _constraints = visitor(_constraints, config.constraints, (dynamic value) => new AnimatedBoxConstraintsValue(value));
    _decoration = visitor(_decoration, config.decoration, (dynamic value) => new AnimatedDecorationValue(value));
    _foregroundDecoration = visitor(_foregroundDecoration, config.foregroundDecoration, (dynamic value) => new AnimatedDecorationValue(value));
    _margin = visitor(_margin, config.margin, (dynamic value) => new AnimatedEdgeDimsValue(value));
    _padding = visitor(_padding, config.padding, (dynamic value) => new AnimatedEdgeDimsValue(value));
    _transform = visitor(_transform, config.transform, (dynamic value) => new AnimatedMatrix4Value(value));
    _width = visitor(_width, config.width, (dynamic value) => new AnimatedValue<double>(value));
    _height = visitor(_height, config.height, (dynamic value) => new AnimatedValue<double>(value));
Adam Barth's avatar
Adam Barth committed
282 283 284 285 286 287 288 289 290 291 292 293 294 295 296
  }

  Widget build(BuildContext context) {
    return new Container(
      child: config.child,
      constraints: _constraints?.value,
      decoration: _decoration?.value,
      foregroundDecoration: _foregroundDecoration?.value,
      margin: _margin?.value,
      padding: _padding?.value,
      transform: _transform?.value,
      width: _width?.value,
      height: _height?.value
    );
  }
Hixie's avatar
Hixie committed
297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316

  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
317
}
Ian Hickson's avatar
Ian Hickson committed
318 319

/// Animated version of [Positioned] which automatically transitions the child's
320
/// position over a given duration whenever the given position changes.
Ian Hickson's avatar
Ian Hickson committed
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 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 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 427 428
///
/// 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> {
  AnimatedValue<double> _left;
  AnimatedValue<double> _top;
  AnimatedValue<double> _right;
  AnimatedValue<double> _bottom;
  AnimatedValue<double> _width;
  AnimatedValue<double> _height;

  void forEachVariable(VariableVisitor visitor) {
    // TODO(ianh): Use constructor tear-offs when it becomes possible
    _left = visitor(_left, config.left, (dynamic value) => new AnimatedValue<double>(value));
    _top = visitor(_top, config.top, (dynamic value) => new AnimatedValue<double>(value));
    _right = visitor(_right, config.right, (dynamic value) => new AnimatedValue<double>(value));
    _bottom = visitor(_bottom, config.bottom, (dynamic value) => new AnimatedValue<double>(value));
    _width = visitor(_width, config.width, (dynamic value) => new AnimatedValue<double>(value));
    _height = visitor(_height, config.height, (dynamic value) => new AnimatedValue<double>(value));
  }

  Widget build(BuildContext context) {
    return new Positioned(
      child: config.child,
      left: _left?.value,
      top: _top?.value,
      right: _right?.value,
      bottom: _bottom?.value,
      width: _width?.value,
      height: _height?.value
    );
  }

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