198 lines
6.7 KiB
Dart
198 lines
6.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 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);
|
|
}
|
|
}
|