From 0fbe3ce92c04748eccbfcef992dc2c94a9ddf41a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Drago=C8=99=20Tiselice?= Date: Tue, 30 Aug 2016 15:02:36 -0700 Subject: [PATCH] Added AnimatedCrossFade. (#5650) Added a widget that cross fades two children while animating the size of the parent based on the children's interpolated sizes. --- .../lib/src/material/expansion_panels.dart | 138 +----------- .../lib/src/widgets/animated_cross_fade.dart | 200 ++++++++++++++++++ packages/flutter/lib/widgets.dart | 1 + .../test/widget/animated_cross_fade_test.dart | 57 +++++ 4 files changed, 263 insertions(+), 133 deletions(-) create mode 100644 packages/flutter/lib/src/widgets/animated_cross_fade.dart create mode 100644 packages/flutter/test/widget/animated_cross_fade_test.dart diff --git a/packages/flutter/lib/src/material/expansion_panels.dart b/packages/flutter/lib/src/material/expansion_panels.dart index f20f4801d5..8bad2041a8 100644 --- a/packages/flutter/lib/src/material/expansion_panels.dart +++ b/packages/flutter/lib/src/material/expansion_panels.dart @@ -151,12 +151,14 @@ class ExpansionPanelList extends StatelessWidget { child: new Column( children: [ header, - new _AnimatedCrossFade( + new AnimatedCrossFade( firstChild: new Container(height: 0.0), secondChild: children[i].body, - crossFadeState: _isChildExpanded(i) ? _CrossFadeState.showSecond : _CrossFadeState.showFirst, + firstCurve: new Interval(0.0, 0.6, curve: Curves.fastOutSlowIn), + secondCurve: new Interval(0.4, 1.0, curve: Curves.fastOutSlowIn), + sizeCurve: Curves.fastOutSlowIn, + crossFadeState: _isChildExpanded(i) ? CrossFadeState.showSecond : CrossFadeState.showFirst, duration: animationDuration, - curve: Curves.fastOutSlowIn ) ] ) @@ -173,133 +175,3 @@ class ExpansionPanelList extends StatelessWidget { ); } } - -// The child that is shown will fade in, and while the other will fade out. -enum _CrossFadeState { - showFirst, - showSecond -} - -// A widget that cross-fades between two children and animates its bottom while -// clipping the children. -class _AnimatedCrossFade extends StatefulWidget { - _AnimatedCrossFade({ - Key key, - this.firstChild, - this.secondChild, - this.crossFadeState, - this.duration, - this.curve - }) : super(key: key); - - final Widget firstChild; - final Widget secondChild; - final _CrossFadeState crossFadeState; - final Duration duration; - final Curve curve; - - @override - _AnimatedCrossFadeState createState() => new _AnimatedCrossFadeState(); -} - -class _AnimatedCrossFadeState extends State<_AnimatedCrossFade> { - AnimationController _controller; - Animation _firstAnimation; - Animation _secondAnimation; - - @override - void initState() { - super.initState(); - _controller = new AnimationController(duration: config.duration); - _firstAnimation = new Tween( - begin: 1.0, - end: 0.0 - ).animate( - new CurvedAnimation( - parent: _controller, - curve: new Interval(0.0, 0.6, curve: config.curve) - ) - ); - _secondAnimation = new CurvedAnimation( - parent: _controller, - curve: new Interval(0.4, 1.0, curve: config.curve.flipped) - ); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - void didUpdateConfig(_AnimatedCrossFade oldConfig) { - super.didUpdateConfig(oldConfig); - if (config.crossFadeState != oldConfig.crossFadeState) { - switch (config.crossFadeState) { - case _CrossFadeState.showFirst: - _controller.reverse(); - break; - case _CrossFadeState.showSecond: - _controller.forward(); - break; - } - } - } - - @override - Widget build(BuildContext context) { - Stack stack; - - if (_controller.status == AnimationStatus.completed || - _controller.status == AnimationStatus.forward) { - stack = new Stack( - overflow: Overflow.visible, - children: [ - new FadeTransition( - opacity: _secondAnimation, - child: config.secondChild - ), - new Positioned( - left: 0.0, - top: 0.0, - right: 0.0, - child: new FadeTransition( - opacity: _firstAnimation, - child: config.firstChild - ) - ) - ] - ); - } else { - stack = new Stack( - overflow: Overflow.visible, - children: [ - new FadeTransition( - opacity: _firstAnimation, - child: config.firstChild - ), - new Positioned( - left: 0.0, - top: 0.0, - right: 0.0, - child: new FadeTransition( - opacity: _secondAnimation, - child: config.secondChild - ) - ) - ] - ); - } - - return new ClipRect( - child: new AnimatedSize( - key: new ValueKey(config.key), - alignment: FractionalOffset.topCenter, - duration: config.duration, - curve: config.curve, - child: stack - ) - ); - } -} diff --git a/packages/flutter/lib/src/widgets/animated_cross_fade.dart b/packages/flutter/lib/src/widgets/animated_cross_fade.dart new file mode 100644 index 0000000000..e2259b8ba1 --- /dev/null +++ b/packages/flutter/lib/src/widgets/animated_cross_fade.dart @@ -0,0 +1,200 @@ +// Copyright 2016 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/rendering.dart'; +import 'package:meta/meta.dart'; + +import 'animated_size.dart'; +import 'basic.dart'; +import 'framework.dart'; +import 'transitions.dart'; + +/// Specifies which of the children to show. See [AnimatedCrossFade]. +/// +/// The child that is shown will fade in, while the other will fade out. +enum CrossFadeState { + /// Show the first child and hide the second. + showFirst, + /// Show the second child and hide the first. + showSecond +} + +/// A widget that cross-fades between two children and animates itself between +/// their sizes. The animation is controlled through the [crossFadeState] +/// parameter. [firstCurve] and [secondCurve] represent the opacity curves of +/// the two children. Note that [firstCurve] is inverted, i.e. it fades out when +/// providing a growing curve like [Curves.linear]. [sizeCurve] is the curve +/// used to animated between the size of the fading out child and the size of +/// the fading in child. +/// +/// This widget is intended to be used to fade a pair of widgets with the same +/// width. In the case where the two children have different heights, the +/// animation crops overflowing children during the animation by aligning their +/// top edge, which means that the bottom will be clipped. +class AnimatedCrossFade extends StatefulWidget { + /// Creates a cross fade animation widget. + /// + /// The [duration] of the animation is the same for all components (fade in, + /// fade out, and size), and you can pass [Interval]s instead of [Curve]s in + /// order to have finer control, e.g., creating an overlap between the fades. + AnimatedCrossFade({ + Key key, + this.firstChild, + this.secondChild, + this.firstCurve: Curves.linear, + this.secondCurve: Curves.linear, + this.sizeCurve: Curves.linear, + @required this.crossFadeState, + @required this.duration + }) : super(key: key) { + assert(this.firstCurve != null); + assert(this.secondCurve != null); + assert(this.sizeCurve != null); + } + + /// The child that is visible when [crossFadeState] is [showFirst]. It fades + /// out when transitioning from [showFirst] to [showSecond] and fades in + /// otherwise. + final Widget firstChild; + + /// The child that is visible when [crossFadeState] is [showSecond]. It fades + /// in when transitioning from [showFirst] to [showSecond] and fades out + /// otherwise. + final Widget secondChild; + + /// This field identifies the child that will be shown when the animation has + /// completed. + final CrossFadeState crossFadeState; + + /// The duration of the whole orchestrated animation. + final Duration duration; + + /// The fade curve of the first child. + final Curve firstCurve; + + /// The fade curve of the second child. + final Curve secondCurve; + + /// The curve of the animation between the two children's sizes. + final Curve sizeCurve; + + @override + _AnimatedCrossFadeState createState() => new _AnimatedCrossFadeState(); +} + +class _AnimatedCrossFadeState extends State { + _AnimatedCrossFadeState() : super(); + + AnimationController _controller; + Animation _firstAnimation; + Animation _secondAnimation; + + Animation _initAnimation(Curve curve, bool inverted) { + final CurvedAnimation animation = new CurvedAnimation( + parent: _controller, + curve: curve + ); + + return inverted ? new Tween( + begin: 1.0, + end: 0.0 + ).animate(animation) : animation; + } + + @override + void initState() { + super.initState(); + _controller = new AnimationController(duration: config.duration); + _firstAnimation = _initAnimation(config.firstCurve, true); + _secondAnimation = _initAnimation(config.secondCurve, false); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + void didUpdateConfig(AnimatedCrossFade oldConfig) { + super.didUpdateConfig(oldConfig); + + if (config.crossFadeState != oldConfig.crossFadeState) { + switch (config.crossFadeState) { + case CrossFadeState.showFirst: + _controller.reverse(); + break; + case CrossFadeState.showSecond: + _controller.forward(); + break; + } + } + + if (config.duration != oldConfig.duration) + _controller.duration = config.duration; + if (config.firstCurve != oldConfig.firstCurve) { + _firstAnimation = _initAnimation(config.firstCurve, true); + } + if (config.secondCurve != oldConfig.secondCurve) { + _secondAnimation = _initAnimation(config.secondCurve, false); + } + } + + @override + Widget build(BuildContext context) { + List children; + + if (_controller.status == AnimationStatus.completed || + _controller.status == AnimationStatus.forward) { + children = [ + new FadeTransition( + opacity: _secondAnimation, + child: config.secondChild + ), + new Positioned( + // TODO(dragostis): Add a way to crop from top right for + // right-to-left languages. + left: 0.0, + top: 0.0, + right: 0.0, + child: new FadeTransition( + opacity: _firstAnimation, + child: config.firstChild + ) + ) + ]; + } else { + children = [ + new FadeTransition( + opacity: _firstAnimation, + child: config.firstChild + ), + new Positioned( + // TODO(dragostis): Add a way to crop from top right for + // right-to-left languages. + left: 0.0, + top: 0.0, + right: 0.0, + child: new FadeTransition( + opacity: _secondAnimation, + child: config.secondChild + ) + ) + ]; + } + + return new ClipRect( + child: new AnimatedSize( + key: new ValueKey(config.key), + alignment: FractionalOffset.topCenter, + duration: config.duration, + curve: config.sizeCurve, + child: new Stack( + overflow: Overflow.visible, + children: children + ) + ) + ); + } +} diff --git a/packages/flutter/lib/widgets.dart b/packages/flutter/lib/widgets.dart index 684159137c..5698acb51e 100644 --- a/packages/flutter/lib/widgets.dart +++ b/packages/flutter/lib/widgets.dart @@ -8,6 +8,7 @@ /// To use, import `package:flutter/widgets.dart`. library widgets; +export 'src/widgets/animated_cross_fade.dart'; export 'src/widgets/animated_size.dart'; export 'src/widgets/app.dart'; export 'src/widgets/auto_layout.dart'; diff --git a/packages/flutter/test/widget/animated_cross_fade_test.dart b/packages/flutter/test/widget/animated_cross_fade_test.dart new file mode 100644 index 0000000000..32f16b4645 --- /dev/null +++ b/packages/flutter/test/widget/animated_cross_fade_test.dart @@ -0,0 +1,57 @@ +// Copyright 2016 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_test/flutter_test.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; + +void main() { + testWidgets('AnimatedCrossFade test', (WidgetTester tester) async { + await tester.pumpWidget( + new Center( + child: new AnimatedCrossFade( + firstChild: new SizedBox( + width: 100.0, + height: 100.0 + ), + secondChild: new SizedBox( + width: 200.0, + height: 200.0 + ), + duration: const Duration(milliseconds: 200), + crossFadeState: CrossFadeState.showFirst + ) + ) + ); + + expect(find.byType(FadeTransition), findsNWidgets(2)); + RenderBox box = tester.renderObject(find.byType(AnimatedCrossFade)); + expect(box.size.width, equals(100.0)); + expect(box.size.height, equals(100.0)); + + await tester.pumpWidget( + new Center( + child: new AnimatedCrossFade( + firstChild: new SizedBox( + width: 100.0, + height: 100.0 + ), + secondChild: new SizedBox( + width: 200.0, + height: 200.0 + ), + duration: const Duration(milliseconds: 200), + crossFadeState: CrossFadeState.showSecond + ) + ) + ); + + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.byType(FadeTransition), findsNWidgets(2)); + box = tester.renderObject(find.byType(AnimatedCrossFade)); + expect(box.size.width, equals(150.0)); + expect(box.size.height, equals(150.0)); + }); +}