Commit 0fbe3ce9 authored by Dragoș Tiselice's avatar Dragoș Tiselice Committed by GitHub

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.
parent 4aba536a
......@@ -151,12 +151,14 @@ class ExpansionPanelList extends StatelessWidget {
child: new Column(
children: <Widget>[
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<double> _firstAnimation;
Animation<double> _secondAnimation;
@override
void initState() {
super.initState();
_controller = new AnimationController(duration: config.duration);
_firstAnimation = new Tween<double>(
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: <Widget>[
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: <Widget>[
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<Key>(config.key),
alignment: FractionalOffset.topCenter,
duration: config.duration,
curve: config.curve,
child: stack
)
);
}
}
// 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<AnimatedCrossFade> {
_AnimatedCrossFadeState() : super();
AnimationController _controller;
Animation<double> _firstAnimation;
Animation<double> _secondAnimation;
Animation<double> _initAnimation(Curve curve, bool inverted) {
final CurvedAnimation animation = new CurvedAnimation(
parent: _controller,
curve: curve
);
return inverted ? new Tween<double>(
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<Widget> children;
if (_controller.status == AnimationStatus.completed ||
_controller.status == AnimationStatus.forward) {
children = <Widget>[
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 = <Widget>[
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<Key>(config.key),
alignment: FractionalOffset.topCenter,
duration: config.duration,
curve: config.sizeCurve,
child: new Stack(
overflow: Overflow.visible,
children: children
)
)
);
}
}
......@@ -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';
......
// 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));
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment