@swmansion/react-native-bottom-sheet 0.9.5 → 0.10.0-next.2
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.
|
@@ -260,12 +260,20 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
260
260
|
|
|
261
261
|
private fun resolveDetentSpecs(): List<DetentSpec> {
|
|
262
262
|
val maxHeight = resolvedMaxDetentHeight()
|
|
263
|
-
val
|
|
264
|
-
validContentHeight().takeIf { it.isFinite() }?.coerceAtMost(maxHeight)
|
|
265
|
-
|
|
263
|
+
val measuredContentHeight =
|
|
264
|
+
validContentHeight().takeIf { maxHeight > 0f && it.isFinite() }?.coerceAtMost(maxHeight)
|
|
265
|
+
val contentHeight = measuredContentHeight ?: maxHeight
|
|
266
|
+
return rawDetentSpecs.mapIndexed { index, spec ->
|
|
266
267
|
val height =
|
|
267
268
|
when (spec.kind) {
|
|
268
|
-
DetentKind.POINTS ->
|
|
269
|
+
DetentKind.POINTS -> {
|
|
270
|
+
if (measuredContentHeight != null && spec.value > contentHeight) {
|
|
271
|
+
throw IllegalArgumentException(
|
|
272
|
+
"Invalid bottom sheet detent at index $index: fixed detent ${spec.value / density} exceeds measured content height ${contentHeight / density}."
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
spec.value
|
|
276
|
+
}
|
|
269
277
|
DetentKind.CONTENT -> contentHeight
|
|
270
278
|
}.coerceIn(0f, maxHeight)
|
|
271
279
|
DetentSpec(height = height, programmatic = spec.programmatic)
|
|
@@ -284,12 +292,15 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
284
292
|
return
|
|
285
293
|
}
|
|
286
294
|
|
|
295
|
+
val previousMaxHeight = detentSpecs.maxOfOrNull { it.height } ?: resolvedMaxDetentHeight()
|
|
287
296
|
detentSpecs = resolvedDetents
|
|
288
297
|
if (width > 0 && height > 0 && detentSpecs.isNotEmpty()) {
|
|
289
298
|
layoutSheetContainer(width, height)
|
|
290
299
|
|
|
291
300
|
if (hasLaidOut && !isPanning) {
|
|
292
301
|
targetIndex = targetIndex.coerceIn(0, detentSpecs.size - 1)
|
|
302
|
+
val newMaxHeight = detentSpecs.maxOfOrNull { it.height } ?: resolvedMaxDetentHeight()
|
|
303
|
+
val targetTy = translationY(targetIndex)
|
|
293
304
|
if (activeAnimation != null && isTargetingClosedDetent) {
|
|
294
305
|
suppressScrimForClosingTarget = true
|
|
295
306
|
hideScrim()
|
|
@@ -301,17 +312,26 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
301
312
|
activeAnimation = null
|
|
302
313
|
activeAnimationEmitsSettle = false
|
|
303
314
|
stopChoreographer()
|
|
304
|
-
|
|
305
|
-
|
|
315
|
+
// Re-anchor the in-flight position to the new container height so the
|
|
316
|
+
// sheet surface keeps the same on-screen height across the resize.
|
|
317
|
+
val visibleHeight = previousMaxHeight - currentTy
|
|
318
|
+
sheetContainer.translationY = (newMaxHeight - visibleHeight).coerceIn(0f, newMaxHeight)
|
|
306
319
|
emitPosition()
|
|
307
320
|
snapToIndex(targetIndex, 0f, emitIndexChange = false, emitSettle = shouldEmitSettle)
|
|
308
321
|
} else {
|
|
309
|
-
val
|
|
310
|
-
val
|
|
311
|
-
if (
|
|
322
|
+
val currentVisibleHeight = previousMaxHeight - sheetContainer.translationY
|
|
323
|
+
val targetHeight = detentSpecs.getOrNull(targetIndex)?.height ?: 0f
|
|
324
|
+
if (targetHeight <= currentVisibleHeight + 0.5f) {
|
|
325
|
+
// Content shrank (or is unchanged): snap immediately. Animating here
|
|
326
|
+
// would expose blank space below the shrunken content.
|
|
312
327
|
sheetContainer.translationY = targetTy
|
|
313
328
|
emitPosition()
|
|
314
329
|
} else {
|
|
330
|
+
// Content grew: re-anchor at the current visible height, then animate
|
|
331
|
+
// up to the taller detent.
|
|
332
|
+
sheetContainer.translationY =
|
|
333
|
+
(newMaxHeight - currentVisibleHeight).coerceIn(0f, newMaxHeight)
|
|
334
|
+
emitPosition()
|
|
315
335
|
snapToIndex(targetIndex, 0f, emitIndexChange = false, emitSettle = false)
|
|
316
336
|
}
|
|
317
337
|
}
|
|
@@ -60,7 +60,8 @@ public final class RNSBottomSheetHostingView: UIView {
|
|
|
60
60
|
private var hasLaidOut = false
|
|
61
61
|
private var isPanning = false
|
|
62
62
|
private var isContentInteractionDisabled = false
|
|
63
|
-
private
|
|
63
|
+
private var contentHeightMarker: UIView?
|
|
64
|
+
private static var markerObservationContext = 0
|
|
64
65
|
|
|
65
66
|
override public init(frame: CGRect) {
|
|
66
67
|
super.init(frame: frame)
|
|
@@ -237,7 +238,7 @@ public final class RNSBottomSheetHostingView: UIView {
|
|
|
237
238
|
hasLaidOut = false
|
|
238
239
|
isPanning = false
|
|
239
240
|
setContentInteractionEnabled(true)
|
|
240
|
-
|
|
241
|
+
stopObservingContentHeightMarker()
|
|
241
242
|
sheetContainer.transform = .identity
|
|
242
243
|
scrimView.alpha = 0
|
|
243
244
|
scrimView.isHidden = true
|
|
@@ -541,12 +542,21 @@ public final class RNSBottomSheetHostingView: UIView {
|
|
|
541
542
|
|
|
542
543
|
private func resolveDetentSpecs() -> [DetentSpec] {
|
|
543
544
|
let maxHeight = resolvedMaxDetentHeight
|
|
544
|
-
let
|
|
545
|
-
|
|
545
|
+
let measuredContentHeight = maxHeight > 0 ? validContentHeight.map { min($0, maxHeight) } : nil
|
|
546
|
+
let contentHeight = measuredContentHeight ?? maxHeight
|
|
547
|
+
return rawDetentSpecs.enumerated().map { index, spec in
|
|
546
548
|
let height: CGFloat
|
|
547
549
|
switch spec.kind {
|
|
548
550
|
case .points:
|
|
549
|
-
|
|
551
|
+
if measuredContentHeight != nil, spec.value > contentHeight {
|
|
552
|
+
NSException(
|
|
553
|
+
name: NSExceptionName.invalidArgumentException,
|
|
554
|
+
reason:
|
|
555
|
+
"Invalid bottom sheet detent at index \(index): fixed detent \(spec.value) exceeds measured content height \(contentHeight).",
|
|
556
|
+
userInfo: nil
|
|
557
|
+
).raise()
|
|
558
|
+
}
|
|
559
|
+
height = spec.value
|
|
550
560
|
case .content:
|
|
551
561
|
height = contentHeight
|
|
552
562
|
}
|
|
@@ -567,6 +577,7 @@ public final class RNSBottomSheetHostingView: UIView {
|
|
|
567
577
|
return
|
|
568
578
|
}
|
|
569
579
|
|
|
580
|
+
let previousMaxHeight = maximumResolvedDetentHeight ?? resolvedMaxDetentHeight
|
|
570
581
|
detentSpecs = resolvedDetents
|
|
571
582
|
|
|
572
583
|
guard bounds.width > 0, bounds.height > 0, !detentSpecs.isEmpty else {
|
|
@@ -575,6 +586,8 @@ public final class RNSBottomSheetHostingView: UIView {
|
|
|
575
586
|
|
|
576
587
|
if hasLaidOut, !isPanning {
|
|
577
588
|
targetIndex = max(0, min(detentSpecs.count - 1, targetIndex))
|
|
589
|
+
let newMaxHeight = maximumResolvedDetentHeight ?? resolvedMaxDetentHeight
|
|
590
|
+
let targetTy = translationY(for: targetIndex)
|
|
578
591
|
|
|
579
592
|
if let animator = activeAnimator {
|
|
580
593
|
stopDisplayLink()
|
|
@@ -583,19 +596,27 @@ public final class RNSBottomSheetHostingView: UIView {
|
|
|
583
596
|
animator.stopAnimation(true)
|
|
584
597
|
activeAnimator = nil
|
|
585
598
|
activeAnimatorEmitsSettle = false
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
)
|
|
599
|
+
// Re-anchor the in-flight position to the new container height so the
|
|
600
|
+
// sheet surface keeps the same on-screen height across the resize.
|
|
601
|
+
let visibleHeight = previousMaxHeight - visualTy
|
|
602
|
+
let reanchoredTy = min(max(newMaxHeight - visibleHeight, 0), newMaxHeight)
|
|
603
|
+
sheetContainer.transform = CGAffineTransform(translationX: 0, y: reanchoredTy)
|
|
590
604
|
emitPosition()
|
|
591
605
|
snapToIndex(targetIndex, velocity: 0, emitIndexChange: false, emitSettle: shouldEmitSettle)
|
|
592
606
|
} else {
|
|
593
|
-
let
|
|
594
|
-
let
|
|
595
|
-
if
|
|
607
|
+
let currentVisibleHeight = previousMaxHeight - currentTranslationY
|
|
608
|
+
let targetHeight = detent(at: targetIndex).height
|
|
609
|
+
if targetHeight <= currentVisibleHeight + 0.5 {
|
|
610
|
+
// Content shrank (or is unchanged): snap immediately. Animating here
|
|
611
|
+
// would expose blank space below the shrunken content.
|
|
596
612
|
sheetContainer.transform = CGAffineTransform(translationX: 0, y: targetTy)
|
|
597
613
|
emitPosition()
|
|
598
614
|
} else {
|
|
615
|
+
// Content grew: re-anchor at the current visible height, then animate
|
|
616
|
+
// up to the taller detent.
|
|
617
|
+
let startTy = min(max(newMaxHeight - currentVisibleHeight, 0), newMaxHeight)
|
|
618
|
+
sheetContainer.transform = CGAffineTransform(translationX: 0, y: startTy)
|
|
619
|
+
emitPosition()
|
|
599
620
|
snapToIndex(targetIndex, velocity: 0, emitIndexChange: false, emitSettle: false)
|
|
600
621
|
}
|
|
601
622
|
}
|
|
@@ -603,7 +624,50 @@ public final class RNSBottomSheetHostingView: UIView {
|
|
|
603
624
|
}
|
|
604
625
|
|
|
605
626
|
private func refreshContentHeightMarker() {
|
|
606
|
-
|
|
627
|
+
let marker = findContentHeightMarker()
|
|
628
|
+
guard marker !== contentHeightMarker else { return }
|
|
629
|
+
stopObservingContentHeightMarker()
|
|
630
|
+
contentHeightMarker = marker
|
|
631
|
+
if let marker {
|
|
632
|
+
// The marker's frame is updated by React Native when content above it
|
|
633
|
+
// resizes; observe its layer so we can re-resolve detents immediately
|
|
634
|
+
// instead of waiting for an unrelated layout pass. This is the iOS
|
|
635
|
+
// counterpart to Android's OnLayoutChangeListener on the marker.
|
|
636
|
+
marker.layer.addObserver(
|
|
637
|
+
self, forKeyPath: "position", options: [], context: &Self.markerObservationContext
|
|
638
|
+
)
|
|
639
|
+
marker.layer.addObserver(
|
|
640
|
+
self, forKeyPath: "bounds", options: [], context: &Self.markerObservationContext
|
|
641
|
+
)
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
private func stopObservingContentHeightMarker() {
|
|
646
|
+
guard let marker = contentHeightMarker else { return }
|
|
647
|
+
marker.layer.removeObserver(
|
|
648
|
+
self, forKeyPath: "position", context: &Self.markerObservationContext
|
|
649
|
+
)
|
|
650
|
+
marker.layer.removeObserver(
|
|
651
|
+
self, forKeyPath: "bounds", context: &Self.markerObservationContext
|
|
652
|
+
)
|
|
653
|
+
contentHeightMarker = nil
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
override public func observeValue(
|
|
657
|
+
forKeyPath keyPath: String?,
|
|
658
|
+
of object: Any?,
|
|
659
|
+
change: [NSKeyValueChangeKey: Any]?,
|
|
660
|
+
context: UnsafeMutableRawPointer?
|
|
661
|
+
) {
|
|
662
|
+
if context == &Self.markerObservationContext {
|
|
663
|
+
refreshDetentsFromLayout()
|
|
664
|
+
} else {
|
|
665
|
+
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
deinit {
|
|
670
|
+
stopObservingContentHeightMarker()
|
|
607
671
|
}
|
|
608
672
|
|
|
609
673
|
private func findContentHeightMarker() -> UIView? {
|
package/package.json
CHANGED