// 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. import 'package:flutter/animation.dart'; import 'basic.dart'; import 'framework.dart'; import 'package:vector_math/vector_math_64.dart'; /// An animated value that interpolates [BoxConstraint]s. class AnimatedBoxConstraintsValue extends AnimatedValue<BoxConstraints> { AnimatedBoxConstraintsValue(BoxConstraints begin, { BoxConstraints end, Curve curve, Curve reverseCurve }) : super(begin, end: end, curve: curve, reverseCurve: reverseCurve); BoxConstraints lerp(double t) => BoxConstraints.lerp(begin, end, t); } /// An animated value that interpolates [Decoration]s. class AnimatedDecorationValue extends AnimatedValue<Decoration> { AnimatedDecorationValue(Decoration begin, { Decoration end, Curve curve, Curve reverseCurve }) : super(begin, end: end, curve: curve, reverseCurve: reverseCurve); Decoration lerp(double t) { if (begin == null && end == null) return null; if (end == null) return begin.lerpTo(end, t); return end.lerpFrom(begin, t); } } /// An animated value that interpolates [EdgeDims]. class AnimatedEdgeDimsValue extends AnimatedValue<EdgeDims> { AnimatedEdgeDimsValue(EdgeDims begin, { EdgeDims end, Curve curve, Curve reverseCurve }) : super(begin, end: end, curve: curve, reverseCurve: reverseCurve); EdgeDims lerp(double t) => EdgeDims.lerp(begin, end, t); } /// An animated value that interpolates [Matrix4]s. /// /// Currently this class works only for translations. class AnimatedMatrix4Value extends AnimatedValue<Matrix4> { AnimatedMatrix4Value(Matrix4 begin, { Matrix4 end, Curve curve, Curve reverseCurve }) : super(begin, end: end, curve: curve, reverseCurve: reverseCurve); 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); } } /// An abstract widget for building components that gradually change their /// values over a period of time. abstract class AnimatedWidgetBase extends StatefulComponent { AnimatedWidgetBase({ Key key, this.curve: Curves.linear, this.duration }) : super(key: key) { assert(curve != null); assert(duration != null); } /// The curve to apply when animating the parameters of this container. final Curve curve; /// The duration over which to animate the parameters of this container. final Duration duration; AnimatedWidgetBaseState createState(); void debugFillDescription(List<String> description) { super.debugFillDescription(description); description.add('duration: ${duration.inMilliseconds}ms'); } } typedef AnimatedValue<T> VariableConstructor<T>(T targetValue); typedef AnimatedValue<T> VariableVisitor<T>(AnimatedValue<T> variable, T targetValue, VariableConstructor<T> constructor); abstract class AnimatedWidgetBaseState<T extends AnimatedWidgetBase> extends State<T> { Performance _performanceController; PerformanceView _performance; void initState() { super.initState(); _performanceController = new Performance( duration: config.duration, debugLabel: '${config.toStringShort()}' ); _updateCurve(); _configAllVariables(); } void didUpdateConfig(T oldConfig) { if (config.curve != oldConfig.curve) _updateCurve(); _performanceController.duration = config.duration; if (_configAllVariables()) { forEachVariable((AnimatedValue variable, dynamic targetValue, VariableConstructor<T> constructor) { _updateBeginValue(variable); return variable; }); _performanceController.progress = 0.0; _performanceController.play(); } } void _updateCurve() { _performance?.removeListener(_updateAllVariables); if (config.curve != null) _performance = new CurvedPerformance(_performanceController, curve: config.curve); else _performance = _performanceController; _performance.addListener(_updateAllVariables); } void dispose() { _performanceController.stop(); super.dispose(); } void _updateVariable(Animatable variable) { if (variable != null) _performance.updateVariable(variable); } void _updateAllVariables() { setState(() { forEachVariable((AnimatedValue variable, dynamic targetValue, VariableConstructor<T> constructor) { _updateVariable(variable); return variable; }); }); } bool _updateEndValue(AnimatedValue variable, dynamic targetValue) { if (targetValue == variable.end) return false; variable.end = targetValue; return true; } void _updateBeginValue(AnimatedValue variable) { variable?.begin = variable.value; } bool _configAllVariables() { bool startAnimation = false; 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; } /// 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); } /// 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); } final Widget child; /// Additional constraints to apply to the child. final BoxConstraints constraints; /// The decoration to paint behind the child. final Decoration decoration; /// The decoration to paint in front of the child. final Decoration foregroundDecoration; /// Empty space to surround the decoration. final EdgeDims margin; /// 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(); 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'); } } 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)); } 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 ); } 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'); } } /// Animated version of [Positioned] which automatically transitions the child's /// position over a given duration whenever the given position changes. /// /// 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'); } }