import 'package:flutter/material.dart'; class FancyScaffold extends StatefulWidget { final Widget bottomPanel; final double bottomPanelHeight; final Widget expandedPanel; final Widget? bottomNavigationBar; final Widget? drawer; final Widget? navigationRail; final Widget body; final void Function(AnimationStatus)? onAnimationStatusChange; const FancyScaffold({ required this.bottomPanel, required this.bottomPanelHeight, required this.expandedPanel, required this.body, this.onAnimationStatusChange, this.bottomNavigationBar, this.navigationRail, this.drawer, super.key, }); static FancyScaffoldState? of(BuildContext context) => context.findAncestorStateOfType(); @override FancyScaffoldState createState() => FancyScaffoldState(); } class FancyScaffoldState extends State with TickerProviderStateMixin { // goes from 0 to 1 (double) // 0 = preview, 1 = expanded late final AnimationController dragController; final statusNotifier = ValueNotifier(AnimationStatus.dismissed); void openPanel() { dragController.fling(velocity: 1.0); } void closePanel() { dragController.fling(velocity: -1.0); } @override void initState() { dragController = AnimationController( vsync: this, duration: const Duration(milliseconds: 500)); dragController.addStatusListener((status) => statusNotifier.value = status); statusNotifier.addListener( () => widget.onAnimationStatusChange?.call(statusNotifier.value)); super.initState(); } @override void dispose() { dragController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final systemPadding = MediaQuery.of(context).viewPadding; final defaultBottomPadding = (widget.bottomNavigationBar == null ? 0 : 80.0) + systemPadding.bottom; final screenHeight = MediaQuery.of(context).size.height; final sizeAnimation = Tween( begin: widget.bottomPanelHeight / MediaQuery.of(context).size.height, end: 1.0, ).animate(dragController); return Stack( children: [ Positioned.fill( child: Scaffold( body: widget.navigationRail != null ? Row(children: [ widget.navigationRail!, const VerticalDivider( indent: 0.0, endIndent: 0.0, width: 2.0, ), Expanded(child: widget.body) ]) : widget.body, drawer: widget.drawer, bottomNavigationBar: Column( mainAxisSize: MainAxisSize.min, children: [ SizedBox(height: widget.bottomPanelHeight), if (widget.bottomNavigationBar != null) SizeTransition( axisAlignment: -1.0, sizeFactor: Tween(begin: 1.0, end: 0.0).animate(sizeAnimation), child: widget.bottomNavigationBar, ), ], ), ), ), Positioned( bottom: 0, left: 0, right: 0, child: AnimatedBuilder( animation: sizeAnimation, builder: (context, child) { final x = 1.0 - sizeAnimation.value; return Padding( padding: EdgeInsets.only( bottom: (defaultBottomPadding /*+ 8.0*/) * x, //right: 8.0 * x, //left: 8.0 * x, ), child: child, ); }, child: ValueListenableBuilder( valueListenable: statusNotifier, builder: (context, state, child) { return GestureDetector( onVerticalDragEnd: _onVerticalDragEnd, onVerticalDragUpdate: _onVerticalDragUpdate, child: child, ); }, child: SizeTransition( sizeFactor: sizeAnimation, axisAlignment: -1.0, axis: Axis.vertical, child: SizedBox( height: screenHeight, width: MediaQuery.of(context).size.width, child: ValueListenableBuilder( valueListenable: statusNotifier, builder: (context, state, _) => Stack( children: [ if (state != AnimationStatus.dismissed) PopScope( canPop: false, onPopInvoked: (_) => dragController.fling(velocity: -1.0), child: Positioned.fill( key: const Key('player_screen'), child: widget.expandedPanel, ), ), if (state != AnimationStatus.completed) Positioned( top: 0, right: 0, left: 0, key: const Key('player_bar'), child: FadeTransition( opacity: Tween(begin: 1.0, end: 0.0) .animate(dragController), child: SizedBox( height: widget.bottomPanelHeight, child: widget.bottomPanel), ), ), ], ), ), )), ), ), ), ], ); } void _onVerticalDragUpdate(DragUpdateDetails details) { dragController.value -= details.delta.dy / MediaQuery.of(context).size.height; } void _onVerticalDragEnd(DragEndDetails details) { // snap widget to size // this should be also handled by drag velocity and not only with bare size. const double minFlingVelocity = 365.0; if (details.velocity.pixelsPerSecond.dy.abs() > minFlingVelocity) { dragController.fling( velocity: -details.velocity.pixelsPerSecond.dy / MediaQuery.of(context).size.height); return; } dragController.fling(velocity: dragController.value > 0.5 ? 1.0 : -1.0); } }