Commit 2be1eb48 authored by Adam Barth's avatar Adam Barth

DropDownMenu should animation away when interrupted (#4485)

If you tap outside the drop down menu while its animating in, we should
animate it away smoothly. Previously, we jumped to the reverseCurve,
which made the menu disappear immediately. Now we hold the animations as
state, which means we keep their _curveDirection property and don't
switch curves unless the animation actually finishes.

Also, fix a subtle bug in CurvedAnimation whereby we'd never set the
_curveDirection if we didn't see a status change in the parent
animation. Now we initialize _curveDirection based on the current value
of the parent's status.

Fixes #4379
parent 4e3e40a1
......@@ -300,8 +300,8 @@ class ReverseAnimation extends Animation<double>
/// [CurvedAnimation] is useful when you want to apply a non-linear [Curve] to
/// an animation object wrapped in the [CurvedAnimation].
///
/// For example, the following code snippet shows how you can apply a curve to a linear animation
/// produced by an [AnimationController]:
/// For example, the following code snippet shows how you can apply a curve to a
/// linear animation produced by an [AnimationController]:
///
/// ``` dart
/// final AnimationController controller =
......@@ -309,9 +309,10 @@ class ReverseAnimation extends Animation<double>
/// final CurvedAnimation animation =
/// new CurvedAnimation(parent: controller, curve: Curves.ease);
///```
/// Depending on the given curve, the output of the [CurvedAnimation] could have a wider range
/// than its input. For example, elastic curves such as [Curves.elasticIn] will significantly
/// overshoot or undershoot the default range of 0.0 to 1.0.
/// Depending on the given curve, the output of the [CurvedAnimation] could have
/// a wider range than its input. For example, elastic curves such as
/// [Curves.elasticIn] will significantly overshoot or undershoot the default
/// range of 0.0 to 1.0.
///
/// If you want to apply a [Curve] to a [Tween], consider using [CurveTween].
class CurvedAnimation extends Animation<double> with AnimationWithParentMixin<double> {
......@@ -325,7 +326,8 @@ class CurvedAnimation extends Animation<double> with AnimationWithParentMixin<do
}) {
assert(parent != null);
assert(curve != null);
parent.addStatusListener(_handleStatusChanged);
_updateCurveDirection(parent.status);
parent.addStatusListener(_updateCurveDirection);
}
/// The animation to which this animation applies a curve.
......@@ -337,6 +339,16 @@ class CurvedAnimation extends Animation<double> with AnimationWithParentMixin<do
/// The curve to use in the reverse direction.
///
/// If the parent animation changes direction without first reaching the
/// [AnimationStatus.completed] or [AnimationStatus.dismissed] status, the
/// [CurvedAnimation] stays on the same curve (albeit in the opposite
/// direction) to avoid visual discontinuities.
///
/// If you use a non-null [reverseCurve], you might want to hold this object
/// in a [State] object rather than recreating it each time your widget builds
/// in order to take advantage of the state in this object that avoids visual
/// discontinuities.
///
/// If this field is null, uses [curve] in both directions.
Curve reverseCurve;
......@@ -347,7 +359,7 @@ class CurvedAnimation extends Animation<double> with AnimationWithParentMixin<do
/// a animation is used to animate.
AnimationStatus _curveDirection;
void _handleStatusChanged(AnimationStatus status) {
void _updateCurveDirection(AnimationStatus status) {
switch (status) {
case AnimationStatus.dismissed:
case AnimationStatus.completed:
......
......@@ -70,14 +70,41 @@ class _DropDownMenuPainter extends CustomPainter {
}
}
class _DropDownMenu<T> extends StatusTransitionWidget {
class _DropDownMenu<T> extends StatefulWidget {
_DropDownMenu({
Key key,
_DropDownRoute<T> route
}) : route = route, super(key: key, animation: route.animation);
}) : route = route, super(key: key);
final _DropDownRoute<T> route;
@override
_DropDownMenuState<T> createState() => new _DropDownMenuState<T>();
}
class _DropDownMenuState<T> extends State<_DropDownMenu<T>> {
CurvedAnimation _fadeOpacity;
CurvedAnimation _resize;
@override
void initState() {
super.initState();
// We need to hold these animations as state because of their curve
// direction. When the route's animation reverses, if we were to recreate
// the CurvedAnimation objects in build, we'd lose
// CurvedAnimation._curveDirection.
_fadeOpacity = new CurvedAnimation(
parent: config.route.animation,
curve: const Interval(0.0, 0.25),
reverseCurve: const Interval(0.75, 1.0)
);
_resize = new CurvedAnimation(
parent: config.route.animation,
curve: const Interval(0.25, 0.5),
reverseCurve: const Step(0.0)
);
}
@override
Widget build(BuildContext context) {
// The menu is shown in three stages (unit timing in brackets):
......@@ -89,17 +116,17 @@ class _DropDownMenu<T> extends StatusTransitionWidget {
// When the menu is dismissed we just fade the entire thing out
// in the first 0.25s.
final _DropDownRoute<T> route = config.route;
final double unit = 0.5 / (route.items.length + 1.5);
final List<Widget> children = <Widget>[];
for (int itemIndex = 0; itemIndex < route.items.length; ++itemIndex) {
CurvedAnimation opacity;
Interval reverseCurve = const Interval(0.75, 1.0);
if (itemIndex == route.selectedIndex) {
opacity = new CurvedAnimation(parent: route.animation, curve: const Step(0.0), reverseCurve: reverseCurve);
opacity = new CurvedAnimation(parent: route.animation, curve: const Step(0.0));
} else {
final double start = (0.5 + (itemIndex + 1) * unit).clamp(0.0, 1.0);
final double end = (start + 1.5 * unit).clamp(0.0, 1.0);
opacity = new CurvedAnimation(parent: route.animation, curve: new Interval(start, end), reverseCurve: reverseCurve);
opacity = new CurvedAnimation(parent: route.animation, curve: new Interval(start, end));
}
children.add(new FadeTransition(
opacity: opacity,
......@@ -117,21 +144,13 @@ class _DropDownMenu<T> extends StatusTransitionWidget {
}
return new FadeTransition(
opacity: new CurvedAnimation(
parent: route.animation,
curve: const Interval(0.0, 0.25),
reverseCurve: const Interval(0.75, 1.0)
),
opacity: _fadeOpacity,
child: new CustomPaint(
painter: new _DropDownMenuPainter(
color: Theme.of(context).canvasColor,
elevation: route.elevation,
selectedIndex: route.selectedIndex,
resize: new CurvedAnimation(
parent: route.animation,
curve: const Interval(0.25, 0.5),
reverseCurve: const Step(0.0)
)
resize: _resize
),
child: new Material(
type: MaterialType.transparency,
......@@ -456,8 +475,7 @@ class _DropDownButtonState<T> extends State<DropDownButton<T>> {
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
final TextStyle style = _textStyle;
if (_currentRoute != null)
_currentRoute.style = style;
_currentRoute?.style = style;
Widget result = new DefaultTextStyle(
style: style,
child: new Row(
......
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