Unverified Commit 6c896ae1 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Add AnimatedChildSwitcher widget (#16192)

The AnimatedChildSwitcher widget (originally authored by Hixie), will cross-fade between a new child, and a previous child (or children, if the switch happens faster than the fade finishes).

It's a good candidate for places where a widget will be added/removed from a slot, and you want a nice transition to occur.
parent f3c742c8
// 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/foundation.dart';
import 'package:flutter/animation.dart';
import 'basic.dart';
import 'framework.dart';
import 'ticker_provider.dart';
import 'transitions.dart';
class _AnimatedChildSwitcherChildEntry {
_AnimatedChildSwitcherChildEntry(this.widget, this.controller, this.animation);
Widget widget;
final AnimationController controller;
final Animation<double> animation;
}
/// A widget that automatically does a [FadeTransition] between a new widget and
/// the widget previously set on the [AnimatedChildSwitcher] as a child.
///
/// More than one previous child can exist and be fading out while the newest
/// one is fading in if they are swapped fast enough (i.e. before [duration]
/// elapses).
///
/// See also:
///
/// * [AnimatedCrossFade], which only fades between two children, but also
/// interpolates their sizes, and is reversible.
/// * [FadeTransition] which [AnimatedChildSwitcher] uses to perform the transition.
class AnimatedChildSwitcher extends StatefulWidget {
/// The [duration], [switchInCurve], and [switchOutCurve] parameters must not
/// be null.
const AnimatedChildSwitcher({
Key key,
this.child,
this.switchInCurve: Curves.linear,
this.switchOutCurve: Curves.linear,
@required this.duration,
}) : assert(switchInCurve != null),
assert(switchOutCurve != null),
assert(duration != null),
super(key: key);
/// The current child widget to display. If there was a previous child,
/// then that child will be cross faded with this child using a
/// [FadeTransition] using the [switchInCurve].
///
/// If there was no previous child, then this child will fade in over the
/// [duration].
final Widget child;
/// The animation curve to use when fading in the current widget.
final Curve switchInCurve;
/// The animation curve to use when fading out the previous widgets.
final Curve switchOutCurve;
/// The duration over which to perform the cross fade using [FadeTransition].
final Duration duration;
@override
_AnimatedChildSwitcherState createState() => new _AnimatedChildSwitcherState();
}
class _AnimatedChildSwitcherState extends State<AnimatedChildSwitcher> with TickerProviderStateMixin {
final Set<_AnimatedChildSwitcherChildEntry> _children = new Set<_AnimatedChildSwitcherChildEntry>();
_AnimatedChildSwitcherChildEntry _currentChild;
@override
void initState() {
super.initState();
addEntry(false);
}
void addEntry(bool animate) {
final AnimationController controller = new AnimationController(
duration: widget.duration,
vsync: this,
);
if (animate) {
if (_currentChild != null) {
_currentChild.controller.reverse();
_children.add(_currentChild);
}
controller.forward();
} else {
assert(_currentChild == null);
assert(_children.isEmpty);
controller.value = 1.0;
}
final Animation<double> animation = new CurvedAnimation(
parent: controller,
curve: widget.switchInCurve,
reverseCurve: widget.switchOutCurve,
);
final _AnimatedChildSwitcherChildEntry entry = new _AnimatedChildSwitcherChildEntry(
widget.child,
controller,
animation,
);
animation.addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.dismissed) {
assert(_children.contains(entry));
setState(() {
_children.remove(entry);
});
controller.dispose();
}
});
_currentChild = entry;
}
@override
void dispose() {
if (_currentChild != null) {
_currentChild.controller.dispose();
}
for (_AnimatedChildSwitcherChildEntry child in _children) {
child.controller.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
if (widget.child != _currentChild.widget) {
addEntry(true);
}
final List<Widget> children = <Widget>[];
for (_AnimatedChildSwitcherChildEntry child in _children) {
children.add(
new FadeTransition(
opacity: child.animation,
child: child.widget,
),
);
}
if (_currentChild != null) {
children.add(
new FadeTransition(
opacity: _currentChild.animation,
child: _currentChild.widget,
),
);
}
return new Stack(
children: children,
alignment: Alignment.center,
);
}
}
......@@ -14,6 +14,7 @@ library widgets;
export 'package:vector_math/vector_math_64.dart' show Matrix4;
export 'src/widgets/animated_child_switcher.dart';
export 'src/widgets/animated_cross_fade.dart';
export 'src/widgets/animated_list.dart';
export 'src/widgets/animated_size.dart';
......
// Copyright 2018 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/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('AnimatedChildSwitcher fades in a new child.', (WidgetTester tester) async {
await tester.pumpWidget(
new AnimatedChildSwitcher(
duration: const Duration(milliseconds: 100),
child: new Container(color: const Color(0x00000000)),
switchInCurve: Curves.linear,
),
);
// First one just appears.
FadeTransition transition = tester.firstWidget(find.byType(FadeTransition));
expect(transition.opacity.value, equals(1.0));
await tester.pumpWidget(
new AnimatedChildSwitcher(
duration: const Duration(milliseconds: 100),
child: new Container(color: const Color(0xff000000)),
switchInCurve: Curves.linear,
),
);
// Second one cross-fades with the first.
await tester.pump(const Duration(milliseconds: 50));
transition = tester.firstWidget(find.byType(FadeTransition));
expect(transition.opacity.value, equals(0.5));
await tester.pumpAndSettle();
});
testWidgets("AnimatedChildSwitcher doesn't start any animations after dispose.", (WidgetTester tester) async {
await tester.pumpWidget(new AnimatedChildSwitcher(
duration: const Duration(milliseconds: 100),
child: new Container(color: const Color(0xff000000)),
switchInCurve: Curves.linear,
));
await tester.pump(const Duration(milliseconds: 50));
// Change the widget tree in the middle of the animation.
await tester.pumpWidget(new Container(color: const Color(0xffff0000)));
expect(await tester.pumpAndSettle(const Duration(milliseconds: 100)), equals(1));
});
}
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