From 2f65fb4f56934e45019d1c618103704a40b2d2a5 Mon Sep 17 00:00:00 2001 From: xxxOVALxxx Date: Thu, 20 Aug 2026 02:08:36 +0500 Subject: [PATCH 1/3] Prevent intercepting CupertinoSheet transitions mid-animation --- packages/cupertino_ui/lib/src/sheet.dart | 7 ++- packages/cupertino_ui/test/sheet_test.dart | 51 ++++++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/packages/cupertino_ui/lib/src/sheet.dart b/packages/cupertino_ui/lib/src/sheet.dart index 0044e001dcaf..5f5d1b9aa11a 100644 --- a/packages/cupertino_ui/lib/src/sheet.dart +++ b/packages/cupertino_ui/lib/src/sheet.dart @@ -776,7 +776,7 @@ class CupertinoSheetRoute extends PageRoute with _CupertinoSheetRouteTrans data: CupertinoUserInterfaceLevelData.elevated, child: _CupertinoSheetScope( child: _CupertinoDraggableScrollableSheet( - enabledCallback: () => enableDrag, + enabledCallback: () => enableDrag && !(controller?.isAnimating ?? false), onStartPopGesture: () => _CupertinoSheetRouteTransitionMixin._startPopGesture(this, topGap), builder: _sheetWithDragHandle, @@ -906,7 +906,7 @@ mixin _CupertinoSheetRouteTransitionMixin on PageRoute { linearTransition: linearTransition, topGap: topGap, child: _CupertinoDragGestureDetector( - enabledCallback: () => enableDrag, + enabledCallback: () => enableDrag && !(route.controller?.isAnimating ?? false), onStartPopGesture: () => _startPopGesture(route, topGap), child: child, ), @@ -1377,6 +1377,9 @@ class _CupertinoDraggableScrollableSheetState void _dragStart() { assert(mounted); + if (!widget.enabledCallback()) { + return; + } _dragGestureController ??= widget.onStartPopGesture(); } diff --git a/packages/cupertino_ui/test/sheet_test.dart b/packages/cupertino_ui/test/sheet_test.dart index 7e9f91570fb8..03290fc9f441 100644 --- a/packages/cupertino_ui/test/sheet_test.dart +++ b/packages/cupertino_ui/test/sheet_test.dart @@ -1395,6 +1395,57 @@ void main() { expect(rootNavigatorPopped, false); }); + testWidgets('Sheet ignores gestures mid-dismissal and finishes closing', ( + WidgetTester tester, + ) async { + final GlobalKey homeKey = GlobalKey(); + final GlobalKey sheetKey = GlobalKey(); + + await tester.pumpWidget(dragGestureApp(homeKey, sheetKey)); + + // Open sheet + await tester.tap(find.text('Push Page 2')); + await tester.pumpAndSettle(); + + final Finder sheetFinder = find.byKey(sheetKey); + final Size sheetSize = tester.getSize(sheetFinder); + final double sheetHeight = sheetSize.height; + + final double dragDistance = sheetHeight / 1.8; + + final Offset sheetTopLeft = tester.getTopLeft(sheetFinder); + final startPoint = Offset(sheetTopLeft.dx + (sheetSize.width / 1.8), sheetTopLeft.dy + 20.0); + + // Drag sheet down + final TestGesture gesture = await tester.startGesture(startPoint); + await gesture.moveBy(Offset(0, dragDistance)); + await tester.pump(); + + // Release sheet + await gesture.up(); + await tester.pump(); + + await tester.pump(const Duration(milliseconds: 50)); + + final box = tester.renderObject(sheetFinder) as RenderBox; + final double currentY = box.localToGlobal(Offset.zero).dy; + + // Try to intercept the gesture by dragging up + final TestGesture interceptGesture = await tester.startGesture( + Offset(startPoint.dx, currentY + 100), + ); + await tester.pump(); + + // Drag up + await interceptGesture.moveBy(const Offset(0, -50)); + await interceptGesture.up(); + + await tester.pumpAndSettle(); + + expect(find.text('Page 2'), findsNothing); + expect(find.text('Page 1'), findsOneWidget); + }); + testWidgets('dragging does not move the sheet when enableDrag is false', ( WidgetTester tester, ) async { From 2cd3df0ba3b5aec5d1fcfa4bd48f638ec3a354ec Mon Sep 17 00:00:00 2001 From: xxxOVALxxx Date: Thu, 20 Aug 2026 03:46:57 +0500 Subject: [PATCH 2/3] Add rubber-band and spring physics --- packages/cupertino_ui/lib/src/sheet.dart | 133 +++++++++++++-- packages/cupertino_ui/test/sheet_test.dart | 188 ++++++++++----------- 2 files changed, 210 insertions(+), 111 deletions(-) diff --git a/packages/cupertino_ui/lib/src/sheet.dart b/packages/cupertino_ui/lib/src/sheet.dart index 5f5d1b9aa11a..dcaf798d818f 100644 --- a/packages/cupertino_ui/lib/src/sheet.dart +++ b/packages/cupertino_ui/lib/src/sheet.dart @@ -2,7 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:math' as math; + import 'package:flutter/gestures.dart'; +import 'package:flutter/physics.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; @@ -36,6 +39,38 @@ const double _kTopGapRatio = 0.08; // running iOS 18.0 simulators. const double _kStretchedTopGapRatio = 0.072; +// Spring description used for the bounce-back animation when enableDrag == false +// or when snapping back to position. Measured on iOS 26 +SpringDescription _sheetSpring = SpringDescription.withDurationAndBounce( + duration: const Duration(milliseconds: 430), +); + +// The standard elasticity coefficient used in Apple's Rubber-Banding formula +// (WWDC 2018: "Designing Fluid Interfaces") +const double _kAppleRubberBandElasticity = 0.55; + +// The asymptotic maximum stretch distance in logical pixels when pulling against resistance. +const double _kMaxRubberBandDistance = 180.0; + +// Calculates the instantaneous friction factor for overscroll resistance using +// the exact derivative of Apple's Rubber-Banding equation: +// +// y(x) = (x * c * d) / (d + c * x) +// dy/dx = c * (1.0 - y / d)^2 +// +// where `c` is [_kAppleRubberBandElasticity], `d` is [_kMaxRubberBandDistance], +// and `y` is the current physical displacement in logical pixels. +// +// https://gist.github.com/originell/6961057 +double _computeRubberBandFriction({ + required double currentDisplacement, + double dimension = _kMaxRubberBandDistance, + double constant = _kAppleRubberBandElasticity, +}) { + final double stretchProgress = (currentDisplacement / dimension).clamp(0.0, 1.0); + return constant * math.pow(1.0 - stretchProgress, 2.0); +} + // Tween for animating a Cupertino sheet onto the screen. // // Begins fully offscreen below the screen and ends onscreen with a small gap at @@ -776,9 +811,14 @@ class CupertinoSheetRoute extends PageRoute with _CupertinoSheetRouteTrans data: CupertinoUserInterfaceLevelData.elevated, child: _CupertinoSheetScope( child: _CupertinoDraggableScrollableSheet( - enabledCallback: () => enableDrag && !(controller?.isAnimating ?? false), - onStartPopGesture: () => - _CupertinoSheetRouteTransitionMixin._startPopGesture(this, topGap), + enabledCallback: () => !(controller?.isAnimating ?? false), + enableDrag: enableDrag, + onStartPopGesture: () => _CupertinoSheetRouteTransitionMixin._startPopGesture( + this, + topGap, + enableDrag: enableDrag, + ), + topGap: topGap, builder: _sheetWithDragHandle, ), ), @@ -878,14 +918,16 @@ mixin _CupertinoSheetRouteTransitionMixin on PageRoute { static _CupertinoDragGestureController _startPopGesture( ModalRoute route, - double topGap, - ) { + double topGap, { + required bool enableDrag, + }) { return _CupertinoDragGestureController( topGap: topGap, navigator: route.navigator!, getIsCurrent: () => route.isCurrent, getIsActive: () => route.isActive, popDragController: route.controller!, // protected access + enableDrag: enableDrag, ); } @@ -906,8 +948,8 @@ mixin _CupertinoSheetRouteTransitionMixin on PageRoute { linearTransition: linearTransition, topGap: topGap, child: _CupertinoDragGestureDetector( - enabledCallback: () => enableDrag && !(route.controller?.isAnimating ?? false), - onStartPopGesture: () => _startPopGesture(route, topGap), + enabledCallback: () => !(route.controller?.isAnimating ?? false), + onStartPopGesture: () => _startPopGesture(route, topGap, enableDrag: enableDrag), child: child, ), ); @@ -1023,9 +1065,9 @@ class _CupertinoDragGestureDetectorState extends State<_CupertinoDragGestureD } final double delta = sheetHeight > 0 ? details.primaryDelta! / sheetHeight : 0.0; _dragGestureController!.dragUpdate( - // Divide by size of the sheet. delta, _stretchDragController!.controller, + sheetHeight: sheetHeight, ); } @@ -1079,6 +1121,7 @@ class _CupertinoDragGestureController { required this.getIsActive, required this.getIsCurrent, required this.topGap, + required this.enableDrag, }) { navigator.didStartUserGesture(); } @@ -1088,10 +1131,11 @@ class _CupertinoDragGestureController { final ValueGetter getIsActive; final ValueGetter getIsCurrent; final double topGap; + final bool enableDrag; /// The drag gesture has changed by [delta]. The total range of the drag /// should be 0.0 to 1.0. - void dragUpdate(double delta, AnimationController? upController) { + void dragUpdate(double delta, AnimationController? upController, {required double sheetHeight}) { if (upController != null && popDragController.value == 1.0 && (upController.value > 0 || delta < 0)) { @@ -1100,7 +1144,17 @@ class _CupertinoDragGestureController { const double stretchDistance = _kTopGapRatio - _kStretchedTopGapRatio; upController.value -= delta / stretchDistance; } else { - popDragController.value -= delta; + if (!enableDrag) { + // Applies the exact derivative of Apple's Rubber-Banding equation. + final double currentDisplacement = + (1.0 - popDragController.value).clamp(0.0, 1.0) * sheetHeight; + final double friction = _computeRubberBandFriction( + currentDisplacement: currentDisplacement, + ); + popDragController.value -= delta * friction; + } else { + popDragController.value -= delta; + } } } @@ -1123,6 +1177,31 @@ class _CupertinoDragGestureController { return; } + if (!enableDrag) { + // When dragging is disabled, spring back to the open position using a physical spring simulation. + final Simulation simulation = SpringSimulation( + _sheetSpring, + popDragController.value, + 1.0, + -velocity, + ); + popDragController.animateWith(simulation); + + if (popDragController.isAnimating) { + void animationStatusCallback(AnimationStatus status) { + if (status == AnimationStatus.completed || status == AnimationStatus.dismissed) { + navigator.didStopUserGesture(); + popDragController.removeStatusListener(animationStatusCallback); + } + } + + popDragController.addStatusListener(animationStatusCallback); + } else { + navigator.didStopUserGesture(); + } + return; + } + // Fling in the appropriate direction. // // This curve has been determined through rigorously eyeballing native iOS @@ -1195,12 +1274,14 @@ class _CupertinoSheetScrollController extends ScrollController { required this.onDragUpdate, required this.onDragEnd, required this.sheetIsDraggedDown, + required this.hasActiveDragController, }); final _DragStartCallback onDragStart; final _DragEndCallback onDragUpdate; final _DragUpdateCallback onDragEnd; final _GetSheetDragged sheetIsDraggedDown; + final ValueGetter hasActiveDragController; @override _CupertinoSheetScrollPosition createScrollPosition( @@ -1216,6 +1297,7 @@ class _CupertinoSheetScrollController extends ScrollController { onDragUpdate: onDragUpdate, onDragEnd: onDragEnd, sheetIsDraggedDown: sheetIsDraggedDown, + hasActiveDragController: hasActiveDragController, ); } } @@ -1241,6 +1323,7 @@ class _CupertinoSheetScrollPosition extends ScrollPositionWithSingleContext { required this.onDragUpdate, required this.onDragEnd, required this.sheetIsDraggedDown, + required this.hasActiveDragController, }); VoidCallback? _dragCancelCallback; @@ -1252,6 +1335,7 @@ class _CupertinoSheetScrollPosition extends ScrollPositionWithSingleContext { final _DragUpdateCallback onDragEnd; final _GetSheetDragged sheetIsDraggedDown; + final ValueGetter hasActiveDragController; @override void absorb(ScrollPosition other) { @@ -1289,7 +1373,8 @@ class _CupertinoSheetScrollPosition extends ScrollPositionWithSingleContext { @override void applyUserOffset(double delta) { onDragStart(); - if (!listShouldScroll && (delta > 0 || sheetIsDraggedDown())) { + final bool canDragSheet = hasActiveDragController() || sheetIsDraggedDown(); + if (canDragSheet && !listShouldScroll && (delta > 0 || sheetIsDraggedDown())) { onDragUpdate(delta); } else { super.applyUserOffset(delta); @@ -1309,9 +1394,15 @@ class _CupertinoSheetScrollPosition extends ScrollPositionWithSingleContext { _dragCancelCallback?.call(); _dragCancelCallback = null; if (velocity < 0.0 && !listShouldScroll) { - onDragEnd(velocity); - super.goBallistic(0); - return; + if (sheetIsDraggedDown()) { + onDragEnd(velocity); + super.goBallistic(0); + return; + } else { + onDragEnd(0.0); + super.goBallistic(velocity); + return; + } } onDragEnd(0.0); super.goBallistic(velocity); @@ -1329,14 +1420,20 @@ class _CupertinoDraggableScrollableSheet extends StatefulWidget { const _CupertinoDraggableScrollableSheet({ super.key, required this.enabledCallback, + required this.enableDrag, required this.onStartPopGesture, required this.builder, + this.topGap = _kTopGapRatio, }); final ScrollableWidgetBuilder builder; final ValueGetter enabledCallback; + final bool enableDrag; + + final double topGap; + final ValueGetter<_CupertinoDragGestureController> onStartPopGesture; @override @@ -1357,6 +1454,7 @@ class _CupertinoDraggableScrollableSheetState onDragUpdate: _dragUpdate, onDragEnd: _handleDragEnd, sheetIsDraggedDown: () => _dragGestureController?.isDragged() ?? false, + hasActiveDragController: () => _dragGestureController != null, ); } @@ -1386,9 +1484,11 @@ class _CupertinoDraggableScrollableSheetState void _dragUpdate(double delta) { assert(mounted); if (_dragGestureController != null) { + final double sheetHeight = context.size?.height ?? 0.0; _dragGestureController!.dragUpdate( - delta / (context.size!.height - (context.size!.height * _kTopGapRatio)), + sheetHeight > 0 ? delta / sheetHeight : 0.0, null, + sheetHeight: sheetHeight, ); } } @@ -1396,7 +1496,8 @@ class _CupertinoDraggableScrollableSheetState void _handleDragEnd(double velocity) { assert(mounted); if (_dragGestureController != null) { - _dragGestureController!.dragEnd(-velocity / context.size!.height, null); + final double sheetHeight = context.size?.height ?? 0.0; + _dragGestureController!.dragEnd(sheetHeight > 0 ? -velocity / sheetHeight : 0.0, null); _dragGestureController = null; } } diff --git a/packages/cupertino_ui/test/sheet_test.dart b/packages/cupertino_ui/test/sheet_test.dart index 03290fc9f441..b5cda608d212 100644 --- a/packages/cupertino_ui/test/sheet_test.dart +++ b/packages/cupertino_ui/test/sheet_test.dart @@ -1395,94 +1395,80 @@ void main() { expect(rootNavigatorPopped, false); }); - testWidgets('Sheet ignores gestures mid-dismissal and finishes closing', ( - WidgetTester tester, - ) async { - final GlobalKey homeKey = GlobalKey(); - final GlobalKey sheetKey = GlobalKey(); - - await tester.pumpWidget(dragGestureApp(homeKey, sheetKey)); + testWidgets( + 'dragging with enableDrag: false rubber-bands and springs back without dismissing', + (WidgetTester tester) async { + Widget nonDragGestureApp(GlobalKey homeScaffoldKey, GlobalKey sheetScaffoldKey) { + return CupertinoApp( + home: CupertinoPageScaffold( + key: homeScaffoldKey, + child: Center( + child: Column( + children: [ + const Text('Page 1'), + CupertinoButton( + onPressed: () { + showCupertinoSheet( + context: homeScaffoldKey.currentContext!, + pageBuilder: (BuildContext context) { + return CupertinoPageScaffold( + key: sheetScaffoldKey, + child: const Center(child: Text('Page 2')), + ); + }, + enableDrag: false, + ); + }, + child: const Text('Push Page 2'), + ), + ], + ), + ), + ), + ); + } - // Open sheet - await tester.tap(find.text('Push Page 2')); - await tester.pumpAndSettle(); + final GlobalKey homeKey = GlobalKey(); + final GlobalKey sheetKey = GlobalKey(); - final Finder sheetFinder = find.byKey(sheetKey); - final Size sheetSize = tester.getSize(sheetFinder); - final double sheetHeight = sheetSize.height; + await tester.pumpWidget(nonDragGestureApp(homeKey, sheetKey)); - final double dragDistance = sheetHeight / 1.8; + await tester.tap(find.text('Push Page 2')); + await tester.pumpAndSettle(); - final Offset sheetTopLeft = tester.getTopLeft(sheetFinder); - final startPoint = Offset(sheetTopLeft.dx + (sheetSize.width / 1.8), sheetTopLeft.dy + 20.0); + expect(find.text('Page 2'), findsOneWidget); - // Drag sheet down - final TestGesture gesture = await tester.startGesture(startPoint); - await gesture.moveBy(Offset(0, dragDistance)); - await tester.pump(); + var box = tester.renderObject(find.byKey(sheetKey)) as RenderBox; + final double initialPosition = box.localToGlobal(Offset.zero).dy; - // Release sheet - await gesture.up(); - await tester.pump(); + final TestGesture gesture = await tester.startGesture(const Offset(100, 200)); + await gesture.moveBy(const Offset(0, 200)); + await tester.pump(); - await tester.pump(const Duration(milliseconds: 50)); + box = tester.renderObject(find.byKey(sheetKey)) as RenderBox; + final double middlePosition = box.localToGlobal(Offset.zero).dy; - final box = tester.renderObject(sheetFinder) as RenderBox; - final double currentY = box.localToGlobal(Offset.zero).dy; + expect(middlePosition, greaterThan(initialPosition)); - // Try to intercept the gesture by dragging up - final TestGesture interceptGesture = await tester.startGesture( - Offset(startPoint.dx, currentY + 100), - ); - await tester.pump(); + await gesture.up(); + await tester.pumpAndSettle(); - // Drag up - await interceptGesture.moveBy(const Offset(0, -50)); - await interceptGesture.up(); + expect(find.text('Page 2'), findsOneWidget); - await tester.pumpAndSettle(); + box = tester.renderObject(find.byKey(sheetKey)) as RenderBox; + final double finalPosition = box.localToGlobal(Offset.zero).dy; - expect(find.text('Page 2'), findsNothing); - expect(find.text('Page 1'), findsOneWidget); - }); + expect(finalPosition, closeTo(initialPosition, 0.1)); + }, + ); - testWidgets('dragging does not move the sheet when enableDrag is false', ( + testWidgets('partial upward drag stretches and returns without popping', ( WidgetTester tester, ) async { - Widget nonDragGestureApp(GlobalKey homeScaffoldKey, GlobalKey sheetScaffoldKey) { - return CupertinoApp( - home: CupertinoPageScaffold( - key: homeScaffoldKey, - child: Center( - child: Column( - children: [ - const Text('Page 1'), - CupertinoButton( - onPressed: () { - showCupertinoSheet( - context: homeScaffoldKey.currentContext!, - pageBuilder: (BuildContext context) { - return CupertinoPageScaffold( - key: sheetScaffoldKey, - child: const Center(child: Text('Page 2')), - ); - }, - enableDrag: false, - ); - }, - child: const Text('Push Page 2'), - ), - ], - ), - ), - ), - ); - } - final GlobalKey homeKey = GlobalKey(); final GlobalKey sheetKey = GlobalKey(); - await tester.pumpWidget(nonDragGestureApp(homeKey, sheetKey)); + await tester.pumpWidget(dragGestureApp(homeKey, sheetKey)); await tester.tap(find.text('Push Page 2')); await tester.pumpAndSettle(); @@ -1492,30 +1478,23 @@ void main() { var box = tester.renderObject(find.byKey(sheetKey)) as RenderBox; final double initialPosition = box.localToGlobal(Offset.zero).dy; - final TestGesture gesture = await tester.startGesture(const Offset(100, 200)); - // Partial drag down - await gesture.moveBy(const Offset(0, 200)); + final TestGesture gesture = await tester.startGesture(const Offset(100, 400)); + await gesture.moveBy(const Offset(0, -100)); await tester.pump(); - // Release gesture. Sheet should not move. box = tester.renderObject(find.byKey(sheetKey)) as RenderBox; - final double middlePosition = box.localToGlobal(Offset.zero).dy; - - expect(middlePosition, equals(initialPosition)); + final double stretchedPosition = box.localToGlobal(Offset.zero).dy; + expect(stretchedPosition, lessThan(initialPosition)); await gesture.up(); await tester.pumpAndSettle(); - expect(find.text('Page 2'), findsOneWidget); - box = tester.renderObject(find.byKey(sheetKey)) as RenderBox; final double finalPosition = box.localToGlobal(Offset.zero).dy; - - expect(finalPosition, equals(middlePosition)); - expect(finalPosition, equals(initialPosition)); + expect(finalPosition, initialPosition); }); - testWidgets('partial upward drag stretches and returns without popping', ( + testWidgets('Sheet ignores gestures mid-dismissal and finishes closing', ( WidgetTester tester, ) async { final GlobalKey homeKey = GlobalKey(); @@ -1523,28 +1502,47 @@ void main() { await tester.pumpWidget(dragGestureApp(homeKey, sheetKey)); + // Open sheet await tester.tap(find.text('Push Page 2')); await tester.pumpAndSettle(); - expect(find.text('Page 2'), findsOneWidget); + final Finder sheetFinder = find.byKey(sheetKey); + final Size sheetSize = tester.getSize(sheetFinder); + final double sheetHeight = sheetSize.height; - var box = tester.renderObject(find.byKey(sheetKey)) as RenderBox; - final double initialPosition = box.localToGlobal(Offset.zero).dy; + final double dragDistance = sheetHeight / 1.8; - final TestGesture gesture = await tester.startGesture(const Offset(100, 400)); - await gesture.moveBy(const Offset(0, -100)); - await tester.pump(); + final Offset sheetTopLeft = tester.getTopLeft(sheetFinder); + final startPoint = Offset(sheetTopLeft.dx + (sheetSize.width / 1.8), sheetTopLeft.dy + 20.0); - box = tester.renderObject(find.byKey(sheetKey)) as RenderBox; - final double stretchedPosition = box.localToGlobal(Offset.zero).dy; - expect(stretchedPosition, lessThan(initialPosition)); + // Drag sheet down + final TestGesture gesture = await tester.startGesture(startPoint); + await gesture.moveBy(Offset(0, dragDistance)); + await tester.pump(); + // Release sheet await gesture.up(); + await tester.pump(); + + await tester.pump(const Duration(milliseconds: 50)); + + final box = tester.renderObject(sheetFinder) as RenderBox; + final double currentY = box.localToGlobal(Offset.zero).dy; + + // Try to intercept the gesture by dragging up + final TestGesture interceptGesture = await tester.startGesture( + Offset(startPoint.dx, currentY + 100), + ); + await tester.pump(); + + // Drag up + await interceptGesture.moveBy(const Offset(0, -50)); + await interceptGesture.up(); + await tester.pumpAndSettle(); - box = tester.renderObject(find.byKey(sheetKey)) as RenderBox; - final double finalPosition = box.localToGlobal(Offset.zero).dy; - expect(finalPosition, initialPosition); + expect(find.text('Page 2'), findsNothing); + expect(find.text('Page 1'), findsOneWidget); }); }); From 494ab24198d0ecceaa3f191a1236ed4571845a5e Mon Sep 17 00:00:00 2001 From: xxxOVALxxx Date: Thu, 20 Aug 2026 20:03:43 +0500 Subject: [PATCH 3/3] Code quality and small fixes --- packages/cupertino_ui/lib/src/sheet.dart | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/cupertino_ui/lib/src/sheet.dart b/packages/cupertino_ui/lib/src/sheet.dart index dcaf798d818f..b89a24bd0b1f 100644 --- a/packages/cupertino_ui/lib/src/sheet.dart +++ b/packages/cupertino_ui/lib/src/sheet.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:math' as math; - import 'package:flutter/gestures.dart'; import 'package:flutter/physics.dart'; import 'package:flutter/services.dart'; @@ -41,7 +39,7 @@ const double _kStretchedTopGapRatio = 0.072; // Spring description used for the bounce-back animation when enableDrag == false // or when snapping back to position. Measured on iOS 26 -SpringDescription _sheetSpring = SpringDescription.withDurationAndBounce( +final SpringDescription _sheetSpring = SpringDescription.withDurationAndBounce( duration: const Duration(milliseconds: 430), ); @@ -68,7 +66,8 @@ double _computeRubberBandFriction({ double constant = _kAppleRubberBandElasticity, }) { final double stretchProgress = (currentDisplacement / dimension).clamp(0.0, 1.0); - return constant * math.pow(1.0 - stretchProgress, 2.0); + final double remainingProgress = 1.0 - stretchProgress; + return constant * remainingProgress * remainingProgress; } // Tween for animating a Cupertino sheet onto the screen. @@ -1190,7 +1189,9 @@ class _CupertinoDragGestureController { if (popDragController.isAnimating) { void animationStatusCallback(AnimationStatus status) { if (status == AnimationStatus.completed || status == AnimationStatus.dismissed) { - navigator.didStopUserGesture(); + if (navigator.mounted) { + navigator.didStopUserGesture(); + } popDragController.removeStatusListener(animationStatusCallback); } }