From c88ca5dbdcd1dd1c863fb6e469d2669876b71f2b Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Fri, 2 Oct 2015 14:38:36 -0700 Subject: [PATCH] Add AnimatedContainer This widget is used in Material and Drawer. We don't currently support animating towards null, but we can add that in a future patch. --- packages/flutter/lib/src/rendering/box.dart | 54 +++++ .../lib/src/widgets/animated_container.dart | 226 ++++++++++++++++++ packages/flutter/lib/src/widgets/basic.dart | 9 +- packages/flutter/lib/src/widgets/drawer.dart | 7 +- .../flutter/lib/src/widgets/material.dart | 7 +- packages/flutter/lib/widgets.dart | 1 + .../test/widget/animated_container_test.dart | 48 ++++ 7 files changed, 343 insertions(+), 9 deletions(-) create mode 100644 packages/flutter/lib/src/widgets/animated_container.dart create mode 100644 packages/unit/test/widget/animated_container_test.dart diff --git a/packages/flutter/lib/src/rendering/box.dart b/packages/flutter/lib/src/rendering/box.dart index 91ba89c548..4baf5c12e8 100644 --- a/packages/flutter/lib/src/rendering/box.dart +++ b/packages/flutter/lib/src/rendering/box.dart @@ -178,6 +178,60 @@ class BoxConstraints extends Constraints { (minHeight <= size.height) && (size.height <= math.max(minHeight, maxHeight)); } + BoxConstraints operator*(double other) { + return new BoxConstraints( + minWidth: minWidth * other, + maxWidth: maxWidth * other, + minHeight: minHeight * other, + maxHeight: maxHeight * other + ); + } + + BoxConstraints operator/(double other) { + return new BoxConstraints( + minWidth: minWidth / other, + maxWidth: maxWidth / other, + minHeight: minHeight / other, + maxHeight: maxHeight / other + ); + } + + BoxConstraints operator~/(double other) { + return new BoxConstraints( + minWidth: (minWidth ~/ other).toDouble(), + maxWidth: (maxWidth ~/ other).toDouble(), + minHeight: (minHeight ~/ other).toDouble(), + maxHeight: (maxHeight ~/ other).toDouble() + ); + } + + BoxConstraints operator%(double other) { + return new BoxConstraints( + minWidth: minWidth % other, + maxWidth: maxWidth % other, + minHeight: minHeight % other, + maxHeight: maxHeight % other + ); + } + + /// Linearly interpolate between two BoxConstraints + /// + /// If either is null, this function interpolates from [BoxConstraints.zero]. + static BoxConstraints lerp(BoxConstraints a, BoxConstraints b, double t) { + if (a == null && b == null) + return null; + if (a == null) + return b * t; + if (b == null) + return a * (1.0 - t); + return new BoxConstraints( + minWidth: sky.lerpDouble(a.minWidth, b.minWidth, t), + maxWidth: sky.lerpDouble(a.maxWidth, b.maxWidth, t), + minHeight: sky.lerpDouble(a.minHeight, b.minHeight, t), + maxHeight: sky.lerpDouble(a.maxHeight, b.maxHeight, t) + ); + } + bool operator ==(other) { if (identical(this, other)) return true; diff --git a/packages/flutter/lib/src/widgets/animated_container.dart b/packages/flutter/lib/src/widgets/animated_container.dart new file mode 100644 index 0000000000..291023e9f2 --- /dev/null +++ b/packages/flutter/lib/src/widgets/animated_container.dart @@ -0,0 +1,226 @@ +// 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:sky/animation.dart'; +import 'package:sky/src/widgets/basic.dart'; +import 'package:sky/src/widgets/framework.dart'; + +import 'package:vector_math/vector_math_64.dart'; + +class AnimatedBoxConstraintsValue extends AnimatedValue { + AnimatedBoxConstraintsValue(BoxConstraints begin, { BoxConstraints end, Curve curve: linear }) + : super(begin, end: end, curve: curve); + + BoxConstraints lerp(double t) => BoxConstraints.lerp(begin, end, t); +} + +class AnimatedBoxDecorationValue extends AnimatedValue { + AnimatedBoxDecorationValue(BoxDecoration begin, { BoxDecoration end, Curve curve: linear }) + : super(begin, end: end, curve: curve); + + BoxDecoration lerp(double t) => BoxDecoration.lerp(begin, end, t); +} + +class AnimatedEdgeDimsValue extends AnimatedValue { + AnimatedEdgeDimsValue(EdgeDims begin, { EdgeDims end, Curve curve: linear }) + : super(begin, end: end, curve: curve); + + EdgeDims lerp(double t) => EdgeDims.lerp(begin, end, t); +} + +class AnimatedMatrix4Value extends AnimatedValue { + AnimatedMatrix4Value(Matrix4 begin, { Matrix4 end, Curve curve: linear }) + : super(begin, end: end, curve: curve); + + 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); + } +} + +class AnimatedContainer extends StatefulComponent { + AnimatedContainer({ + Key key, + this.child, + this.constraints, + this.decoration, + this.foregroundDecoration, + this.margin, + this.padding, + this.transform, + this.width, + this.height, + this.curve: linear, + this.duration + }) : super(key: key) { + assert(margin == null || margin.isNonNegative); + assert(padding == null || padding.isNonNegative); + assert(curve != null); + assert(duration != null); + } + + final Widget child; + final BoxConstraints constraints; + final BoxDecoration decoration; + final BoxDecoration foregroundDecoration; + final EdgeDims margin; + final EdgeDims padding; + final Matrix4 transform; + final double width; + final double height; + + final Curve curve; + final Duration duration; + + AnimatedContainerState createState() => new AnimatedContainerState(); +} + +class AnimatedContainerState extends State { + AnimatedBoxConstraintsValue _constraints; + AnimatedBoxDecorationValue _decoration; + AnimatedBoxDecorationValue _foregroundDecoration; + AnimatedEdgeDimsValue _margin; + AnimatedEdgeDimsValue _padding; + AnimatedMatrix4Value _transform; + AnimatedValue _width; + AnimatedValue _height; + + AnimationPerformance _performance; + + void initState() { + super.initState(); + _performance = new AnimationPerformance(duration: config.duration) + ..timing = new AnimationTiming(curve: config.curve) + ..addListener(_updateAllVariables); + _configAllVariables(); + } + + void didUpdateConfig(AnimatedContainer oldConfig) { + _performance + ..duration = config.duration + ..timing.curve = config.curve; + if (_configAllVariables()) { + _performance.progress = 0.0; + _performance.play(); + } + } + + void dispose() { + _performance.stop(); + super.dispose(); + } + + void _updateVariable(AnimatedVariable variable) { + if (variable != null) + _performance.updateVariable(variable); + } + + void _updateAllVariables() { + setState(() { + _updateVariable(_constraints); + _updateVariable(_constraints); + _updateVariable(_decoration); + _updateVariable(_foregroundDecoration); + _updateVariable(_margin); + _updateVariable(_padding); + _updateVariable(_transform); + _updateVariable(_width); + _updateVariable(_height); + }); + } + + bool _configVariable(AnimatedValue variable, dynamic targetValue) { + dynamic currentValue = variable.value; + variable.end = targetValue; + variable.begin = currentValue; + return currentValue != targetValue; + } + + bool _configAllVariables() { + bool needsAnimation = false; + if (config.constraints != null) { + _constraints ??= new AnimatedBoxConstraintsValue(config.constraints); + if (_configVariable(_constraints, config.constraints)) + needsAnimation = true; + } else { + _constraints = null; + } + + if (config.decoration != null) { + _decoration ??= new AnimatedBoxDecorationValue(config.decoration); + if (_configVariable(_decoration, config.decoration)) + needsAnimation = true; + } else { + _decoration = null; + } + + if (config.foregroundDecoration != null) { + _foregroundDecoration ??= new AnimatedBoxDecorationValue(config.foregroundDecoration); + if (_configVariable(_foregroundDecoration, config.foregroundDecoration)) + needsAnimation = true; + } else { + _foregroundDecoration = null; + } + + if (config.margin != null) { + _margin ??= new AnimatedEdgeDimsValue(config.margin); + if (_configVariable(_margin, config.margin)) + needsAnimation = true; + } else { + _margin = null; + } + + if (config.padding != null) { + _padding ??= new AnimatedEdgeDimsValue(config.padding); + if (_configVariable(_padding, config.padding)) + needsAnimation = true; + } else { + _padding = null; + } + + if (config.transform != null) { + _transform ??= new AnimatedMatrix4Value(config.transform); + if (_configVariable(_transform, config.transform)) + needsAnimation = true; + } else { + _transform = null; + } + + if (config.width != null) { + _width ??= new AnimatedValue(config.width); + if (_configVariable(_width, config.width)) + needsAnimation = true; + } else { + _width = null; + } + + if (config.height != null) { + _height ??= new AnimatedValue(config.height); + if (_configVariable(_height, config.height)) + needsAnimation = true; + } else { + _height = null; + } + + return needsAnimation; + } + + 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 + ); + } +} diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index 66bc316745..e5f087760a 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -22,6 +22,7 @@ export 'package:sky/rendering.dart' show FlexAlignItems, FlexDirection, FlexJustifyContent, + Matrix4, Offset, Paint, Path, @@ -392,11 +393,11 @@ class Container extends StatelessComponent { this.constraints, this.decoration, this.foregroundDecoration, - this.width, - this.height, this.margin, this.padding, - this.transform + this.transform, + this.width, + this.height }) : super(key: key) { assert(margin == null || margin.isNonNegative); assert(padding == null || padding.isNonNegative); @@ -940,4 +941,4 @@ class KeyedSubtree extends StatelessComponent { final Widget child; Widget build(BuildContext context) => child; -} \ No newline at end of file +} diff --git a/packages/flutter/lib/src/widgets/drawer.dart b/packages/flutter/lib/src/widgets/drawer.dart index 04435cccca..edee253af6 100644 --- a/packages/flutter/lib/src/widgets/drawer.dart +++ b/packages/flutter/lib/src/widgets/drawer.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'package:sky/animation.dart'; import 'package:sky/material.dart'; +import 'package:sky/src/widgets/animated_container.dart'; import 'package:sky/src/widgets/framework.dart'; import 'package:sky/src/widgets/basic.dart'; import 'package:sky/src/widgets/gesture_detector.dart'; @@ -102,9 +103,9 @@ class DrawerState extends State { Widget content = new SlideTransition( performance: _performance.view, position: new AnimatedValue(_kClosedPosition, end: _kOpenPosition), - // TODO(abarth): Use AnimatedContainer - child: new Container( - // behavior: implicitlyAnimate(const Duration(milliseconds: 200)), + child: new AnimatedContainer( + curve: ease, + duration: const Duration(milliseconds: 200), decoration: new BoxDecoration( backgroundColor: Theme.of(context).canvasColor, boxShadow: shadows[config.level]), diff --git a/packages/flutter/lib/src/widgets/material.dart b/packages/flutter/lib/src/widgets/material.dart index 02ea4ce80e..eb79290ec9 100644 --- a/packages/flutter/lib/src/widgets/material.dart +++ b/packages/flutter/lib/src/widgets/material.dart @@ -2,8 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:sky/animation.dart'; import 'package:sky/painting.dart'; import 'package:sky/material.dart'; +import 'package:sky/src/widgets/animated_container.dart'; import 'package:sky/src/widgets/basic.dart'; import 'package:sky/src/widgets/framework.dart'; import 'package:sky/src/widgets/theme.dart'; @@ -61,10 +63,11 @@ class Material extends StatelessComponent { ); } } - // TODO(abarth): This should use AnimatedContainer. return new DefaultTextStyle( style: Theme.of(context).text.body1, - child: new Container( + child: new AnimatedContainer( + curve: ease, + duration: const Duration(milliseconds: 200), decoration: new BoxDecoration( backgroundColor: getBackgroundColor(context), borderRadius: edges[type], diff --git a/packages/flutter/lib/widgets.dart b/packages/flutter/lib/widgets.dart index 3e7f257d57..e3fa205fc9 100644 --- a/packages/flutter/lib/widgets.dart +++ b/packages/flutter/lib/widgets.dart @@ -6,6 +6,7 @@ library widgets; export 'src/widgets/animated_component.dart'; +export 'src/widgets/animated_container.dart'; export 'src/widgets/app.dart'; export 'src/widgets/basic.dart'; export 'src/widgets/binding.dart'; diff --git a/packages/unit/test/widget/animated_container_test.dart b/packages/unit/test/widget/animated_container_test.dart new file mode 100644 index 0000000000..b98cee45e9 --- /dev/null +++ b/packages/unit/test/widget/animated_container_test.dart @@ -0,0 +1,48 @@ +import 'package:sky/rendering.dart'; +import 'package:sky/widgets.dart'; +import 'package:test/test.dart'; + +import 'widget_tester.dart'; + +void main() { + test('AnimatedContainer control test', () { + testWidgets((WidgetTester tester) { + GlobalKey key = new GlobalKey(); + + BoxDecoration decorationA = new BoxDecoration( + backgroundColor: new Color(0xFF00FF00) + ); + + BoxDecoration decorationB = new BoxDecoration( + backgroundColor: new Color(0xFF0000FF) + ); + + tester.pumpWidget( + new AnimatedContainer( + key: key, + duration: const Duration(milliseconds: 200), + decoration: decorationA + ) + ); + + RenderDecoratedBox box = key.currentState.context.findRenderObject(); + expect(box.decoration.backgroundColor, equals(decorationA.backgroundColor)); + + tester.pumpWidget( + new AnimatedContainer( + key: key, + duration: const Duration(milliseconds: 200), + decoration: decorationB + ) + ); + + expect(key.currentState.context.findRenderObject(), equals(box)); + expect(box.decoration.backgroundColor, equals(decorationA.backgroundColor)); + + tester.pump(const Duration(seconds: 1)); + + expect(box.decoration.backgroundColor, equals(decorationB.backgroundColor)); + + }); + }); +}