@swmansion/react-native-bottom-sheet 0.15.0-next.2 → 0.15.0-next.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ios/BottomSheetHostingView.swift +59 -81
- package/package.json +1 -1
- package/ios/CriticalSpring.swift +0 -39
|
@@ -68,9 +68,8 @@ public final class BottomSheetHostingView: UIView {
|
|
|
68
68
|
public let sheetContainer = UIView()
|
|
69
69
|
private let scrimView = UIControl()
|
|
70
70
|
private var panGesture: UIPanGestureRecognizer!
|
|
71
|
-
private var
|
|
72
|
-
private var
|
|
73
|
-
private var activeSpringEmitsSettle = false
|
|
71
|
+
private var activeAnimator: UIViewPropertyAnimator?
|
|
72
|
+
private var activeAnimatorEmitsSettle = false
|
|
74
73
|
private var scrimPinnedFull = false
|
|
75
74
|
private var displayLink: CADisplayLink?
|
|
76
75
|
private var pendingIndex: Int?
|
|
@@ -182,13 +181,20 @@ public final class BottomSheetHostingView: UIView {
|
|
|
182
181
|
return
|
|
183
182
|
}
|
|
184
183
|
|
|
185
|
-
if
|
|
184
|
+
if activeAnimator != nil || isPanning { return }
|
|
186
185
|
sheetContainer.transform = CGAffineTransform(translationX: 0, y: translationY(for: targetIndex))
|
|
187
186
|
updateScrim()
|
|
188
187
|
}
|
|
189
188
|
|
|
189
|
+
private var presentedSheetFrame: CGRect {
|
|
190
|
+
if activeAnimator != nil, let presentation = sheetContainer.layer.presentation() {
|
|
191
|
+
return presentation.frame
|
|
192
|
+
}
|
|
193
|
+
return sheetContainer.frame
|
|
194
|
+
}
|
|
195
|
+
|
|
190
196
|
override public func point(inside point: CGPoint, with _: UIEvent?) -> Bool {
|
|
191
|
-
if
|
|
197
|
+
if presentedSheetFrame.contains(point) {
|
|
192
198
|
return true
|
|
193
199
|
}
|
|
194
200
|
|
|
@@ -198,7 +204,7 @@ public final class BottomSheetHostingView: UIView {
|
|
|
198
204
|
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
199
205
|
guard self.point(inside: point, with: event) else { return nil }
|
|
200
206
|
|
|
201
|
-
if isScrimVisible, !
|
|
207
|
+
if isScrimVisible, !presentedSheetFrame.contains(point) {
|
|
202
208
|
let scrimPoint = convert(point, to: scrimView)
|
|
203
209
|
return scrimView.hitTest(scrimPoint, with: event)
|
|
204
210
|
}
|
|
@@ -263,8 +269,8 @@ public final class BottomSheetHostingView: UIView {
|
|
|
263
269
|
}
|
|
264
270
|
|
|
265
271
|
public func resetSheetState() {
|
|
266
|
-
|
|
267
|
-
|
|
272
|
+
activeAnimator?.stopAnimation(true)
|
|
273
|
+
activeAnimator = nil
|
|
268
274
|
stopDisplayLink()
|
|
269
275
|
rawDetentSpecs = []
|
|
270
276
|
detentSpecs = []
|
|
@@ -358,7 +364,7 @@ public final class BottomSheetHostingView: UIView {
|
|
|
358
364
|
|
|
359
365
|
private func startDisplayLink() {
|
|
360
366
|
guard displayLink == nil else { return }
|
|
361
|
-
let link = CADisplayLink(target: self, selector: #selector(displayLinkFired
|
|
367
|
+
let link = CADisplayLink(target: self, selector: #selector(displayLinkFired))
|
|
362
368
|
link.add(to: .main, forMode: .common)
|
|
363
369
|
displayLink = link
|
|
364
370
|
}
|
|
@@ -379,9 +385,8 @@ public final class BottomSheetHostingView: UIView {
|
|
|
379
385
|
isContentInteractionDisabled = !isEnabled
|
|
380
386
|
}
|
|
381
387
|
|
|
382
|
-
@objc private func displayLinkFired(
|
|
383
|
-
|
|
384
|
-
stepSpring(targetTime: link.targetTimestamp)
|
|
388
|
+
@objc private func displayLinkFired() {
|
|
389
|
+
emitPosition()
|
|
385
390
|
}
|
|
386
391
|
|
|
387
392
|
@objc private func handleScrimPress() {
|
|
@@ -389,7 +394,7 @@ public final class BottomSheetHostingView: UIView {
|
|
|
389
394
|
modal,
|
|
390
395
|
let closedIndex,
|
|
391
396
|
targetIndex != closedIndex,
|
|
392
|
-
|
|
397
|
+
activeAnimator == nil || currentSheetHeight > 0.5
|
|
393
398
|
else {
|
|
394
399
|
return
|
|
395
400
|
}
|
|
@@ -413,71 +418,43 @@ public final class BottomSheetHostingView: UIView {
|
|
|
413
418
|
let currentTy = sheetContainer.transform.ty
|
|
414
419
|
let targetTy = translationY(for: index)
|
|
415
420
|
let distance = targetTy - currentTy
|
|
416
|
-
|
|
417
421
|
let velocityRatio = distance != 0 ? velocity / distance : 0
|
|
418
422
|
let clampedRatio = min(max(velocityRatio, -5), 5)
|
|
419
|
-
let
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
423
|
+
let initialVelocity = CGVector(dx: 0, dy: clampedRatio)
|
|
424
|
+
|
|
425
|
+
activeAnimatorEmitsSettle = emitSettle
|
|
426
|
+
activeAnimator?.stopAnimation(true)
|
|
427
|
+
|
|
428
|
+
let spring = UISpringTimingParameters(dampingRatio: 1.0, initialVelocity: initialVelocity)
|
|
429
|
+
let animator = UIViewPropertyAnimator(duration: 0.45, timingParameters: spring)
|
|
430
|
+
|
|
431
|
+
animator.addAnimations {
|
|
432
|
+
self.sheetContainer.transform = CGAffineTransform(translationX: 0, y: targetTy)
|
|
433
|
+
}
|
|
434
|
+
animator.addCompletion { [weak self] position in
|
|
435
|
+
guard let self, position == .end else { return }
|
|
436
|
+
self.stopDisplayLink()
|
|
437
|
+
self.emitPosition()
|
|
438
|
+
self.activeAnimator = nil
|
|
439
|
+
self.activeAnimatorEmitsSettle = false
|
|
440
|
+
self.scrimPinnedFull = false
|
|
441
|
+
self.setContentInteractionEnabled(true)
|
|
442
|
+
self.updateInteractionState()
|
|
443
|
+
if emitSettle {
|
|
444
|
+
self.eventDelegate?.bottomSheetHostingView(self, didSettle: index)
|
|
445
|
+
}
|
|
446
|
+
}
|
|
442
447
|
// Report the index change as soon as the snap is committed, not when it
|
|
443
448
|
// finishes: `targetIndex` is already set, and a programmatic snap's start is
|
|
444
449
|
// known to the caller. `onSettle` remains the signal for movement end.
|
|
445
450
|
if emitIndexChange {
|
|
446
451
|
eventDelegate?.bottomSheetHostingView(self, didChangeIndex: index)
|
|
447
452
|
}
|
|
453
|
+
animator.startAnimation()
|
|
454
|
+
activeAnimator = animator
|
|
448
455
|
startDisplayLink()
|
|
449
456
|
}
|
|
450
457
|
|
|
451
|
-
private func stepSpring(targetTime: CFTimeInterval) {
|
|
452
|
-
guard let spring = activeSpring else { return }
|
|
453
|
-
if spring.isFinished(at: targetTime) {
|
|
454
|
-
finishSpring()
|
|
455
|
-
return
|
|
456
|
-
}
|
|
457
|
-
let ty = spring.value(at: targetTime)
|
|
458
|
-
sheetContainer.transform = CGAffineTransform(translationX: 0, y: ty)
|
|
459
|
-
emitPosition()
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
private func finishSpring() {
|
|
463
|
-
let index = activeSpringTargetIndex
|
|
464
|
-
let emitSettle = activeSpringEmitsSettle
|
|
465
|
-
let targetTy = translationY(for: index)
|
|
466
|
-
|
|
467
|
-
activeSpring = nil
|
|
468
|
-
activeSpringEmitsSettle = false
|
|
469
|
-
stopDisplayLink()
|
|
470
|
-
|
|
471
|
-
sheetContainer.transform = CGAffineTransform(translationX: 0, y: targetTy)
|
|
472
|
-
emitPosition()
|
|
473
|
-
scrimPinnedFull = false
|
|
474
|
-
setContentInteractionEnabled(true)
|
|
475
|
-
updateInteractionState()
|
|
476
|
-
if emitSettle {
|
|
477
|
-
eventDelegate?.bottomSheetHostingView(self, didSettle: index)
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
458
|
@objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
|
|
482
459
|
let maxHeight = sheetContainerHeight
|
|
483
460
|
|
|
@@ -493,10 +470,12 @@ public final class BottomSheetHostingView: UIView {
|
|
|
493
470
|
handler.isEnabled = true
|
|
494
471
|
}
|
|
495
472
|
gesture.setTranslation(.zero, in: self)
|
|
496
|
-
if
|
|
473
|
+
if let animator = activeAnimator {
|
|
497
474
|
stopDisplayLink()
|
|
498
|
-
|
|
499
|
-
|
|
475
|
+
let visual = sheetContainer.layer.presentation()?.affineTransform() ?? sheetContainer.transform
|
|
476
|
+
animator.stopAnimation(true)
|
|
477
|
+
sheetContainer.transform = visual
|
|
478
|
+
activeAnimator = nil
|
|
500
479
|
}
|
|
501
480
|
|
|
502
481
|
case .changed:
|
|
@@ -714,13 +693,13 @@ public final class BottomSheetHostingView: UIView {
|
|
|
714
693
|
let newMaxHeight = sheetContainerHeight
|
|
715
694
|
let targetTy = translationY(for: targetIndex)
|
|
716
695
|
|
|
717
|
-
if
|
|
696
|
+
if let animator = activeAnimator {
|
|
718
697
|
stopDisplayLink()
|
|
719
|
-
|
|
720
|
-
let
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
698
|
+
let visualTy = sheetContainer.layer.presentation()?.affineTransform().ty ?? sheetContainer.transform.ty
|
|
699
|
+
let shouldEmitSettle = activeAnimatorEmitsSettle
|
|
700
|
+
animator.stopAnimation(true)
|
|
701
|
+
activeAnimator = nil
|
|
702
|
+
activeAnimatorEmitsSettle = false
|
|
724
703
|
// Re-anchor the in-flight position to the new container height so the
|
|
725
704
|
// sheet surface keeps the same on-screen height across the resize.
|
|
726
705
|
let visibleHeight = previousMaxHeight - visualTy
|
|
@@ -861,10 +840,9 @@ extension BottomSheetHostingView: UIGestureRecognizerDelegate {
|
|
|
861
840
|
|
|
862
841
|
private extension BottomSheetHostingView {
|
|
863
842
|
var currentTranslationY: CGFloat {
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
// render server and we'd have to read `layer.presentation()` here.)
|
|
843
|
+
if activeAnimator != nil, let presentation = sheetContainer.layer.presentation() {
|
|
844
|
+
return presentation.affineTransform().ty
|
|
845
|
+
}
|
|
868
846
|
return sheetContainer.transform.ty
|
|
869
847
|
}
|
|
870
848
|
|
|
@@ -890,7 +868,7 @@ private extension BottomSheetHostingView {
|
|
|
890
868
|
if
|
|
891
869
|
let closedIndex,
|
|
892
870
|
targetIndex == closedIndex,
|
|
893
|
-
|
|
871
|
+
activeAnimator == nil,
|
|
894
872
|
!isPanning
|
|
895
873
|
{
|
|
896
874
|
scrimView.alpha = 0
|
package/package.json
CHANGED
package/ios/CriticalSpring.swift
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import QuartzCore
|
|
2
|
-
|
|
3
|
-
/// A spring we evaluate ourselves each frame instead of letting
|
|
4
|
-
/// `UIViewPropertyAnimator` run the settle. The animator runs on the "render
|
|
5
|
-
/// server", so its value is only readable one frame late via `presentation()` —
|
|
6
|
-
/// the cause of the follower view (fed by `onPositionChange`) trailing the
|
|
7
|
-
/// modal. Computing it in-process lets us emit the exact value we set.
|
|
8
|
-
/// Critically damped (ζ = 1): reaches the target as fast as possible without
|
|
9
|
-
/// overshooting.
|
|
10
|
-
struct CriticalSpring {
|
|
11
|
-
let from: CGFloat
|
|
12
|
-
let target: CGFloat
|
|
13
|
-
/// Initial velocity (points/sec) — e.g. carried over from a finger flick.
|
|
14
|
-
let v0: CGFloat
|
|
15
|
-
/// Angular frequency (rad/sec) — the spring's stiffness/speed. Higher ω snaps faster;
|
|
16
|
-
let omega: CGFloat
|
|
17
|
-
let startTime: CFTimeInterval
|
|
18
|
-
let duration: CFTimeInterval
|
|
19
|
-
|
|
20
|
-
/// Position at an absolute `time`, from the closed-form solution of a
|
|
21
|
-
/// critically-damped spring (ζ = 1):
|
|
22
|
-
/// x(t) = target + e^(−ω·t)·[A + (v0 + ω·A)·t]
|
|
23
|
-
/// where A is the starting offset from the target. The `e^(−ω·t)` term decays
|
|
24
|
-
/// the offset toward 0 (so x → target), and the linear `…·t` factor is what
|
|
25
|
-
/// lets a critically-damped spring carry initial velocity without oscillating.
|
|
26
|
-
func value(at time: CFTimeInterval) -> CGFloat {
|
|
27
|
-
// Seconds since the spring started (clamped so a past `time` reads as t = 0).
|
|
28
|
-
let t = CGFloat(max(0, time - startTime))
|
|
29
|
-
// A: how far `from` is from `target` — the offset the spring must close.
|
|
30
|
-
let a = from - target
|
|
31
|
-
// Exponential envelope: 1 at t = 0, shrinking toward 0 as time passes.
|
|
32
|
-
let decay = exp(-omega * t)
|
|
33
|
-
return target + decay * (a + (v0 + omega * a) * t)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
func isFinished(at time: CFTimeInterval) -> Bool {
|
|
37
|
-
time - startTime >= duration
|
|
38
|
-
}
|
|
39
|
-
}
|