126 lines
3.7 KiB
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,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|