Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 121 additions & 16 deletions packages/cupertino_ui/lib/src/sheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// found in the LICENSE file.

import 'package:flutter/gestures.dart';
import 'package:flutter/physics.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';

Expand Down Expand Up @@ -36,6 +37,39 @@ 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
final 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);
final double remainingProgress = 1.0 - stretchProgress;
return constant * remainingProgress * remainingProgress;
}

// Tween for animating a Cupertino sheet onto the screen.
//
// Begins fully offscreen below the screen and ends onscreen with a small gap at
Expand Down Expand Up @@ -776,9 +810,14 @@ class CupertinoSheetRoute<T> extends PageRoute<T> with _CupertinoSheetRouteTrans
data: CupertinoUserInterfaceLevelData.elevated,
child: _CupertinoSheetScope(
child: _CupertinoDraggableScrollableSheet<T>(
enabledCallback: () => enableDrag,
onStartPopGesture: () =>
_CupertinoSheetRouteTransitionMixin._startPopGesture<T>(this, topGap),
enabledCallback: () => !(controller?.isAnimating ?? false),
enableDrag: enableDrag,
onStartPopGesture: () => _CupertinoSheetRouteTransitionMixin._startPopGesture<T>(
this,
topGap,
enableDrag: enableDrag,
),
topGap: topGap,
builder: _sheetWithDragHandle,
),
),
Expand Down Expand Up @@ -878,14 +917,16 @@ mixin _CupertinoSheetRouteTransitionMixin<T> on PageRoute<T> {

static _CupertinoDragGestureController<T> _startPopGesture<T>(
ModalRoute<T> route,
double topGap,
) {
double topGap, {
required bool enableDrag,
}) {
return _CupertinoDragGestureController<T>(
topGap: topGap,
navigator: route.navigator!,
getIsCurrent: () => route.isCurrent,
getIsActive: () => route.isActive,
popDragController: route.controller!, // protected access
enableDrag: enableDrag,
);
}

Expand All @@ -906,8 +947,8 @@ mixin _CupertinoSheetRouteTransitionMixin<T> on PageRoute<T> {
linearTransition: linearTransition,
topGap: topGap,
child: _CupertinoDragGestureDetector<T>(
enabledCallback: () => enableDrag,
onStartPopGesture: () => _startPopGesture<T>(route, topGap),
enabledCallback: () => !(route.controller?.isAnimating ?? false),
onStartPopGesture: () => _startPopGesture<T>(route, topGap, enableDrag: enableDrag),
child: child,
),
);
Expand Down Expand Up @@ -1023,9 +1064,9 @@ class _CupertinoDragGestureDetectorState<T> 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,
);
}

Expand Down Expand Up @@ -1079,6 +1120,7 @@ class _CupertinoDragGestureController<T> {
required this.getIsActive,
required this.getIsCurrent,
required this.topGap,
required this.enableDrag,
}) {
navigator.didStartUserGesture();
}
Expand All @@ -1088,10 +1130,11 @@ class _CupertinoDragGestureController<T> {
final ValueGetter<bool> getIsActive;
final ValueGetter<bool> 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)) {
Expand All @@ -1100,7 +1143,17 @@ class _CupertinoDragGestureController<T> {
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;
}
}
}

Expand All @@ -1123,6 +1176,33 @@ class _CupertinoDragGestureController<T> {
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) {
if (navigator.mounted) {
navigator.didStopUserGesture();
}
popDragController.removeStatusListener(animationStatusCallback);
}
}
Comment thread
xxxOVALxxx marked this conversation as resolved.

popDragController.addStatusListener(animationStatusCallback);
} else {
navigator.didStopUserGesture();
}
return;
}

// Fling in the appropriate direction.
//
// This curve has been determined through rigorously eyeballing native iOS
Expand Down Expand Up @@ -1195,12 +1275,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<bool> hasActiveDragController;

@override
_CupertinoSheetScrollPosition createScrollPosition(
Expand All @@ -1216,6 +1298,7 @@ class _CupertinoSheetScrollController extends ScrollController {
onDragUpdate: onDragUpdate,
onDragEnd: onDragEnd,
sheetIsDraggedDown: sheetIsDraggedDown,
hasActiveDragController: hasActiveDragController,
);
}
}
Expand All @@ -1241,6 +1324,7 @@ class _CupertinoSheetScrollPosition extends ScrollPositionWithSingleContext {
required this.onDragUpdate,
required this.onDragEnd,
required this.sheetIsDraggedDown,
required this.hasActiveDragController,
});

VoidCallback? _dragCancelCallback;
Expand All @@ -1252,6 +1336,7 @@ class _CupertinoSheetScrollPosition extends ScrollPositionWithSingleContext {
final _DragUpdateCallback onDragEnd;

final _GetSheetDragged sheetIsDraggedDown;
final ValueGetter<bool> hasActiveDragController;

@override
void absorb(ScrollPosition other) {
Expand Down Expand Up @@ -1289,7 +1374,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);
Expand All @@ -1309,9 +1395,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);
Expand All @@ -1329,14 +1421,20 @@ class _CupertinoDraggableScrollableSheet<T> 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<bool> enabledCallback;

final bool enableDrag;

final double topGap;

final ValueGetter<_CupertinoDragGestureController<T>> onStartPopGesture;

@override
Expand All @@ -1357,6 +1455,7 @@ class _CupertinoDraggableScrollableSheetState<T>
onDragUpdate: _dragUpdate,
onDragEnd: _handleDragEnd,
sheetIsDraggedDown: () => _dragGestureController?.isDragged() ?? false,
hasActiveDragController: () => _dragGestureController != null,
);
}

Expand All @@ -1377,23 +1476,29 @@ class _CupertinoDraggableScrollableSheetState<T>

void _dragStart() {
assert(mounted);
if (!widget.enabledCallback()) {
return;
}
_dragGestureController ??= widget.onStartPopGesture();
}

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,
);
}
}

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;
}
}
Expand Down
Loading