freezer/lib/ui/fancy_scaffold.dart
Pato05 4b5d0bd09c
improve player screen with blurred album art
ui improvements in lyrics screen
animated bars when track is playing
fix back button when player screen is open
instantly pop when track is changed in queue list
2024-04-29 16:23:22 +02:00

202 lines
7 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);
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<double>(
begin: widget.bottomPanelHeight / MediaQuery.of(context).size.height,
end: 1.0,
).animate(dragController);
return ValueListenableBuilder(
valueListenable: statusNotifier,
builder: (context, state, child) => PopScope(
canPop: state != AnimationStatus.dismissed,
onPopInvoked: state == AnimationStatus.dismissed
? null
: (_) => dragController.fling(velocity: -1.0),
child: child!),
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);
}
}