diff --git a/packages/flutter/lib/src/widgets/container.dart b/packages/flutter/lib/src/widgets/container.dart index 09f64b336c..3199352e6e 100644 --- a/packages/flutter/lib/src/widgets/container.dart +++ b/packages/flutter/lib/src/widgets/container.dart @@ -16,6 +16,10 @@ import 'image.dart'; /// not. /// /// Commonly used with [BoxDecoration]. +/// +/// See also: +/// +/// * [DecoratedBoxTransition], the version of this class that animates on the [decoration] property. class DecoratedBox extends SingleChildRenderObjectWidget { /// Creates a widget that paints a [Decoration]. /// diff --git a/packages/flutter/lib/src/widgets/implicit_animations.dart b/packages/flutter/lib/src/widgets/implicit_animations.dart index 7bfc8e0b50..990cb015c9 100644 --- a/packages/flutter/lib/src/widgets/implicit_animations.dart +++ b/packages/flutter/lib/src/widgets/implicit_animations.dart @@ -233,7 +233,8 @@ abstract class AnimatedWidgetBaseState exten /// This class is useful for generating simple implicit transitions between /// different parameters to [Container] with its internal /// [AnimationController]. For more complex animations, you'll likely want to -/// use a subclass of [Transition] or use your own [AnimationController]. +/// use a subclass of [Transition] such as the [DecoratedBoxTransition] or use +/// your own [AnimationController]. class AnimatedContainer extends ImplicitlyAnimatedWidget { /// Creates a container that animates its parameters implicitly. /// diff --git a/packages/flutter/lib/src/widgets/transitions.dart b/packages/flutter/lib/src/widgets/transitions.dart index 81546264dc..65cac3cc83 100644 --- a/packages/flutter/lib/src/widgets/transitions.dart +++ b/packages/flutter/lib/src/widgets/transitions.dart @@ -8,6 +8,7 @@ import 'package:flutter/foundation.dart'; import 'package:vector_math/vector_math_64.dart' show Matrix4; import 'basic.dart'; +import 'container.dart'; import 'framework.dart'; export 'package:flutter/rendering.dart' show RelativeRect; @@ -388,6 +389,52 @@ class RelativePositionedTransition extends AnimatedWidget { } } +/// Animated version of a [DecoratedBox] that animates the different properties +/// of its [Decoration]. +/// +/// See also: +/// +/// * [DecoratedBox], which also draws a [Decoration] but is not animated. +/// * [AnimatedContainer], a more full-featured container that also animates on +/// decoration using an internal animation. +class DecoratedBoxTransition extends AnimatedWidget { + /// Creates an animated [DecorationBox] whose [Decoration] animation updates + /// the widget. + /// + /// The [decoration] and [position] cannot be null. + /// + /// See also: + /// + /// * [new DecoratedBox]. + DecoratedBoxTransition({ + Key key, + @required this.decoration, + this.position: DecorationPosition.background, + @required this.child, + }) : super(key: key, listenable: decoration); + + /// Animation of the decoration to paint. + /// + /// Can be created using a [DecorationTween] interpolating typically between + /// two [BoxDecoration]. + final Animation decoration; + + /// Whether to paint the box decoration behind or in front of the child. + final DecorationPosition position; + + /// The widget below this widget in the tree. + final Widget child; + + @override + Widget build(BuildContext context) { + return new DecoratedBox( + decoration: decoration.value, + position: position, + child: child, + ); + } +} + /// A builder that builds a widget given a child. /// /// The child should typically be part of the returned widget tree. diff --git a/packages/flutter/test/widgets/transitions_test.dart b/packages/flutter/test/widgets/transitions_test.dart index c92c0a6dac..686173c9f5 100644 --- a/packages/flutter/test/widgets/transitions_test.dart +++ b/packages/flutter/test/widgets/transitions_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; void main() { @@ -13,4 +14,132 @@ void main() { ); expect(widget.toString, isNot(throwsException)); }); + + group('ContainerTransition test', () { + final DecorationTween decorationTween = new DecorationTween( + begin: new BoxDecoration( + backgroundColor: const Color(0xFFFFFFFF), + border: new Border.all( + color: const Color(0xFF000000), + style: BorderStyle.solid, + width: 4.0, + ), + borderRadius: BorderRadius.zero, + shape: BoxShape.rectangle, + boxShadow: const [const BoxShadow( + color: const Color(0x66000000), + blurRadius: 10.0, + spreadRadius: 4.0, + )], + ), + end: new BoxDecoration( + backgroundColor: const Color(0xFF000000), + border: new Border.all( + color: const Color(0xFF202020), + style: BorderStyle.solid, + width: 1.0, + ), + borderRadius: new BorderRadius.circular(10.0), + shape: BoxShape.rectangle, + // No shadow. + ), + ); + + AnimationController controller; + + setUp(() { + controller = new AnimationController(vsync: const TestVSync()); + }); + + testWidgets( + 'decoration test', + (WidgetTester tester) async { + final DecoratedBoxTransition transitionUnderTest = + new DecoratedBoxTransition( + decoration: decorationTween.animate(controller), + child: const Text("Doesn't matter"), + ); + + await tester.pumpWidget(transitionUnderTest); + RenderDecoratedBox actualBox = + tester.renderObject(find.byType(DecoratedBox)); + BoxDecoration actualDecoration = actualBox.decoration; + + expect(actualDecoration.backgroundColor, const Color(0xFFFFFFFF)); + expect(actualDecoration.boxShadow[0].blurRadius, 10.0); + expect(actualDecoration.boxShadow[0].spreadRadius, 4.0); + expect(actualDecoration.boxShadow[0].color, const Color(0x66000000)); + + controller.value = 0.5; + + await tester.pump(); + actualBox = tester.renderObject(find.byType(DecoratedBox)); + actualDecoration = actualBox.decoration; + + expect(actualDecoration.backgroundColor, const Color(0xFF7F7F7F)); + expect(actualDecoration.border.left.width, 2.5); + expect(actualDecoration.border.left.style, BorderStyle.solid); + expect(actualDecoration.border.left.color, const Color(0xFF101010)); + expect(actualDecoration.borderRadius, new BorderRadius.circular(5.0)); + expect(actualDecoration.shape, BoxShape.rectangle); + expect(actualDecoration.boxShadow[0].blurRadius, 5.0); + expect(actualDecoration.boxShadow[0].spreadRadius, 2.0); + // Scaling a shadow doesn't change the color. + expect(actualDecoration.boxShadow[0].color, const Color(0x66000000)); + + controller.value = 1.0; + + await tester.pump(); + actualBox = tester.renderObject(find.byType(DecoratedBox)); + actualDecoration = actualBox.decoration; + + expect(actualDecoration.backgroundColor, const Color(0xFF000000)); + expect(actualDecoration.boxShadow, null); + } + ); + + testWidgets('animations work with curves test', (WidgetTester tester) async { + final Animation curvedDecorationAnimation = + decorationTween.animate(new CurvedAnimation( + parent: controller, + curve: Curves.easeOut, + )); + + final DecoratedBoxTransition transitionUnderTest = + new DecoratedBoxTransition( + decoration: curvedDecorationAnimation, + position: DecorationPosition.foreground, + child: const Text("Doesn't matter"), + ); + + await tester.pumpWidget(transitionUnderTest); + RenderDecoratedBox actualBox = + tester.renderObject(find.byType(DecoratedBox)); + BoxDecoration actualDecoration = actualBox.decoration; + + expect(actualDecoration.backgroundColor, const Color(0xFFFFFFFF)); + expect(actualDecoration.boxShadow[0].blurRadius, 10.0); + expect(actualDecoration.boxShadow[0].spreadRadius, 4.0); + expect(actualDecoration.boxShadow[0].color, const Color(0x66000000)); + + controller.value = 0.5; + + await tester.pump(); + actualBox = tester.renderObject(find.byType(DecoratedBox)); + actualDecoration = actualBox.decoration; + + // Same as the test above but the values should be much closer to the + // tween's end values given the easeOut curve. + expect(actualDecoration.backgroundColor, const Color(0xFF505050)); + expect(actualDecoration.border.left.width, closeTo(1.9, 0.1)); + expect(actualDecoration.border.left.style, BorderStyle.solid); + expect(actualDecoration.border.left.color, const Color(0xFF151515)); + expect(actualDecoration.borderRadius.topLeft.x, closeTo(6.8, 0.1)); + expect(actualDecoration.shape, BoxShape.rectangle); + expect(actualDecoration.boxShadow[0].blurRadius, closeTo(3.1, 0.1)); + expect(actualDecoration.boxShadow[0].spreadRadius, closeTo(1.2, 0.1)); + // Scaling a shadow doesn't change the color. + expect(actualDecoration.boxShadow[0].color, const Color(0x66000000)); + }); + }); }