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.
This commit is contained in:
parent
4aba536aa9
commit
0fbe3ce92c
@ -151,12 +151,14 @@ class ExpansionPanelList extends StatelessWidget {
|
|||||||
child: new Column(
|
child: new Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
header,
|
header,
|
||||||
new _AnimatedCrossFade(
|
new AnimatedCrossFade(
|
||||||
firstChild: new Container(height: 0.0),
|
firstChild: new Container(height: 0.0),
|
||||||
secondChild: children[i].body,
|
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,
|
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
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
200
packages/flutter/lib/src/widgets/animated_cross_fade.dart
Normal file
200
packages/flutter/lib/src/widgets/animated_cross_fade.dart
Normal file
@ -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<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`.
|
/// To use, import `package:flutter/widgets.dart`.
|
||||||
library widgets;
|
library widgets;
|
||||||
|
|
||||||
|
export 'src/widgets/animated_cross_fade.dart';
|
||||||
export 'src/widgets/animated_size.dart';
|
export 'src/widgets/animated_size.dart';
|
||||||
export 'src/widgets/app.dart';
|
export 'src/widgets/app.dart';
|
||||||
export 'src/widgets/auto_layout.dart';
|
export 'src/widgets/auto_layout.dart';
|
||||||
|
57
packages/flutter/test/widget/animated_cross_fade_test.dart
Normal file
57
packages/flutter/test/widget/animated_cross_fade_test.dart
Normal file
@ -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));
|
||||||
|
});
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user