Roadbound-BRR/lib/pages/home/widgets/swiper.dart

126 lines
3.7 KiB
Dart

import "dart:math" as math;
import "package:flutter/material.dart";
class SidebarSwiper extends StatefulWidget {
const SidebarSwiper({
required this.sidebar,
required this.child,
this.maxSidebarWidth = 360,
this.sidebarWidthFactor = 0.92,
this.edgeDragWidth = 44,
this.animationDuration = const Duration(milliseconds: 220),
super.key,
});
final Widget sidebar;
final Widget child;
final double maxSidebarWidth;
final double sidebarWidthFactor;
final double edgeDragWidth;
final Duration animationDuration;
@override
State<SidebarSwiper> createState() => _SidebarSwiperState();
}
class _SidebarSwiperState extends State<SidebarSwiper> {
static const double _closedExtraOffset = 12;
double _progress = 0; // 0 = closed, 1 = fully open
bool _isDragging = false;
bool _canDrag = false;
double _dragStartGlobalX = 0;
double _dragStartProgress = 0;
void _setOpen(bool value) {
setState(() {
_isDragging = false;
_progress = value ? 1 : 0;
});
}
void _handleDragStart(DragStartDetails details, double sidebarWidth) {
final isOpen = _progress > 0.001;
final fromEdge = details.globalPosition.dx <= widget.edgeDragWidth;
final fromSidebarZone = details.globalPosition.dx <= sidebarWidth;
_canDrag = fromEdge || (isOpen && fromSidebarZone);
if (!_canDrag) return;
setState(() {
_isDragging = true;
_dragStartGlobalX = details.globalPosition.dx;
_dragStartProgress = _progress;
});
}
void _handleDragUpdate(DragUpdateDetails details, double sidebarWidth) {
if (!_canDrag || !_isDragging) return;
if (sidebarWidth <= 0) return;
final movedX = details.globalPosition.dx - _dragStartGlobalX;
setState(() {
_progress = (_dragStartProgress + (movedX / sidebarWidth)).clamp(0, 1);
});
}
void _handleDragEnd(DragEndDetails details) {
if (!_canDrag) return;
_canDrag = false;
final velocity = details.primaryVelocity ?? 0;
final shouldOpen = velocity > 250
? true
: (velocity < -250 ? false : _progress >= 0.35);
_setOpen(shouldOpen);
}
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final sidebarWidth = math.min(
widget.maxSidebarWidth,
screenWidth * widget.sidebarWidthFactor,
);
final leftOffset =
-(sidebarWidth + _closedExtraOffset) +
((sidebarWidth + _closedExtraOffset) * _progress);
final showScrim = _progress > 0;
return Stack(
children: [
widget.child,
Positioned.fill(
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onHorizontalDragStart: (details) =>
_handleDragStart(details, sidebarWidth),
onHorizontalDragUpdate: (details) =>
_handleDragUpdate(details, sidebarWidth),
onHorizontalDragEnd: _handleDragEnd,
child: const SizedBox.expand(),
),
),
if (showScrim)
Positioned.fill(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => _setOpen(false),
child: Container(
color: Colors.black.withValues(alpha: 0.45 * _progress),
),
),
),
AnimatedPositioned(
duration: _isDragging ? Duration.zero : widget.animationDuration,
curve: Curves.easeOutCubic,
left: leftOffset,
top: 0,
bottom: 0,
width: sidebarWidth,
child: Material(
color: Theme.of(context).scaffoldBackgroundColor,
child: widget.sidebar,
),
),
],
);
}
}