blob: 8aba05efca60a7f39f48a75d11f91c231de88bfa [file] [log] [blame]
// 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/animation.dart';
import 'basic.dart';
import 'framework.dart';
import 'ticker_provider.dart';
import 'transitions.dart';
// Internal representation of a child that, now or in the past, was set on the
// AnimatedSwitcher.child field, but is now in the process of
// transitioning. The internal representation includes fields that we don't want
// to expose to the public API (like the controller).
class _AnimatedSwitcherChildEntry {
_AnimatedSwitcherChildEntry({
@required this.animation,
@required this.transition,
@required this.controller,
@required this.widgetChild,
}) : assert(animation != null),
assert(transition != null),
assert(controller != null);
final Animation<double> animation;
// The currently built transition for this child.
Widget transition;
// The animation controller for the child's transition.
final AnimationController controller;
// The widget's child at the time this entry was created or updated.
Widget widgetChild;
}
/// Signature for builders used to generate custom transitions for
/// [AnimatedSwitcher].
///
/// The [child] should be transitioning in when the [animation] is running in
/// the forward direction.
///
/// The function should return a widget which wraps the given [child]. It may
/// also use the [animation] to inform its transition. It must not return null.
typedef Widget AnimatedSwitcherTransitionBuilder(Widget child, Animation<double> animation);
/// Signature for builders used to generate custom layouts for
/// [AnimatedSwitcher].
///
/// The function should return a widget which contains the given children, laid
/// out as desired. It must not return null.
typedef Widget AnimatedSwitcherLayoutBuilder(List<Widget> children);
/// A widget that by default does a [FadeTransition] between a new widget and
/// the widget previously set on the [AnimatedSwitcher] as a child.
///
/// If they are swapped fast enough (i.e. before [duration] elapses), more than
/// one previous child can exist and be transitioning out while the newest one
/// is transitioning in.
///
/// If the "new" child is the same widget type as the "old" child, but with
/// different parameters, then [AnimatedSwitcher] will *not* do a
/// transition between them, since as far as the framework is concerned, they
/// are the same widget, and the existing widget can be updated with the new
/// parameters. If you wish to force the transition to occur, set a [Key]
/// (typically a [ValueKey] taking any widget data that would change the visual
/// appearance of the widget) on each child widget that you wish to be
/// considered unique.
///
/// ## Sample code
///
/// ```dart
/// class ClickCounter extends StatefulWidget {
/// const ClickCounter({Key key}) : super(key: key);
///
/// @override
/// _ClickCounterState createState() => new _ClickCounterState();
/// }
///
/// class _ClickCounterState extends State<ClickCounter> {
/// int _count = 0;
///
/// @override
/// Widget build(BuildContext context) {
/// return new Material(
/// child: Column(
/// mainAxisAlignment: MainAxisAlignment.center,
/// children: <Widget>[
/// new AnimatedSwitcher(
/// duration: const Duration(milliseconds: 200),
/// transitionBuilder: (Widget child, Animation<double> animation) {
/// return new ScaleTransition(child: child, scale: animation);
/// },
/// child: new Text(
/// '$_count',
/// // Must have this key to build a unique widget when _count changes.
/// key: new ValueKey<int>(_count),
/// textScaleFactor: 3.0,
/// ),
/// ),
/// new RaisedButton(
/// child: new Text('Click!'),
/// onPressed: () {
/// setState(() {
/// _count += 1;
/// });
/// },
/// ),
/// ],
/// ),
/// );
/// }
/// }
/// ```
///
/// See also:
///
/// * [AnimatedCrossFade], which only fades between two children, but also
/// interpolates their sizes, and is reversible.
/// * [FadeTransition] which [AnimatedSwitcher] uses to perform the transition.
class AnimatedSwitcher extends StatefulWidget {
/// Creates an [AnimatedSwitcher].
///
/// The [duration], [transitionBuilder], [layoutBuilder], [switchInCurve], and
/// [switchOutCurve] parameters must not be null.
const AnimatedSwitcher({
Key key,
this.child,
@required this.duration,
this.switchInCurve: Curves.linear,
this.switchOutCurve: Curves.linear,
this.transitionBuilder: AnimatedSwitcher.defaultTransitionBuilder,
this.layoutBuilder: AnimatedSwitcher.defaultLayoutBuilder,
}) : assert(duration != null),
assert(switchInCurve != null),
assert(switchOutCurve != null),
assert(transitionBuilder != null),
assert(layoutBuilder != 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 duration of the transition from the old [child] value to the new one.
final Duration duration;
/// The animation curve to use when transitioning in [child].
final Curve switchInCurve;
/// The animation curve to use when transitioning the previous [child] out.
final Curve switchOutCurve;
/// A function that wraps the new [child] with an animation that transitions
/// the [child] in when the animation runs in the forward direction and out
/// when the animation runs in the reverse direction.
///
/// The default is [AnimatedSwitcher.defaultTransitionBuilder].
///
/// See also:
///
/// * [AnimatedSwitcherTransitionBuilder] for more information about
/// how a transition builder should function.
final AnimatedSwitcherTransitionBuilder transitionBuilder;
/// A function that wraps all of the children that are transitioning out, and
/// the [child] that's transitioning in, with a widget that lays all of them
/// out.
///
/// The default is [AnimatedSwitcher.defaultLayoutBuilder].
///
/// See also:
///
/// * [AnimatedSwitcherLayoutBuilder] for more information about
/// how a layout builder should function.
final AnimatedSwitcherLayoutBuilder layoutBuilder;
@override
_AnimatedSwitcherState createState() => new _AnimatedSwitcherState();
/// The default transition algorithm used by [AnimatedSwitcher].
///
/// The new child is given a [FadeTransition] which increases opacity as
/// the animation goes from 0.0 to 1.0, and decreases when the animation is
/// reversed.
///
/// The default value for the [transitionBuilder], an
/// [AnimatedSwitcherTransitionBuilder] function.
static Widget defaultTransitionBuilder(Widget child, Animation<double> animation) {
return new FadeTransition(
opacity: animation,
child: child,
);
}
/// The default layout algorithm used by [AnimatedSwitcher].
///
/// The new child is placed in a [Stack] that sizes itself to match the
/// largest of the child or a previous child. The children are centered on
/// each other.
///
/// This is the default value for [layoutBuilder]. It implements
/// [AnimatedSwitcherLayoutBuilder].
static Widget defaultLayoutBuilder(List<Widget> children) {
return new Stack(
children: children,
alignment: Alignment.center,
);
}
}
class _AnimatedSwitcherState extends State<AnimatedSwitcher> with TickerProviderStateMixin {
final Set<_AnimatedSwitcherChildEntry> _children = new Set<_AnimatedSwitcherChildEntry>();
_AnimatedSwitcherChildEntry _currentChild;
@override
void initState() {
super.initState();
_addEntry(animate: false);
}
Widget _generateTransition(Animation<double> animation) {
return new KeyedSubtree(
key: new UniqueKey(),
child: widget.transitionBuilder(widget.child, animation),
);
}
_AnimatedSwitcherChildEntry _newEntry({
@required AnimationController controller,
@required Animation<double> animation,
}) {
final _AnimatedSwitcherChildEntry entry = new _AnimatedSwitcherChildEntry(
widgetChild: widget.child,
transition: _generateTransition(animation),
animation: animation,
controller: controller,
);
animation.addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.dismissed) {
assert(_children.contains(entry));
setState(() {
_children.remove(entry);
});
controller.dispose();
}
});
return entry;
}
void _addEntry({@required bool animate}) {
if (widget.child == null) {
if (animate && _currentChild != null) {
_currentChild.controller.reverse();
assert(!_children.contains(_currentChild));
_children.add(_currentChild);
}
_currentChild = null;
return;
}
final AnimationController controller = new AnimationController(
duration: widget.duration,
vsync: this,
);
if (animate) {
if (_currentChild != null) {
_currentChild.controller.reverse();
assert(!_children.contains(_currentChild));
_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,
);
_currentChild = _newEntry(controller: controller, animation: animation);
}
@override
void dispose() {
if (_currentChild != null) {
_currentChild.controller.dispose();
}
for (_AnimatedSwitcherChildEntry child in _children) {
child.controller.dispose();
}
super.dispose();
}
bool get hasNewChild => widget.child != null;
bool get hasOldChild => _currentChild != null;
@override
void didUpdateWidget(AnimatedSwitcher oldWidget) {
super.didUpdateWidget(oldWidget);
if (hasNewChild != hasOldChild || hasNewChild &&
!Widget.canUpdate(widget.child, _currentChild.widgetChild)) {
_addEntry(animate: true);
} else {
if (_currentChild != null) {
_currentChild.widgetChild = widget.child;
_currentChild.transition = _generateTransition(_currentChild.animation);
}
}
}
@override
Widget build(BuildContext context) {
final List<Widget> children = _children.map<Widget>(
(_AnimatedSwitcherChildEntry entry) {
return entry.transition;
},
).toList();
if (_currentChild != null) {
children.add(_currentChild.transition);
}
return widget.layoutBuilder(children);
}
}