871 lines
30 KiB
Dart
871 lines
30 KiB
Dart
// Copyright 2019 The Flutter 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/material.dart';
|
|
import 'package:flutter/scheduler.dart';
|
|
import 'extension_helper.dart';
|
|
|
|
/// Signature for a function that creates a [Widget] to be used within an
|
|
/// [OpenContainer].
|
|
///
|
|
/// The `action` callback provided to [OpenContainer.openBuilder] can be used
|
|
/// to open the container. The `action` callback provided to
|
|
/// [OpenContainer.closedBuilder] can be used to close the container again.
|
|
typedef OpenContainerBuilder = Widget Function(
|
|
BuildContext context,
|
|
VoidCallback action,
|
|
bool hide,
|
|
);
|
|
|
|
/// The [OpenContainer] widget's fade transition type.
|
|
///
|
|
/// This determines the type of fade transition that the incoming and outgoing
|
|
/// contents will use.
|
|
enum ContainerTransitionType {
|
|
/// Fades the incoming element in over the outgoing element.
|
|
fade,
|
|
|
|
/// First fades the outgoing element out, and starts fading the incoming
|
|
/// element in once the outgoing element has completely faded out.
|
|
fadeThrough,
|
|
}
|
|
|
|
/// A container that grows to fill the screen to reveal new content when tapped.
|
|
///
|
|
/// While the container is closed, it shows the [Widget] returned by
|
|
/// [closedBuilder]. When the container is tapped it grows to fill the entire
|
|
/// size of the surrounding [Navigator] while fading out the widget returned by
|
|
/// [closedBuilder] and fading in the widget returned by [openBuilder]. When the
|
|
/// container is closed again via the callback provided to [openBuilder] or via
|
|
/// Android's back button, the animation is reversed: The container shrinks back
|
|
/// to its original size while the widget returned by [openBuilder] is faded out
|
|
/// and the widget returned by [openBuilder] is faded back in.
|
|
///
|
|
/// By default, the container is in the closed state. During the transition from
|
|
/// closed to open and vice versa the widgets returned by the [openBuilder] and
|
|
/// [closedBuilder] exist in the tree at the same time. Therefore, the widgets
|
|
/// returned by these builders cannot include the same global key.
|
|
///
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [Transitions with animated containers](https://material.io/design/motion/choreography.html#transformation)
|
|
/// in the Material spec.
|
|
class OpenContainer extends StatefulWidget {
|
|
/// Creates an [OpenContainer].
|
|
///
|
|
/// All arguments except for [key] must not be null. The arguments
|
|
/// [closedBuilder] and [closedBuilder] are required.
|
|
const OpenContainer({
|
|
Key key,
|
|
this.closedColor = Colors.white,
|
|
this.openColor = Colors.white,
|
|
this.beginColor = Colors.white,
|
|
this.endColor = Colors.white,
|
|
this.closedElevation = 1.0,
|
|
this.openElevation = 4.0,
|
|
this.closedShape = const RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.all(Radius.circular(4.0)),
|
|
),
|
|
this.openShape = const RoundedRectangleBorder(),
|
|
@required this.closedBuilder,
|
|
@required this.openBuilder,
|
|
this.flightWidget,
|
|
this.flightWidgetSize,
|
|
this.playerRunning,
|
|
this.playerHeight,
|
|
this.tappable = true,
|
|
this.transitionDuration = const Duration(milliseconds: 300),
|
|
this.transitionType = ContainerTransitionType.fade,
|
|
}) : assert(closedColor != null),
|
|
assert(openColor != null),
|
|
assert(closedElevation != null),
|
|
assert(openElevation != null),
|
|
assert(closedShape != null),
|
|
assert(openShape != null),
|
|
assert(closedBuilder != null),
|
|
assert(openBuilder != null),
|
|
assert(tappable != null),
|
|
assert(transitionType != null),
|
|
super(key: key);
|
|
|
|
/// Background color of the container while it is closed.
|
|
///
|
|
/// When the container is opened, it will first transition from this color
|
|
/// to [Colors.white] and then transition from there to [openColor] in one
|
|
/// smooth animation. When the container is closed, it will transition back to
|
|
/// this color from [openColor] via [Colors.white].
|
|
///
|
|
/// Defaults to [Colors.white].
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [Material.color], which is used to implement this property.
|
|
///
|
|
final Color beginColor;
|
|
final Color endColor;
|
|
final Color closedColor;
|
|
final Widget flightWidget;
|
|
final double flightWidgetSize;
|
|
final bool playerRunning;
|
|
final double playerHeight;
|
|
|
|
/// Background color of the container while it is open.
|
|
///
|
|
/// When the container is closed, it will first transition from [closedColor]
|
|
/// to [Colors.white] and then transition from there to this color in one
|
|
/// smooth animation. When the container is closed, it will transition back to
|
|
/// [closedColor] from this color via [Colors.white].
|
|
///
|
|
/// Defaults to [Colors.white].
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [Material.color], which is used to implement this property.
|
|
final Color openColor;
|
|
|
|
/// Elevation of the container while it is closed.
|
|
///
|
|
/// When the container is opened, it will transition from this elevation to
|
|
/// [openElevation]. When the container is closed, it will transition back
|
|
/// from [openElevation] to this elevation.
|
|
///
|
|
/// Defaults to 1.0.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [Material.elevation], which is used to implement this property.
|
|
final double closedElevation;
|
|
|
|
/// Elevation of the container while it is open.
|
|
///
|
|
/// When the container is opened, it will transition to this elevation from
|
|
/// [closedElevation]. When the container is closed, it will transition back
|
|
/// from this elevation to [closedElevation].
|
|
///
|
|
/// Defaults to 4.0.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [Material.elevation], which is used to implement this property.
|
|
final double openElevation;
|
|
|
|
/// Shape of the container while it is closed.
|
|
///
|
|
/// When the container is opened it will transition from this shape to
|
|
/// [openShape]. When the container is closed, it will transition back to this
|
|
/// shape.
|
|
///
|
|
/// Defaults to a [RoundedRectangleBorder] with a [Radius.circular] of 4.0.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [Material.shape], which is used to implement this property.
|
|
final ShapeBorder closedShape;
|
|
|
|
/// Shape of the container while it is open.
|
|
///
|
|
/// When the container is opened it will transition from [closedShape] to
|
|
/// this shape. When the container is closed, it will transition from this
|
|
/// shape back to [closedShape].
|
|
///
|
|
/// Defaults to a rectangular.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [Material.shape], which is used to implement this property.
|
|
final ShapeBorder openShape;
|
|
|
|
/// Called to obtain the child for the container in the closed state.
|
|
///
|
|
/// The [Widget] returned by this builder is faded out when the container
|
|
/// opens and at the same time the widget returned by [openBuilder] is faded
|
|
/// in while the container grows to fill the surrounding [Navigator].
|
|
///
|
|
/// The `action` callback provided to the builder can be called to open the
|
|
/// container.
|
|
final OpenContainerBuilder closedBuilder;
|
|
|
|
/// Called to obtain the child for the container in the open state.
|
|
///
|
|
/// The [Widget] returned by this builder is faded in when the container
|
|
/// opens and at the same time the widget returned by [closedBuilder] is
|
|
/// faded out while the container grows to fill the surrounding [Navigator].
|
|
///
|
|
/// The `action` callback provided to the builder can be called to close the
|
|
/// container.
|
|
final OpenContainerBuilder openBuilder;
|
|
|
|
/// Whether the entire closed container can be tapped to open it.
|
|
///
|
|
/// Defaults to true.
|
|
///
|
|
/// When this is set to false the container can only be opened by calling the
|
|
/// `action` callback that is provided to the [closedBuilder].
|
|
final bool tappable;
|
|
|
|
/// The time it will take to animate the container from its closed to its
|
|
/// open state and vice versa.
|
|
///
|
|
/// Defaults to 300ms.
|
|
final Duration transitionDuration;
|
|
|
|
/// The type of fade transition that the container will use for its
|
|
/// incoming and outgoing widgets.
|
|
///
|
|
/// Defaults to [ContainerTransitionType.fade].
|
|
final ContainerTransitionType transitionType;
|
|
|
|
@override
|
|
_OpenContainerState createState() => _OpenContainerState();
|
|
}
|
|
|
|
class _OpenContainerState extends State<OpenContainer> {
|
|
// Key used in [_OpenContainerRoute] to hide the widget returned by
|
|
// [OpenContainer.openBuilder] in the source route while the container is
|
|
// opening/open. A copy of that widget is included in the
|
|
// [_OpenContainerRoute] where it fades out. To avoid issues with double
|
|
// shadows and transparency, we hide it in the source route.
|
|
final GlobalKey<_HideableState> _hideableKey = GlobalKey<_HideableState>();
|
|
|
|
// Key used to steal the state of the widget returned by
|
|
// [OpenContainer.openBuilder] from the source route and attach it to the
|
|
// same widget included in the [_OpenContainerRoute] where it fades out.
|
|
final GlobalKey _closedBuilderKey = GlobalKey();
|
|
|
|
void openContainer() {
|
|
Navigator.of(context).push(_OpenContainerRoute(
|
|
beginColor: widget.beginColor,
|
|
endColor: widget.endColor,
|
|
closedColor: widget.closedColor,
|
|
openColor: widget.openColor,
|
|
closedElevation: widget.closedElevation,
|
|
openElevation: widget.openElevation,
|
|
closedShape: widget.closedShape,
|
|
openShape: widget.openShape,
|
|
closedBuilder: widget.closedBuilder,
|
|
openBuilder: widget.openBuilder,
|
|
hideableKey: _hideableKey,
|
|
closedBuilderKey: _closedBuilderKey,
|
|
transitionDuration: widget.transitionDuration,
|
|
transitionType: widget.transitionType,
|
|
flightWidget: widget.flightWidget,
|
|
flightWidgetSize: widget.flightWidgetSize,
|
|
playerRunning: widget.playerRunning,
|
|
playerHeight: widget.playerHeight,
|
|
));
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return _Hideable(
|
|
key: _hideableKey,
|
|
child: GestureDetector(
|
|
onTap: widget.tappable ? openContainer : null,
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
// clipBehavior: Clip.antiAlias,
|
|
// color: widget.closedColor,
|
|
// elevation: widget.closedElevation,
|
|
// shape: widget.closedShape,
|
|
child: Builder(
|
|
key: _closedBuilderKey,
|
|
builder: (context) {
|
|
return widget.closedBuilder(context, openContainer, false);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Controls the visibility of its child.
|
|
///
|
|
/// The child can be in one of three states:
|
|
///
|
|
/// * It is included in the tree and fully visible. (The `placeholderSize` is
|
|
/// null and `isVisible` is true.)
|
|
/// * It is included in the tree, but not visible; its size is maintained.
|
|
/// (The `placeholderSize` is null and `isVisible` is false.)
|
|
/// * It is not included in the tree. Instead a [SizedBox] of dimensions
|
|
/// specified by `placeholderSize` is included in the tree. (The value of
|
|
/// `isVisible` is ignored).
|
|
class _Hideable extends StatefulWidget {
|
|
const _Hideable({
|
|
Key key,
|
|
this.child,
|
|
}) : super(key: key);
|
|
|
|
final Widget child;
|
|
|
|
@override
|
|
State<_Hideable> createState() => _HideableState();
|
|
}
|
|
|
|
class _HideableState extends State<_Hideable> {
|
|
/// When non-null the child is replaced by a [SizedBox] of the set size.
|
|
Size get placeholderSize => _placeholderSize;
|
|
Size _placeholderSize;
|
|
set placeholderSize(Size value) {
|
|
if (_placeholderSize == value) {
|
|
return;
|
|
}
|
|
setState(() {
|
|
_placeholderSize = value;
|
|
});
|
|
}
|
|
|
|
/// When true the child is not visible, but will maintain its size.
|
|
///
|
|
/// The value of this property is ignored when [placeholderSize] is non-null
|
|
/// (i.e. [isInTree] returns false).
|
|
bool get isVisible => _visible;
|
|
bool _visible = true;
|
|
set isVisible(bool value) {
|
|
assert(value != null);
|
|
if (_visible == value) {
|
|
return;
|
|
}
|
|
setState(() {
|
|
_visible = value;
|
|
});
|
|
}
|
|
|
|
/// Whether the child is currently included in the tree.
|
|
///
|
|
/// When it is included, it may be visible or not according to [isVisible].
|
|
bool get isInTree => _placeholderSize == null;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (_placeholderSize != null) {
|
|
return SizedBox.fromSize(size: _placeholderSize);
|
|
}
|
|
return Opacity(
|
|
opacity: _visible ? 1.0 : 0.0,
|
|
child: widget.child,
|
|
);
|
|
}
|
|
}
|
|
|
|
class _OpenContainerRoute extends ModalRoute<void> {
|
|
_OpenContainerRoute({
|
|
@required this.closedColor,
|
|
@required this.openColor,
|
|
@required this.beginColor,
|
|
@required this.endColor,
|
|
@required double closedElevation,
|
|
@required this.openElevation,
|
|
@required ShapeBorder closedShape,
|
|
@required this.openShape,
|
|
@required this.closedBuilder,
|
|
@required this.openBuilder,
|
|
@required this.hideableKey,
|
|
@required this.closedBuilderKey,
|
|
@required this.transitionDuration,
|
|
@required this.transitionType,
|
|
this.flightWidget,
|
|
this.flightWidgetSize,
|
|
this.playerRunning,
|
|
this.playerHeight,
|
|
}) : assert(closedColor != null),
|
|
assert(openColor != null),
|
|
assert(closedElevation != null),
|
|
assert(openElevation != null),
|
|
assert(closedShape != null),
|
|
assert(openBuilder != null),
|
|
assert(closedBuilder != null),
|
|
assert(hideableKey != null),
|
|
assert(closedBuilderKey != null),
|
|
assert(transitionType != null),
|
|
_elevationTween = Tween<double>(
|
|
begin: closedElevation,
|
|
end: openElevation,
|
|
),
|
|
_shapeTween = ShapeBorderTween(
|
|
begin: closedShape,
|
|
end: openShape,
|
|
),
|
|
_colorTween = _getColorTween(
|
|
transitionType: transitionType,
|
|
closedColor: closedColor,
|
|
openColor: openColor,
|
|
beginColor: beginColor,
|
|
endColor: endColor),
|
|
_closedOpacityTween = _getClosedOpacityTween(transitionType),
|
|
_openOpacityTween = _getOpenOpacityTween(transitionType);
|
|
|
|
final Widget flightWidget;
|
|
final double flightWidgetSize;
|
|
final bool playerRunning;
|
|
final double playerHeight;
|
|
static _FlippableTweenSequence<Color> _getColorTween({
|
|
@required ContainerTransitionType transitionType,
|
|
@required Color closedColor,
|
|
@required Color openColor,
|
|
@required Color beginColor,
|
|
@required Color endColor,
|
|
}) {
|
|
switch (transitionType) {
|
|
case ContainerTransitionType.fade:
|
|
return _FlippableTweenSequence<Color>(
|
|
<TweenSequenceItem<Color>>[
|
|
TweenSequenceItem<Color>(
|
|
tween: ConstantTween<Color>(closedColor),
|
|
weight: 1 / 5,
|
|
),
|
|
TweenSequenceItem<Color>(
|
|
tween: ColorTween(begin: closedColor, end: openColor),
|
|
weight: 1 / 5,
|
|
),
|
|
TweenSequenceItem<Color>(
|
|
tween: ConstantTween<Color>(openColor),
|
|
weight: 3 / 5,
|
|
),
|
|
],
|
|
);
|
|
case ContainerTransitionType.fadeThrough:
|
|
return _FlippableTweenSequence<Color>(
|
|
<TweenSequenceItem<Color>>[
|
|
TweenSequenceItem<Color>(
|
|
tween: ColorTween(begin: closedColor, end: endColor),
|
|
weight: 1 / 5,
|
|
),
|
|
TweenSequenceItem<Color>(
|
|
tween: ColorTween(begin: beginColor, end: openColor),
|
|
weight: 4 / 5,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
return null; // unreachable
|
|
}
|
|
|
|
static _FlippableTweenSequence<double> _getClosedOpacityTween(
|
|
ContainerTransitionType transitionType) {
|
|
switch (transitionType) {
|
|
case ContainerTransitionType.fade:
|
|
return _FlippableTweenSequence<double>(
|
|
<TweenSequenceItem<double>>[
|
|
TweenSequenceItem<double>(
|
|
tween: ConstantTween<double>(1.0),
|
|
weight: 1,
|
|
),
|
|
],
|
|
);
|
|
break;
|
|
case ContainerTransitionType.fadeThrough:
|
|
return _FlippableTweenSequence<double>(
|
|
<TweenSequenceItem<double>>[
|
|
TweenSequenceItem<double>(
|
|
tween: Tween<double>(begin: 1.0, end: 0.0),
|
|
weight: 1 / 5,
|
|
),
|
|
TweenSequenceItem<double>(
|
|
tween: ConstantTween<double>(0.0),
|
|
weight: 4 / 5,
|
|
),
|
|
],
|
|
);
|
|
break;
|
|
}
|
|
return null; // unreachable
|
|
}
|
|
|
|
static _FlippableTweenSequence<double> _getOpenOpacityTween(
|
|
ContainerTransitionType transitionType) {
|
|
switch (transitionType) {
|
|
case ContainerTransitionType.fade:
|
|
return _FlippableTweenSequence<double>(
|
|
<TweenSequenceItem<double>>[
|
|
TweenSequenceItem<double>(
|
|
tween: ConstantTween<double>(0.0),
|
|
weight: 1 / 5,
|
|
),
|
|
TweenSequenceItem<double>(
|
|
tween: Tween<double>(begin: 0.0, end: 1.0),
|
|
weight: 1 / 5,
|
|
),
|
|
TweenSequenceItem<double>(
|
|
tween: ConstantTween<double>(1.0),
|
|
weight: 3 / 5,
|
|
),
|
|
],
|
|
);
|
|
break;
|
|
case ContainerTransitionType.fadeThrough:
|
|
return _FlippableTweenSequence<double>(
|
|
<TweenSequenceItem<double>>[
|
|
TweenSequenceItem<double>(
|
|
tween: ConstantTween<double>(0.0),
|
|
weight: 1 / 5,
|
|
),
|
|
TweenSequenceItem<double>(
|
|
tween: Tween<double>(begin: 0.0, end: 1.0),
|
|
weight: 4 / 5,
|
|
),
|
|
],
|
|
);
|
|
break;
|
|
}
|
|
return null; // unreachable
|
|
}
|
|
|
|
final Color closedColor;
|
|
final Color openColor;
|
|
final Color beginColor;
|
|
final Color endColor;
|
|
final double openElevation;
|
|
final ShapeBorder openShape;
|
|
final OpenContainerBuilder closedBuilder;
|
|
final OpenContainerBuilder openBuilder;
|
|
|
|
// See [_OpenContainerState._hideableKey].
|
|
final GlobalKey<_HideableState> hideableKey;
|
|
|
|
// See [_OpenContainerState._closedBuilderKey].
|
|
final GlobalKey closedBuilderKey;
|
|
|
|
@override
|
|
final Duration transitionDuration;
|
|
final ContainerTransitionType transitionType;
|
|
|
|
final Tween<double> _elevationTween;
|
|
final ShapeBorderTween _shapeTween;
|
|
final _FlippableTweenSequence<double> _closedOpacityTween;
|
|
final _FlippableTweenSequence<double> _openOpacityTween;
|
|
final _FlippableTweenSequence<Color> _colorTween;
|
|
|
|
// Key used for the widget returned by [OpenContainer.openBuilder] to keep
|
|
// its state when the shape of the widget tree is changed at the end of the
|
|
// animation to remove all the craft that was necessary to make the animation
|
|
// work.
|
|
final GlobalKey _openBuilderKey = GlobalKey();
|
|
|
|
// Defines the position and the size of the (opening) [OpenContainer] within
|
|
// the bounds of the enclosing [Navigator].
|
|
final RectTween _rectTween = RectTween();
|
|
final Tween<Offset> _positionTween = Tween<Offset>();
|
|
final Tween<double> _avatarScaleTween = Tween<double>();
|
|
AnimationStatus _lastAnimationStatus;
|
|
AnimationStatus _currentAnimationStatus;
|
|
|
|
@override
|
|
TickerFuture didPush() {
|
|
_takeMeasurements(navigatorContext: hideableKey.currentContext);
|
|
|
|
animation.addStatusListener((status) {
|
|
_lastAnimationStatus = _currentAnimationStatus;
|
|
_currentAnimationStatus = status;
|
|
switch (status) {
|
|
case AnimationStatus.dismissed:
|
|
hideableKey.currentState
|
|
..placeholderSize = null
|
|
..isVisible = true;
|
|
break;
|
|
case AnimationStatus.completed:
|
|
hideableKey.currentState
|
|
..placeholderSize = null
|
|
..isVisible = false;
|
|
break;
|
|
case AnimationStatus.forward:
|
|
case AnimationStatus.reverse:
|
|
break;
|
|
}
|
|
});
|
|
|
|
return super.didPush();
|
|
}
|
|
|
|
@override
|
|
bool didPop(void result) {
|
|
_takeMeasurements(
|
|
navigatorContext: subtreeContext,
|
|
delayForSourceRoute: true,
|
|
);
|
|
return super.didPop(result);
|
|
}
|
|
|
|
void _takeMeasurements({
|
|
BuildContext navigatorContext,
|
|
bool delayForSourceRoute = false,
|
|
}) {
|
|
final RenderBox navigator =
|
|
Navigator.of(navigatorContext).context.findRenderObject();
|
|
final navSize = _getSize(navigator);
|
|
_rectTween.end = Offset.zero & navSize;
|
|
void takeMeasurementsInSourceRoute([Duration _]) {
|
|
if (!navigator.attached || hideableKey.currentContext == null) {
|
|
return;
|
|
}
|
|
_rectTween.begin = _getRect(hideableKey, navigator);
|
|
|
|
hideableKey.currentState.placeholderSize = _rectTween.begin.size;
|
|
}
|
|
|
|
if (delayForSourceRoute) {
|
|
SchedulerBinding.instance
|
|
.addPostFrameCallback(takeMeasurementsInSourceRoute);
|
|
} else {
|
|
takeMeasurementsInSourceRoute();
|
|
}
|
|
}
|
|
|
|
Size _getSize(RenderBox render) {
|
|
assert(render != null && render.hasSize);
|
|
return render.size;
|
|
}
|
|
|
|
// Returns the bounds of the [RenderObject] identified by `key` in the
|
|
// coordinate system of `ancestor`.
|
|
Rect _getRect(GlobalKey key, RenderBox ancestor) {
|
|
assert(key.currentContext != null);
|
|
assert(ancestor != null && ancestor.hasSize);
|
|
final RenderBox render = key.currentContext.findRenderObject();
|
|
assert(render != null && render.hasSize);
|
|
return MatrixUtils.transformRect(
|
|
render.getTransformTo(ancestor),
|
|
Offset.zero & render.size,
|
|
);
|
|
}
|
|
|
|
bool get _transitionWasInterrupted {
|
|
var wasInProgress = false;
|
|
var isInProgress = false;
|
|
|
|
switch (_currentAnimationStatus) {
|
|
case AnimationStatus.completed:
|
|
case AnimationStatus.dismissed:
|
|
isInProgress = false;
|
|
break;
|
|
case AnimationStatus.forward:
|
|
case AnimationStatus.reverse:
|
|
isInProgress = true;
|
|
break;
|
|
}
|
|
switch (_lastAnimationStatus) {
|
|
case AnimationStatus.completed:
|
|
case AnimationStatus.dismissed:
|
|
wasInProgress = false;
|
|
break;
|
|
case AnimationStatus.forward:
|
|
case AnimationStatus.reverse:
|
|
wasInProgress = true;
|
|
break;
|
|
}
|
|
return wasInProgress && isInProgress;
|
|
}
|
|
|
|
void closeContainer() {
|
|
Navigator.of(subtreeContext).pop();
|
|
}
|
|
|
|
@override
|
|
Widget buildPage(
|
|
BuildContext context,
|
|
Animation<double> animation,
|
|
Animation<double> secondaryAnimation,
|
|
) {
|
|
return Align(
|
|
alignment: Alignment.topLeft,
|
|
child: AnimatedBuilder(
|
|
animation: animation,
|
|
builder: (context, child) {
|
|
if (animation.isCompleted) {
|
|
return SizedBox.expand(
|
|
child: Material(
|
|
color: openColor,
|
|
elevation: openElevation,
|
|
shape: openShape,
|
|
child: Builder(
|
|
key: _openBuilderKey,
|
|
builder: (context) {
|
|
return openBuilder(context, closeContainer, false);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
final Animation<double> curvedAnimation = CurvedAnimation(
|
|
parent: animation,
|
|
curve: Curves.fastOutSlowIn,
|
|
reverseCurve:
|
|
_transitionWasInterrupted ? null : Curves.fastOutSlowIn.flipped,
|
|
);
|
|
final Animation<double> secondCurvedAnimation = CurvedAnimation(
|
|
parent: animation,
|
|
curve: Curves.easeOutCirc,
|
|
reverseCurve:
|
|
_transitionWasInterrupted ? null : Curves.easeOutCirc.flipped,
|
|
);
|
|
TweenSequence<Color> colorTween;
|
|
TweenSequence<double> closedOpacityTween, openOpacityTween;
|
|
switch (animation.status) {
|
|
case AnimationStatus.dismissed:
|
|
case AnimationStatus.forward:
|
|
closedOpacityTween = _closedOpacityTween;
|
|
openOpacityTween = _openOpacityTween;
|
|
colorTween = _colorTween;
|
|
break;
|
|
case AnimationStatus.reverse:
|
|
if (_transitionWasInterrupted) {
|
|
closedOpacityTween = _closedOpacityTween;
|
|
openOpacityTween = _openOpacityTween;
|
|
colorTween = _colorTween;
|
|
break;
|
|
}
|
|
closedOpacityTween = _closedOpacityTween.flipped;
|
|
openOpacityTween = _openOpacityTween.flipped;
|
|
colorTween = _colorTween.flipped;
|
|
break;
|
|
case AnimationStatus.completed:
|
|
assert(false); // Unreachable.
|
|
break;
|
|
}
|
|
assert(colorTween != null);
|
|
assert(closedOpacityTween != null);
|
|
assert(openOpacityTween != null);
|
|
|
|
final rect = _rectTween.evaluate(curvedAnimation);
|
|
_positionTween.begin =
|
|
Offset(_rectTween.begin.left + 10, _rectTween.begin.top + 10);
|
|
_positionTween.end = Offset(
|
|
10,
|
|
playerRunning
|
|
? MediaQuery.of(context).size.height - 40 - playerHeight
|
|
: MediaQuery.of(context).size.height - 40);
|
|
|
|
_avatarScaleTween.begin = flightWidgetSize;
|
|
_avatarScaleTween.end = 30;
|
|
return SizedBox.expand(
|
|
child: Stack(
|
|
children: <Widget>[
|
|
Container(
|
|
child: Align(
|
|
alignment: Alignment.topLeft,
|
|
child: Transform.translate(
|
|
offset: Offset(rect.left, rect.top),
|
|
child: SizedBox(
|
|
width: rect.width,
|
|
height: rect.height *
|
|
(playerRunning
|
|
? (1 - playerHeight / context.height)
|
|
: 1),
|
|
child: Material(
|
|
clipBehavior: Clip.antiAlias,
|
|
animationDuration: Duration.zero,
|
|
color: colorTween.evaluate(animation),
|
|
shape: _shapeTween.evaluate(curvedAnimation),
|
|
elevation: _elevationTween.evaluate(curvedAnimation),
|
|
child: Stack(
|
|
fit: StackFit.passthrough,
|
|
children: <Widget>[
|
|
// Closed child fading out.
|
|
FittedBox(
|
|
fit: BoxFit.fitWidth,
|
|
alignment: Alignment.topLeft,
|
|
child: SizedBox(
|
|
width: _rectTween.begin.width,
|
|
height: _rectTween.begin.height,
|
|
child: hideableKey.currentState.isInTree
|
|
? null
|
|
: Opacity(
|
|
opacity: closedOpacityTween
|
|
.evaluate(animation),
|
|
child: Builder(
|
|
key: closedBuilderKey,
|
|
builder: (context) {
|
|
// Use dummy "open container" callback
|
|
// since we are in the process of opening.
|
|
return closedBuilder(
|
|
context, () {}, true);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
// Open child fading in.
|
|
FittedBox(
|
|
fit: BoxFit.fitWidth,
|
|
alignment: Alignment.topLeft,
|
|
child: SizedBox(
|
|
width: _rectTween.end.width,
|
|
height: _rectTween.end.height,
|
|
child: Opacity(
|
|
opacity:
|
|
openOpacityTween.evaluate(animation),
|
|
child: Builder(
|
|
key: _openBuilderKey,
|
|
builder: (context) {
|
|
return openBuilder(
|
|
context, closeContainer, true);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Positioned(
|
|
top: _positionTween.evaluate(secondCurvedAnimation).dy,
|
|
left: _positionTween.evaluate(secondCurvedAnimation).dx,
|
|
child: SizedBox(
|
|
height: _avatarScaleTween.evaluate(secondCurvedAnimation),
|
|
width: _avatarScaleTween.evaluate(secondCurvedAnimation),
|
|
child: flightWidget,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
bool get maintainState => true;
|
|
|
|
@override
|
|
Color get barrierColor => null;
|
|
|
|
@override
|
|
bool get opaque => true;
|
|
|
|
@override
|
|
bool get barrierDismissible => false;
|
|
|
|
@override
|
|
String get barrierLabel => null;
|
|
}
|
|
|
|
class _FlippableTweenSequence<T> extends TweenSequence<T> {
|
|
_FlippableTweenSequence(this._items) : super(_items);
|
|
|
|
final List<TweenSequenceItem<T>> _items;
|
|
_FlippableTweenSequence<T> _flipped;
|
|
|
|
_FlippableTweenSequence<T> get flipped {
|
|
if (_flipped == null) {
|
|
final newItems = <TweenSequenceItem<T>>[];
|
|
for (var i = 0; i < _items.length; i++) {
|
|
newItems.add(TweenSequenceItem<T>(
|
|
tween: _items[i].tween,
|
|
weight: _items[_items.length - 1 - i].weight,
|
|
));
|
|
}
|
|
_flipped = _FlippableTweenSequence<T>(newItems);
|
|
}
|
|
return _flipped;
|
|
}
|
|
}
|