From ac0f3b5dccecc16da611a394ab5d56f9bddadfbf Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Sun, 14 Sep 2025 11:40:01 +0200 Subject: [PATCH] improve bottom sheet logic --- .../browser/presentation/screens/browser.dart | 64 +++++----- .../widgets/draggable_scrollable_header.dart | 117 +++++++++++++++++- .../presentation/widgets/sheets/view_tab.dart | 28 +++-- 3 files changed, 159 insertions(+), 50 deletions(-) diff --git a/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart b/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart index b9d6da26..bb15a884 100644 --- a/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart +++ b/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart @@ -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( - key: ValueKey(next), - onNotification: dismissOnThreshold, - child: _ViewTabsSheet( - maxChildSize: MediaQuery.of(context).relativeSafeArea(), + final sheet = switch (next) { + ViewTabsSheet() => + NotificationListener( + key: ValueKey(next), + onNotification: dismissOnThreshold, + child: _ViewTabsSheet(maxChildSize: relativeSafeArea), ), - ), - final EditUrlSheet parameter => - NotificationListener( - key: ValueKey(parameter), - onNotification: dismissOnThreshold, - child: _ViewUrlSheet( - initialTabState: parameter.tabState, - maxChildSize: MediaQuery.of(context).relativeSafeArea(), + final EditUrlSheet parameter => + NotificationListener( + key: ValueKey(parameter), + onNotification: dismissOnThreshold, + child: _ViewUrlSheet( + initialTabState: parameter.tabState, + maxChildSize: relativeSafeArea, + ), ), - ), - }; + }; - sheetController.value = state.showBottomSheet((context) => sheet); + return sheet; + }); } } }); diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart index 81ecc8ea..9e36e50e 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart @@ -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 createState() => + _DraggableScrollableHeaderState(); +} + +class _DraggableScrollableHeaderState extends State + with TickerProviderStateMixin { + late AnimationController _animationController; + late Animation? _animation; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + vsync: this, + duration: widget.animationDuration, + ); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + Future _animateToPosition( + double targetSize, { + Duration? customDuration, + }) async { + if (customDuration != null) { + _animationController.duration = customDuration; + } else { + _animationController.duration = widget.animationDuration; + } + + _animation = Tween(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, ); } } diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tab.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tab.dart index c8f25f29..9f0bb960 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tab.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tab.dart @@ -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; });