import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:provider/provider.dart'; import '../models/models.dart'; typedef CloseContainerActionCallback = void Function({S? returnValue}); typedef OpenContainerBuilder = Widget Function( BuildContext context, CloseContainerActionCallback action, ); typedef CloseContainerBuilder = Widget Function( BuildContext context, VoidCallback action, ); enum ContainerTransitionType { fade, fadeThrough, } typedef ClosedCallback = void Function(S data); @optionalTypeArgs class OpenContainer extends StatefulWidget { const OpenContainer({ super.key, this.middleColor, this.onClosed, required this.closedBuilder, required this.openBuilder, this.tappable = true, this.transitionDuration = const Duration(milliseconds: 300), this.transitionType = ContainerTransitionType.fade, this.useRootNavigator = false, this.routeSettings, this.clipBehavior = Clip.antiAlias, }); final Color? middleColor; final ClosedCallback? onClosed; final CloseContainerBuilder closedBuilder; final OpenContainerBuilder openBuilder; final bool tappable; final Duration transitionDuration; final ContainerTransitionType transitionType; final bool useRootNavigator; final RouteSettings? routeSettings; final Clip clipBehavior; @override State> createState() => _OpenContainerState(); } class _OpenContainerState extends State> { final GlobalKey<_HideableState> _hideableKey = GlobalKey<_HideableState>(); final GlobalKey _closedBuilderKey = GlobalKey(); Future openContainer() async { final Color middleColor = widget.middleColor ?? Theme.of(context).canvasColor; final T? data = await Navigator.of( context, rootNavigator: widget.useRootNavigator, ).push(_OpenContainerRoute( middleColor: middleColor, closedBuilder: widget.closedBuilder, openBuilder: widget.openBuilder, hideableKey: _hideableKey, closedBuilderKey: _closedBuilderKey, transitionDuration: widget.transitionDuration, transitionType: widget.transitionType, useRootNavigator: widget.useRootNavigator, routeSettings: widget.routeSettings, )); if (widget.onClosed != null) { widget.onClosed!(data); } } @override Widget build(BuildContext context) { return _Hideable( key: _hideableKey, child: GestureDetector( onTap: widget.tappable ? openContainer : null, child: Material( clipBehavior: widget.clipBehavior, child: Builder( key: _closedBuilderKey, builder: (BuildContext context) { return widget.closedBuilder(context, openContainer); }, ), ), ), ); } } class _Hideable extends StatefulWidget { const _Hideable({ super.key, required this.child, }); final Widget child; @override State<_Hideable> createState() => _HideableState(); } class _HideableState extends State<_Hideable> { Size? get placeholderSize => _placeholderSize; Size? _placeholderSize; set placeholderSize(Size? value) { if (_placeholderSize == value) { return; } setState(() { _placeholderSize = value; }); } bool get isVisible => _visible; bool _visible = true; set isVisible(bool value) { if (_visible == value) { return; } setState(() { _visible = value; }); } bool get isInTree => _placeholderSize == null; @override Widget build(BuildContext context) { if (_placeholderSize != null) { return SizedBox.fromSize(size: _placeholderSize); } return Visibility( visible: _visible, maintainSize: true, maintainState: true, maintainAnimation: true, child: widget.child, ); } } class _OpenContainerRoute extends ModalRoute { _OpenContainerRoute({ required this.middleColor, required this.closedBuilder, required this.openBuilder, required this.hideableKey, required this.closedBuilderKey, required this.transitionDuration, required this.transitionType, required this.useRootNavigator, required RouteSettings? routeSettings, }) : _closedOpacityTween = _getClosedOpacityTween(transitionType), _openOpacityTween = _getOpenOpacityTween(transitionType), super(settings: routeSettings); static _FlippableTweenSequence _getColorTween({ required ContainerTransitionType transitionType, required Color closedColor, required Color openColor, required Color middleColor, }) { switch (transitionType) { case ContainerTransitionType.fade: return _FlippableTweenSequence( >[ TweenSequenceItem( tween: ConstantTween(closedColor), weight: 1 / 5, ), TweenSequenceItem( tween: ColorTween(begin: closedColor, end: openColor), weight: 1 / 5, ), TweenSequenceItem( tween: ConstantTween(openColor), weight: 3 / 5, ), ], ); case ContainerTransitionType.fadeThrough: return _FlippableTweenSequence( >[ TweenSequenceItem( tween: ColorTween(begin: closedColor, end: middleColor), weight: 1 / 5, ), TweenSequenceItem( tween: ColorTween(begin: middleColor, end: openColor), weight: 4 / 5, ), ], ); } } static _FlippableTweenSequence _getClosedOpacityTween( ContainerTransitionType transitionType) { switch (transitionType) { case ContainerTransitionType.fade: return _FlippableTweenSequence( >[ TweenSequenceItem( tween: ConstantTween(1.0), weight: 1, ), ], ); case ContainerTransitionType.fadeThrough: return _FlippableTweenSequence( >[ TweenSequenceItem( tween: Tween(begin: 1.0, end: 0.0), weight: 1 / 5, ), TweenSequenceItem( tween: ConstantTween(0.0), weight: 4 / 5, ), ], ); } } static _FlippableTweenSequence _getOpenOpacityTween( ContainerTransitionType transitionType) { switch (transitionType) { case ContainerTransitionType.fade: return _FlippableTweenSequence( >[ TweenSequenceItem( tween: ConstantTween(0.0), weight: 1 / 5, ), TweenSequenceItem( tween: Tween(begin: 0.0, end: 1.0), weight: 1 / 5, ), TweenSequenceItem( tween: ConstantTween(1.0), weight: 3 / 5, ), ], ); case ContainerTransitionType.fadeThrough: return _FlippableTweenSequence( >[ TweenSequenceItem( tween: ConstantTween(0.0), weight: 1 / 5, ), TweenSequenceItem( tween: Tween(begin: 0.0, end: 1.0), weight: 4 / 5, ), ], ); } } final Color middleColor; final CloseContainerBuilder closedBuilder; final OpenContainerBuilder openBuilder; final GlobalKey<_HideableState> hideableKey; final GlobalKey closedBuilderKey; @override final Duration transitionDuration; final ContainerTransitionType transitionType; final bool useRootNavigator; final _FlippableTweenSequence _closedOpacityTween; final _FlippableTweenSequence _openOpacityTween; late _FlippableTweenSequence _colorTween; final GlobalKey _openBuilderKey = GlobalKey(); final RectTween _rectTween = RectTween(); AnimationStatus? _lastAnimationStatus; AnimationStatus? _currentAnimationStatus; @override TickerFuture didPush() { _takeMeasurements(navigatorContext: hideableKey.currentContext!); animation!.addStatusListener((AnimationStatus status) { _lastAnimationStatus = _currentAnimationStatus; _currentAnimationStatus = status; switch (status) { case AnimationStatus.dismissed: _toggleHideable(hide: false); break; case AnimationStatus.completed: _toggleHideable(hide: true); break; case AnimationStatus.forward: case AnimationStatus.reverse: break; } }); return super.didPush(); } @override bool didPop(T? result) { _takeMeasurements( navigatorContext: subtreeContext!, delayForSourceRoute: true, ); return super.didPop(result); } @override void dispose() { if (hideableKey.currentState?.isVisible == false) { SchedulerBinding.instance .addPostFrameCallback((Duration d) => _toggleHideable(hide: false)); } super.dispose(); } void _toggleHideable({required bool hide}) { if (hideableKey.currentState != null) { hideableKey.currentState! ..placeholderSize = null ..isVisible = !hide; } } void _takeMeasurements({ required BuildContext navigatorContext, bool delayForSourceRoute = false, }) { final RenderBox navigator = Navigator.of( navigatorContext, rootNavigator: useRootNavigator, ).context.findRenderObject()! as RenderBox; final Size 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.hasSize); return render.size; } Rect _getRect(GlobalKey key, RenderBox ancestor) { assert(key.currentContext != null); assert(ancestor.hasSize); final RenderBox render = key.currentContext!.findRenderObject()! as RenderBox; assert(render.hasSize); return MatrixUtils.transformRect( render.getTransformTo(ancestor), Offset.zero & render.size, ); } bool get _transitionWasInterrupted { bool wasInProgress = false; bool isInProgress = false; switch (_currentAnimationStatus) { case AnimationStatus.completed: case AnimationStatus.dismissed: isInProgress = false; break; case AnimationStatus.forward: case AnimationStatus.reverse: isInProgress = true; break; case null: break; } switch (_lastAnimationStatus) { case AnimationStatus.completed: case AnimationStatus.dismissed: wasInProgress = false; break; case AnimationStatus.forward: case AnimationStatus.reverse: wasInProgress = true; break; case null: break; } return wasInProgress && isInProgress; } void closeContainer({T? returnValue}) { Navigator.of(subtreeContext!).pop(returnValue); } @override Widget buildPage( BuildContext context, Animation animation, Animation secondaryAnimation, ) { return Selector( selector: (_, config) => config.themeMode, builder: (_, __, ___) { _colorTween = _getColorTween( transitionType: transitionType, closedColor: Theme.of(context).colorScheme.surface, openColor: Theme.of(context).colorScheme.surface, middleColor: middleColor, ); return Align( alignment: Alignment.topLeft, child: AnimatedBuilder( animation: animation, builder: (BuildContext context, Widget? child) { if (animation.isCompleted) { return SizedBox.expand( child: Material( child: Builder( key: _openBuilderKey, builder: (BuildContext context) { return openBuilder(context, closeContainer); }, ), ), ); } final Animation curvedAnimation = CurvedAnimation( parent: animation, curve: Curves.fastOutSlowIn, reverseCurve: _transitionWasInterrupted ? null : Curves.fastOutSlowIn.flipped, ); TweenSequence? colorTween; TweenSequence? 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 rect = _rectTween.evaluate(curvedAnimation)!; return SizedBox.expand( child: Align( alignment: Alignment.topLeft, child: Transform.translate( offset: Offset(rect.left, rect.top), child: SizedBox( width: rect.width, height: rect.height, child: Material( clipBehavior: Clip.antiAlias, animationDuration: Duration.zero, color: colorTween!.evaluate(animation), child: Stack( fit: StackFit.passthrough, children: [ // 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 ?? false) ? null : FadeTransition( opacity: closedOpacityTween! .animate(animation), child: Builder( key: closedBuilderKey, builder: (BuildContext context) { // Use dummy "open container" callback // since we are in the process of opening. return closedBuilder( context, () {}); }, ), ), ), ), // Open child fading in. FittedBox( fit: BoxFit.fitWidth, alignment: Alignment.topLeft, child: SizedBox( width: _rectTween.end!.width, height: _rectTween.end!.height, child: FadeTransition( opacity: openOpacityTween!.animate(animation), child: Builder( key: _openBuilderKey, builder: (BuildContext context) { return openBuilder( context, closeContainer); }, ), ), ), ), ], ), ), ), ), ), ); }, ), ); }, ); } @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 extends TweenSequence { _FlippableTweenSequence(this._items) : super(_items); final List> _items; _FlippableTweenSequence? _flipped; _FlippableTweenSequence? get flipped { if (_flipped == null) { final List> newItems = >[]; for (int i = 0; i < _items.length; i++) { newItems.add(TweenSequenceItem( tween: _items[i].tween, weight: _items[_items.length - 1 - i].weight, )); } _flipped = _FlippableTweenSequence(newItems); } return _flipped; } }