improve bottom sheet logic

This commit is contained in:
Fabian Freund
2025-09-14 11:42:13 +02:00
parent 90118278f1
commit 3242460c49
3 changed files with 159 additions and 50 deletions
@@ -115,44 +115,44 @@ class BrowserScreen extends HookConsumerWidget {
}
if (next != null) {
bool dismissOnThreshold(
DraggableScrollableNotification notification,
) {
if (!context.mounted) return false;
final relativeSafeArea = MediaQuery.of(context).relativeSafeArea();
if (notification.extent <= 0.1) {
ref.read(bottomSheetControllerProvider.notifier).dismiss();
return true;
} else {
ref
.read(bottomSheetExtendProvider.notifier)
.add(notification.extent);
sheetController.value = state.showBottomSheet((context) {
bool dismissOnThreshold(
DraggableScrollableNotification notification,
) {
if (notification.extent <= 0.1) {
ref.read(bottomSheetControllerProvider.notifier).dismiss();
return true;
} else {
ref
.read(bottomSheetExtendProvider.notifier)
.add(notification.extent);
}
return false;
}
return false;
}
final sheet = switch (next) {
ViewTabsSheet() =>
NotificationListener<DraggableScrollableNotification>(
key: ValueKey(next),
onNotification: dismissOnThreshold,
child: _ViewTabsSheet(
maxChildSize: MediaQuery.of(context).relativeSafeArea(),
final sheet = switch (next) {
ViewTabsSheet() =>
NotificationListener<DraggableScrollableNotification>(
key: ValueKey(next),
onNotification: dismissOnThreshold,
child: _ViewTabsSheet(maxChildSize: relativeSafeArea),
),
),
final EditUrlSheet parameter =>
NotificationListener<DraggableScrollableNotification>(
key: ValueKey(parameter),
onNotification: dismissOnThreshold,
child: _ViewUrlSheet(
initialTabState: parameter.tabState,
maxChildSize: MediaQuery.of(context).relativeSafeArea(),
final EditUrlSheet parameter =>
NotificationListener<DraggableScrollableNotification>(
key: ValueKey(parameter),
onNotification: dismissOnThreshold,
child: _ViewUrlSheet(
initialTabState: parameter.tabState,
maxChildSize: relativeSafeArea,
),
),
),
};
};
sheetController.value = state.showBottomSheet((context) => sheet);
return sheet;
});
}
}
});
@@ -21,26 +21,133 @@ import 'dart:math';
import 'package:flutter/material.dart';
class DraggableScrollableHeader extends StatelessWidget {
class DraggableScrollableHeader extends StatefulWidget {
final DraggableScrollableController controller;
final Widget child;
// Velocity control parameters
final double velocitySensitivity;
final double maxVelocity;
final double minVelocity;
final Duration animationDuration;
final Curve animationCurve;
final double dragSensitivity;
const DraggableScrollableHeader({
super.key,
required this.controller,
required this.child,
this.velocitySensitivity = 0.35, // Higher = more sensitive to velocity
this.maxVelocity = 2500.0, // Maximum velocity to consider
this.minVelocity = 150.0, // Minimum velocity to trigger momentum
this.animationDuration = const Duration(milliseconds: 300),
this.animationCurve = Curves.decelerate,
this.dragSensitivity = 1.0, // Controls drag responsiveness
});
@override
State<DraggableScrollableHeader> createState() =>
_DraggableScrollableHeaderState();
}
class _DraggableScrollableHeaderState extends State<DraggableScrollableHeader>
with TickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double>? _animation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: widget.animationDuration,
);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
Future<void> _animateToPosition(
double targetSize, {
Duration? customDuration,
}) async {
if (customDuration != null) {
_animationController.duration = customDuration;
} else {
_animationController.duration = widget.animationDuration;
}
_animation = Tween<double>(begin: widget.controller.size, end: targetSize)
.animate(
CurvedAnimation(
parent: _animationController,
curve: widget.animationCurve,
),
);
_animationController.reset();
await _animationController.forward();
_animation!.addListener(() {
widget.controller.jumpTo(_animation!.value);
});
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onVerticalDragUpdate: (details) {
// Use the DraggableScrollableSheet's controller
controller.jumpTo(
min(1, controller.pixelsToSize(controller.pixels - details.delta.dy)),
_animationController.stop();
// Apply drag sensitivity
final adjustedDelta = details.delta.dy * widget.dragSensitivity;
widget.controller.jumpTo(
min(
1,
widget.controller.pixelsToSize(
widget.controller.pixels - adjustedDelta,
),
),
);
},
child: child,
onVerticalDragEnd: (details) async {
final velocity = details.primaryVelocity ?? 0;
final currentSize = widget.controller.size;
// Clamp velocity to defined range
final clampedVelocity = velocity.clamp(
-widget.maxVelocity,
widget.maxVelocity,
);
// Only apply momentum if velocity exceeds minimum threshold
if (clampedVelocity.abs() < widget.minVelocity) {
return; // No momentum, stay at current position
}
// Calculate momentum-based target with sensitivity control
final velocityFactor =
clampedVelocity / 1000 * widget.velocitySensitivity;
double targetSize = currentSize - velocityFactor;
// Clamp to valid range
targetSize = targetSize.clamp(0.0, 1.0);
// Calculate animation duration based on velocity (faster velocity = longer animation)
final velocityRatio = clampedVelocity.abs() / widget.maxVelocity;
final dynamicDuration = Duration(
milliseconds:
(widget.animationDuration.inMilliseconds * (0.5 + velocityRatio))
.round(),
);
await _animateToPosition(targetSize, customDuration: dynamicDuration);
},
child: widget.child,
);
}
}
@@ -96,18 +96,17 @@ class ViewTabSheetWidget extends HookConsumerWidget {
final totalHeight =
headerBox.size.height + textBox.size.height + kToolbarHeight;
final relative = totalHeight / MediaQuery.of(context).size.height;
final relative = (totalHeight / MediaQuery.of(context).size.height)
.clamp(0.0, 1.0);
if (relative >= 0 && relative <= 1) {
if (draggableScrollableController.size < relative &&
relative > scrolledTo.value) {
await draggableScrollableController.animateTo(
relative,
duration: const Duration(milliseconds: 150),
curve: Curves.easeInOut,
);
scrolledTo.value = relative;
}
if (draggableScrollableController.size < relative &&
relative > scrolledTo.value) {
await draggableScrollableController.animateTo(
relative,
duration: const Duration(milliseconds: 150),
curve: Curves.easeInOut,
);
scrolledTo.value = relative;
}
}
}
@@ -125,10 +124,13 @@ class ViewTabSheetWidget extends HookConsumerWidget {
MediaQuery.of(context).size.height) -
bottomInsets.value;
draggableScrollableController.jumpTo(
draggableScrollableController.size + diff,
final jumpValue = (draggableScrollableController.size + diff).clamp(
0.0,
1.0,
);
draggableScrollableController.jumpTo(jumpValue);
bottomInsets.value += diff;
});