freezer/lib/ui/fancy_scaffold.dart

196 lines
6.8 KiB
Dart

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<FancyScaffoldState>();
@override
FancyScaffoldState createState() => FancyScaffoldState();
}
class FancyScaffoldState extends State<FancyScaffold>
with TickerProviderStateMixin {
// goes from 0 to 1 (double)
// 0 = preview, 1 = expanded
late final AnimationController dragController;
final statusNotifier =
ValueNotifier<AnimationStatus>(AnimationStatus.dismissed);
@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<double>(
begin: widget.bottomPanelHeight / MediaQuery.of(context).size.height,
end: 1.0,
).animate(dragController);
return WillPopScope(
onWillPop: () {
if (statusNotifier.value == AnimationStatus.completed ||
statusNotifier.value == AnimationStatus.reverse) {
dragController.fling(velocity: -1.0);
return Future.value(false);
}
return Future.value(true);
},
child: 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)
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);
}
}