@lodev09/react-native-true-sheet 4.0.0-beta.25 → 4.0.0-beta.27
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/README.md +1 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +19 -1
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayView.kt +1 -1
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetStateUpdater.kt +11 -3
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +31 -10
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +23 -3
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +5 -0
- package/android/src/main/java/com/lodev09/truesheet/core/RNScreensEventObserver.kt +8 -16
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetBehavior.kt +9 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +0 -8
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetOverlayRootView.kt +8 -1
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetStackManager.kt +29 -38
- package/android/src/main/jni/TrueSheetStateUpdater.cpp +37 -6
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.cpp +6 -2
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.cpp +12 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.h +2 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.h +8 -2
- package/ios/TrueSheetContainerView.h +1 -0
- package/ios/TrueSheetContainerView.mm +6 -0
- package/ios/TrueSheetContentView.h +1 -0
- package/ios/TrueSheetContentView.mm +2 -0
- package/ios/TrueSheetFooterView.mm +32 -12
- package/ios/TrueSheetView.mm +9 -2
- package/ios/TrueSheetViewController.h +12 -0
- package/ios/TrueSheetViewController.mm +332 -29
- package/ios/core/TrueSheetDetentCalculator.h +11 -0
- package/ios/core/TrueSheetDetentCalculator.mm +17 -5
- package/ios/core/TrueSheetKeyboardObserver.mm +89 -9
- package/lib/module/fabric/TrueSheetViewNativeComponent.ts +6 -0
- package/lib/typescript/src/TrueSheet.types.d.ts +31 -0
- package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +5 -0
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/navigation/types.d.ts +1 -1
- package/lib/typescript/src/navigation/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/TrueSheet.types.ts +34 -0
- package/src/fabric/TrueSheetViewNativeComponent.ts +6 -0
- package/src/navigation/types.ts +1 -0
package/README.md
CHANGED
|
@@ -35,6 +35,7 @@ The true native bottom sheet experience for your React Native Apps. 💩
|
|
|
35
35
|
* 🍞 **`TrueSheetOverlay`** - Render toasts, dialogs, and other [overlays above your sheets](https://sheet.lodev09.com/guides/overlays) — no more `FullWindowOverlay`/`Modal` workarounds
|
|
36
36
|
* 🔍 **Element inspector** - React Native's dev menu inspector now works inside a presented sheet
|
|
37
37
|
* 🎨 **Dim color and opacity** - [Customize the dim](https://sheet.lodev09.com/guides/dimming#dim-color-and-opacity) behind the sheet with `dimColor` and `dimOpacity`
|
|
38
|
+
* 📱 **Foldables** - [Rest the sheet on the hinge](https://sheet.lodev09.com/guides/foldables) of a half-folded iPhone with `foldOptions` (iOS 27.1+)
|
|
38
39
|
|
|
39
40
|
```sh
|
|
40
41
|
npx expo install @lodev09/react-native-true-sheet@beta
|
|
@@ -32,10 +32,24 @@ class TrueSheetFooterView(reactContext: ThemedReactContext) : ReactViewGroup(rea
|
|
|
32
32
|
|
|
33
33
|
private var lastWidth = 0
|
|
34
34
|
private var lastHeight = 0
|
|
35
|
+
private var stateTranslationY = 0f
|
|
35
36
|
|
|
36
37
|
var bottomInset = 0
|
|
37
38
|
private set
|
|
38
39
|
|
|
40
|
+
/**
|
|
41
|
+
* positionFooter slides the footer off its Yoga position (above the
|
|
42
|
+
* keyboard, or riding the sheet). Mirror where it rests into the shadow
|
|
43
|
+
* node so measure() — and Pressability's press region — match where it's
|
|
44
|
+
* drawn. Called once the footer settles: every push is a commit.
|
|
45
|
+
*/
|
|
46
|
+
fun syncTranslationState() {
|
|
47
|
+
if (translationY == stateTranslationY) return
|
|
48
|
+
val sw = stateWrapper ?: return
|
|
49
|
+
stateTranslationY = translationY
|
|
50
|
+
TrueSheetStateUpdater.updateFooterTranslation(sw, translationY.pxToDp())
|
|
51
|
+
}
|
|
52
|
+
|
|
39
53
|
/**
|
|
40
54
|
* Height the footer occupies above the keyboard — its layout height minus
|
|
41
55
|
* the safe-area inset, which rides below the keyboard's top edge while the
|
|
@@ -55,14 +69,18 @@ class TrueSheetFooterView(reactContext: ThemedReactContext) : ReactViewGroup(rea
|
|
|
55
69
|
|
|
56
70
|
val sw = stateWrapper ?: return
|
|
57
71
|
val insetDp = bottomInset.toFloat().pxToDp()
|
|
72
|
+
// Carried along — this update replaces a still-queued translation update
|
|
73
|
+
stateTranslationY = translationY
|
|
74
|
+
val translateYDp = translationY.pxToDp()
|
|
58
75
|
|
|
59
76
|
// Synchronous update — the footer must be padded before detents are
|
|
60
77
|
// configured, otherwise the auto detent is set up an inset short
|
|
61
|
-
if (TrueSheetStateUpdater.updateFooterState(sw, insetDp)) return
|
|
78
|
+
if (TrueSheetStateUpdater.updateFooterState(sw, insetDp, translateYDp)) return
|
|
62
79
|
|
|
63
80
|
// Fallback: async state update
|
|
64
81
|
val newState = WritableNativeMap()
|
|
65
82
|
newState.putDouble("bottomInset", insetDp.toDouble())
|
|
83
|
+
newState.putDouble("translateY", translateYDp.toDouble())
|
|
66
84
|
sw.updateState(newState)
|
|
67
85
|
}
|
|
68
86
|
|
|
@@ -67,7 +67,7 @@ class TrueSheetOverlayView(private val reactContext: ThemedReactContext) :
|
|
|
67
67
|
|
|
68
68
|
override fun onDetachedFromWindow() {
|
|
69
69
|
viewTreeObserver.removeOnWindowAttachListener(windowAttachListener)
|
|
70
|
-
detachRootView()
|
|
70
|
+
rootContainerView?.post { if (!isAttachedToWindow) detachRootView() }
|
|
71
71
|
super.onDetachedFromWindow()
|
|
72
72
|
}
|
|
73
73
|
|
|
@@ -41,8 +41,13 @@ object TrueSheetStateUpdater {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/** Returns false when unavailable — caller should fall back to async updateState. */
|
|
44
|
-
fun updateFooterState(stateWrapper: StateWrapper, bottomInsetDp: Float): Boolean =
|
|
45
|
-
isAvailable && nativeUpdateFooterState(stateWrapper, bottomInsetDp)
|
|
44
|
+
fun updateFooterState(stateWrapper: StateWrapper, bottomInsetDp: Float, translateYDp: Float): Boolean =
|
|
45
|
+
isAvailable && nativeUpdateFooterState(stateWrapper, bottomInsetDp, translateYDp)
|
|
46
|
+
|
|
47
|
+
/** Mirrors the footer's native translation so measure() matches where it's drawn. */
|
|
48
|
+
fun updateFooterTranslation(stateWrapper: StateWrapper, translateYDp: Float) {
|
|
49
|
+
if (isAvailable) nativeUpdateFooterTranslation(stateWrapper, translateYDp)
|
|
50
|
+
}
|
|
46
51
|
|
|
47
52
|
/**
|
|
48
53
|
* Publishes the precalculated bottom safe-area inset so a late-mounted
|
|
@@ -56,7 +61,10 @@ object TrueSheetStateUpdater {
|
|
|
56
61
|
private external fun nativeUpdateState(stateWrapper: Any, width: Float, height: Float): Boolean
|
|
57
62
|
|
|
58
63
|
@JvmStatic
|
|
59
|
-
private external fun nativeUpdateFooterState(stateWrapper: Any, bottomInset: Float): Boolean
|
|
64
|
+
private external fun nativeUpdateFooterState(stateWrapper: Any, bottomInset: Float, translateY: Float): Boolean
|
|
65
|
+
|
|
66
|
+
@JvmStatic
|
|
67
|
+
private external fun nativeUpdateFooterTranslation(stateWrapper: Any, translateY: Float)
|
|
60
68
|
|
|
61
69
|
@JvmStatic
|
|
62
70
|
private external fun nativeSetBottomSafeArea(insetDp: Float)
|
|
@@ -72,6 +72,9 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
72
72
|
// Debounce flag to coalesce rapid layout changes into a single sheet update
|
|
73
73
|
private var isSheetUpdatePending: Boolean = false
|
|
74
74
|
|
|
75
|
+
// didFocus waiting for the slide-back after a child dismissed
|
|
76
|
+
private var isDidFocusPending: Boolean = false
|
|
77
|
+
|
|
75
78
|
// Root container for the coordinator layout (activity or Modal dialog content view)
|
|
76
79
|
internal var rootContainerView: ViewGroup? = null
|
|
77
80
|
|
|
@@ -610,7 +613,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
610
613
|
|
|
611
614
|
// Propagate any additional translation up the stack
|
|
612
615
|
if (additionalTranslation > 0) {
|
|
613
|
-
|
|
616
|
+
viewController.parentSheetView?.addTranslation(additionalTranslation)
|
|
614
617
|
}
|
|
615
618
|
}
|
|
616
619
|
|
|
@@ -642,7 +645,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
642
645
|
|
|
643
646
|
val additionalTranslation = newTranslation - currentTranslation
|
|
644
647
|
if (additionalTranslation > 0) {
|
|
645
|
-
|
|
648
|
+
viewController.parentSheetView?.addTranslation(additionalTranslation, animated = false)
|
|
646
649
|
}
|
|
647
650
|
}
|
|
648
651
|
|
|
@@ -653,7 +656,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
653
656
|
if (viewController.isExpanded) return
|
|
654
657
|
|
|
655
658
|
viewController.translateSheet(viewController.currentTranslationY + amount, animated)
|
|
656
|
-
|
|
659
|
+
viewController.parentSheetView?.addTranslation(amount, animated)
|
|
657
660
|
}
|
|
658
661
|
|
|
659
662
|
/**
|
|
@@ -666,7 +669,25 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
666
669
|
|
|
667
670
|
// Parent should recalculate its translation based on this sheet's position
|
|
668
671
|
val mySheetTop = viewController.detentCalculator.getSheetTopForDetentIndex(viewController.currentDetentIndex)
|
|
669
|
-
|
|
672
|
+
viewController.parentSheetView?.updateTranslationForChild(mySheetTop)
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Resets translation after a child dismissed and emits didFocus once the sheet settles.
|
|
677
|
+
* A sheet presented on top before then cancels the slide-back, so the next blur flushes it.
|
|
678
|
+
*/
|
|
679
|
+
private fun focusAfterResetTranslation() {
|
|
680
|
+
isDidFocusPending = true
|
|
681
|
+
resetTranslation { emitPendingDidFocus() }
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
private fun emitPendingDidFocus() {
|
|
685
|
+
if (!isDidFocusPending) return
|
|
686
|
+
isDidFocusPending = false
|
|
687
|
+
|
|
688
|
+
if (viewController.isPresented && !viewController.isBeingDismissed) {
|
|
689
|
+
viewControllerDidFocus()
|
|
690
|
+
}
|
|
670
691
|
}
|
|
671
692
|
|
|
672
693
|
// ==================== TrueSheetViewControllerDelegate ====================
|
|
@@ -701,6 +722,9 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
701
722
|
}
|
|
702
723
|
|
|
703
724
|
override fun viewControllerDidDismiss(parent: TrueSheetView?) {
|
|
725
|
+
// A slide-back cut short by this sheet's own dismissal never emits; don't carry it over
|
|
726
|
+
isDidFocusPending = false
|
|
727
|
+
|
|
704
728
|
// Detach coordinator from the root container view
|
|
705
729
|
viewController.coordinatorLayout?.let { rootContainerView?.removeView(it) }
|
|
706
730
|
rootContainerView = null
|
|
@@ -715,12 +739,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
715
739
|
// The sheets below stop blending this sheet's dim
|
|
716
740
|
parent?.viewController?.takeIf { it.isPresented }?.updateDimAmount()
|
|
717
741
|
|
|
718
|
-
parent?.
|
|
719
|
-
val parentController = parent.viewController
|
|
720
|
-
if (parentController.isPresented && !parentController.isBeingDismissed) {
|
|
721
|
-
parent.viewControllerDidFocus()
|
|
722
|
-
}
|
|
723
|
-
}
|
|
742
|
+
parent?.focusAfterResetTranslation()
|
|
724
743
|
}
|
|
725
744
|
|
|
726
745
|
override fun viewControllerDidChangeDetent(index: Int, position: Float, detent: Float) {
|
|
@@ -763,6 +782,8 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
763
782
|
}
|
|
764
783
|
|
|
765
784
|
override fun viewControllerWillBlur() {
|
|
785
|
+
emitPendingDidFocus()
|
|
786
|
+
|
|
766
787
|
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
|
767
788
|
eventDispatcher?.dispatchEvent(WillBlurEvent(surfaceId, id))
|
|
768
789
|
}
|
|
@@ -374,6 +374,13 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
374
374
|
private val isKeyboardTransitioning: Boolean
|
|
375
375
|
get() = keyboardObserver?.isTransitioning ?: false
|
|
376
376
|
|
|
377
|
+
// The footer slides every frame until the sheet and keyboard come to rest
|
|
378
|
+
private val isSheetMoving: Boolean
|
|
379
|
+
get() = isPresentAnimating ||
|
|
380
|
+
isKeyboardTransitioning ||
|
|
381
|
+
behavior?.state == BottomSheetBehavior.STATE_DRAGGING ||
|
|
382
|
+
behavior?.state == BottomSheetBehavior.STATE_SETTLING
|
|
383
|
+
|
|
377
384
|
// Keyboard is up or animating — detents are (or are about to be) keyboard-shifted
|
|
378
385
|
val isKeyboardActive: Boolean
|
|
379
386
|
get() = keyboardInset > 0 || isKeyboardTransitioning
|
|
@@ -724,6 +731,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
724
731
|
}
|
|
725
732
|
|
|
726
733
|
private fun handleStateSettled(sheetView: View, newState: Int) {
|
|
734
|
+
// The settle's last slide frame has positioned the footer
|
|
735
|
+
containerView?.footerView?.syncTranslationState()
|
|
736
|
+
|
|
727
737
|
if (interactionState is InteractionState.Reconfiguring) return
|
|
728
738
|
|
|
729
739
|
// A release already at a stop settles without a slide frame
|
|
@@ -886,6 +896,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
886
896
|
setupSheetDetents()
|
|
887
897
|
setupDimmedBackground()
|
|
888
898
|
setupKeyboardObserver()
|
|
899
|
+
// Before the slide, so an input focused in onWillPresent insets the scrollable
|
|
900
|
+
containerView?.setupKeyboardHandler()
|
|
889
901
|
|
|
890
902
|
displayedBackgroundIndex = detentIndex
|
|
891
903
|
setupBackground(animated = false)
|
|
@@ -1008,11 +1020,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1008
1020
|
// Restore isHideable to actual value after present animation
|
|
1009
1021
|
behavior?.isHideable = dismissible
|
|
1010
1022
|
|
|
1011
|
-
containerView?.setupKeyboardHandler()
|
|
1012
|
-
|
|
1013
1023
|
val (index, position, detent) = getDetentInfoWithValue(currentDetentIndex)
|
|
1014
|
-
delegate?.viewControllerDidPresent(index, position, detent)
|
|
1015
1024
|
parentSheetView?.viewControllerDidBlur()
|
|
1025
|
+
delegate?.viewControllerDidPresent(index, position, detent)
|
|
1016
1026
|
delegate?.viewControllerDidFocus()
|
|
1017
1027
|
sheetView?.updateGrabberAccessibilityValue(index, detents.size)
|
|
1018
1028
|
|
|
@@ -1288,6 +1298,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1288
1298
|
// absolute footer floats above it
|
|
1289
1299
|
if (!absoluteFooter) {
|
|
1290
1300
|
footerView.translationY = 0f
|
|
1301
|
+
footerView.syncTranslationState()
|
|
1291
1302
|
return
|
|
1292
1303
|
}
|
|
1293
1304
|
|
|
@@ -1313,6 +1324,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1313
1324
|
// Clamp to prevent footer going above safe area
|
|
1314
1325
|
val maxAllowedY = (sheetHeight - topInset - footerHeight).toFloat()
|
|
1315
1326
|
footerView.y = minOf(footerY, maxAllowedY)
|
|
1327
|
+
// Mid-motion slides are synced once the sheet settles or the keyboard
|
|
1328
|
+
// lands (handleStateSettled, keyboardDidShow/keyboardDidHide)
|
|
1329
|
+
if (!isSheetMoving) footerView.syncTranslationState()
|
|
1316
1330
|
if (!footerAvoidsKeyboard) {
|
|
1317
1331
|
containerView?.contentView?.updateContentInset()
|
|
1318
1332
|
}
|
|
@@ -1403,7 +1417,13 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1403
1417
|
)
|
|
1404
1418
|
}
|
|
1405
1419
|
|
|
1420
|
+
override fun keyboardDidShow(height: Int) {
|
|
1421
|
+
// The keyboard's last frame has positioned the footer
|
|
1422
|
+
containerView?.footerView?.syncTranslationState()
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1406
1425
|
override fun keyboardDidHide() {
|
|
1426
|
+
containerView?.footerView?.syncTranslationState()
|
|
1407
1427
|
if (!shouldHandleKeyboard(checkFocus = false)) return
|
|
1408
1428
|
isKeyboardDismissProgrammatic = false
|
|
1409
1429
|
// Keep detentIndexBeforeKeyboard for the settle's detent-change check
|
|
@@ -253,6 +253,11 @@ class TrueSheetViewManager :
|
|
|
253
253
|
// iOS-specific prop - no-op on Android
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
+
@ReactProp(name = "foldOptions")
|
|
257
|
+
override fun setFoldOptions(view: TrueSheetView, options: ReadableMap?) {
|
|
258
|
+
// iOS-specific prop - no-op on Android
|
|
259
|
+
}
|
|
260
|
+
|
|
256
261
|
@ReactProp(name = "insetAdjustment")
|
|
257
262
|
override fun setInsetAdjustment(view: TrueSheetView, insetAdjustment: String?) {
|
|
258
263
|
view.setInsetAdjustment(insetAdjustment ?: "automatic")
|
|
@@ -44,22 +44,9 @@ class RNScreensEventObserver : EventDispatcherListener {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
fun capturePresenterScreenFromView(view: View?) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
var current: View? = view
|
|
51
|
-
while (current != null) {
|
|
52
|
-
if (isScreenView(current)) {
|
|
53
|
-
if (presenterScreenTag == 0) {
|
|
54
|
-
presenterScreenTag = current.id
|
|
55
|
-
} else {
|
|
56
|
-
ancestors.add(current.id)
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
current = (current.parent as? View)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
ancestorScreenTags = ancestors
|
|
47
|
+
val tags = screenTags(view).toList()
|
|
48
|
+
presenterScreenTag = tags.firstOrNull() ?: 0
|
|
49
|
+
ancestorScreenTags = tags.drop(1).toSet()
|
|
63
50
|
}
|
|
64
51
|
|
|
65
52
|
override fun onEventDispatch(event: Event<*>) {
|
|
@@ -84,5 +71,10 @@ class RNScreensEventObserver : EventDispatcherListener {
|
|
|
84
71
|
|
|
85
72
|
companion object {
|
|
86
73
|
private fun isScreenView(view: View): Boolean = view.javaClass.name == RN_SCREENS_VIEW_CLASS
|
|
74
|
+
|
|
75
|
+
// Tags of the screens enclosing the view, innermost first
|
|
76
|
+
private fun screenTags(view: View?): Sequence<Int> = generateSequence(view) { it.parent as? View }.filter(::isScreenView).map { it.id }
|
|
77
|
+
|
|
78
|
+
fun findPresenterScreenTag(view: View?): Int = screenTags(view).firstOrNull() ?: 0
|
|
87
79
|
}
|
|
88
80
|
}
|
|
@@ -40,6 +40,15 @@ class TrueSheetBottomSheetBehavior<V : View> : BottomSheetBehavior<V>() {
|
|
|
40
40
|
hideFriction = HIDE_FRICTION
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
// Material's first layout registers an IME callback that rides the sheet's translationY
|
|
44
|
+
// with the keyboard. TrueSheet positions sheets for the keyboard itself, and the callback
|
|
45
|
+
// overwrites a parent's stack translation when the keyboard rises mid-translation.
|
|
46
|
+
override fun onLayoutChild(parent: CoordinatorLayout, child: V, layoutDirection: Int): Boolean {
|
|
47
|
+
val handled = super.onLayoutChild(parent, child, layoutDirection)
|
|
48
|
+
ViewCompat.setWindowInsetsAnimationCallback(child, null)
|
|
49
|
+
return handled
|
|
50
|
+
}
|
|
51
|
+
|
|
43
52
|
// With the short threshold, settle at or below the lowest detent like
|
|
44
53
|
// Compose Material3's sheet instead of Material's half-height rule.
|
|
45
54
|
private fun releaseState(child: V, dragged: Boolean, velocityY: Float): Int? {
|
|
@@ -81,14 +81,6 @@ class TrueSheetBottomSheetView(private val reactContext: ThemedReactContext) : F
|
|
|
81
81
|
clipToPadding = false
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
override fun setTranslationY(translationY: Float) {
|
|
85
|
-
// This prevents keyboard inset animations from resetting parent sheet translation
|
|
86
|
-
if (translationY == 0f && this.translationY != 0f) {
|
|
87
|
-
return
|
|
88
|
-
}
|
|
89
|
-
super.setTranslationY(translationY)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
84
|
// =============================================================================
|
|
93
85
|
// MARK: - Layout
|
|
94
86
|
// =============================================================================
|
|
@@ -48,7 +48,14 @@ class TrueSheetOverlayRootView(private val reactContext: ThemedReactContext) :
|
|
|
48
48
|
translationZ = OVERLAY_Z
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
// Children are positioned by Fabric — a FrameLayout pass would stretch them to match_parent
|
|
51
|
+
// Children are measured and positioned by Fabric — a FrameLayout pass would stretch them to match_parent
|
|
52
|
+
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
53
|
+
setMeasuredDimension(
|
|
54
|
+
MeasureSpec.getSize(widthMeasureSpec),
|
|
55
|
+
MeasureSpec.getSize(heightMeasureSpec)
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
52
59
|
override fun onLayout(
|
|
53
60
|
changed: Boolean,
|
|
54
61
|
left: Int,
|
|
@@ -12,16 +12,6 @@ object TrueSheetStackManager {
|
|
|
12
12
|
|
|
13
13
|
private val presentedSheetStack = mutableListOf<TrueSheetView>()
|
|
14
14
|
|
|
15
|
-
/**
|
|
16
|
-
* Gets the parent sheet at the given index, if any.
|
|
17
|
-
* Only returns a parent if it's in the same container.
|
|
18
|
-
* Must be called within synchronized block.
|
|
19
|
-
*/
|
|
20
|
-
private fun getParentSheetAt(index: Int, rootContainer: ViewGroup?): TrueSheetView? {
|
|
21
|
-
if (index <= 0) return null
|
|
22
|
-
return presentedSheetStack[index - 1].takeIf { it.rootContainerView == rootContainer }
|
|
23
|
-
}
|
|
24
|
-
|
|
25
15
|
/**
|
|
26
16
|
* Returns the topmost presented and visible sheet, skipping sheets that are dismissing.
|
|
27
17
|
* A dismissing sheet can't parent a new one: its dim would suppress the child's own dim
|
|
@@ -33,6 +23,22 @@ object TrueSheetStackManager {
|
|
|
33
23
|
it.viewController.run { isPresented && isSheetVisible && !isBeingDismissed }
|
|
34
24
|
}
|
|
35
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Returns the topmost sheet hidden by screen navigation that was presented from the same
|
|
28
|
+
* screen as the given sheet. Sheets presented on the covering screen must not parent to it,
|
|
29
|
+
* or the hidden sheet would receive focus and blur events while it can't be seen.
|
|
30
|
+
* Must be called within synchronized block.
|
|
31
|
+
*/
|
|
32
|
+
private fun findScreenHiddenSheet(sheetView: TrueSheetView): TrueSheetView? {
|
|
33
|
+
val screenTag = RNScreensEventObserver.findPresenterScreenTag(sheetView)
|
|
34
|
+
if (screenTag == 0) return null
|
|
35
|
+
|
|
36
|
+
return presentedSheetStack.lastOrNull {
|
|
37
|
+
it.viewController.run { isPresented && wasHiddenByScreen && !isBeingDismissed } &&
|
|
38
|
+
it.screensEventObserver?.presenterScreenTag == screenTag
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
36
42
|
/**
|
|
37
43
|
* Registers a sheet in the stack and returns its parent sheet if any.
|
|
38
44
|
* Only returns a parent if it's in the same container (e.g., same Screen).
|
|
@@ -40,7 +46,8 @@ object TrueSheetStackManager {
|
|
|
40
46
|
@JvmStatic
|
|
41
47
|
fun registerSheet(sheetView: TrueSheetView): TrueSheetView? {
|
|
42
48
|
synchronized(presentedSheetStack) {
|
|
43
|
-
val parentSheet = findTopmostSheet()
|
|
49
|
+
val parentSheet = (findTopmostSheet() ?: findScreenHiddenSheet(sheetView))
|
|
50
|
+
?.takeIf { it.rootContainerView == sheetView.rootContainerView }
|
|
44
51
|
|
|
45
52
|
if (!presentedSheetStack.contains(sheetView)) {
|
|
46
53
|
presentedSheetStack.add(sheetView)
|
|
@@ -62,24 +69,20 @@ object TrueSheetStackManager {
|
|
|
62
69
|
|
|
63
70
|
/**
|
|
64
71
|
* Updates parent sheet translation based on the child sheet's position.
|
|
65
|
-
* Only affects parent sheets in the same container.
|
|
66
72
|
*/
|
|
67
73
|
@JvmStatic
|
|
68
74
|
fun updateParentTranslation(sheetView: TrueSheetView) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
sheetView.viewController.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
val childSheetTop = maxOf(childMinSheetTop, childCurrentSheetTop)
|
|
81
|
-
parentSheet.updateTranslationForChild(childSheetTop)
|
|
82
|
-
}
|
|
75
|
+
val parentSheet = sheetView.viewController.parentSheetView ?: return
|
|
76
|
+
|
|
77
|
+
// Post to ensure layout is complete before reading position
|
|
78
|
+
sheetView.viewController.post {
|
|
79
|
+
val childMinSheetTop = sheetView.viewController.detentCalculator.getSheetTopForDetentIndex(0)
|
|
80
|
+
val childCurrentSheetTop = sheetView.viewController.detentCalculator.getSheetTopForDetentIndex(
|
|
81
|
+
sheetView.viewController.currentDetentIndex
|
|
82
|
+
)
|
|
83
|
+
// Cap to minimum detent position
|
|
84
|
+
val childSheetTop = maxOf(childMinSheetTop, childCurrentSheetTop)
|
|
85
|
+
parentSheet.updateTranslationForChild(childSheetTop)
|
|
83
86
|
}
|
|
84
87
|
}
|
|
85
88
|
|
|
@@ -113,18 +116,6 @@ object TrueSheetStackManager {
|
|
|
113
116
|
}
|
|
114
117
|
}
|
|
115
118
|
|
|
116
|
-
/**
|
|
117
|
-
* Gets the parent sheet of the given sheet, if any.
|
|
118
|
-
* Only returns a parent if it's in the same container.
|
|
119
|
-
*/
|
|
120
|
-
@JvmStatic
|
|
121
|
-
fun getParentSheet(sheetView: TrueSheetView): TrueSheetView? {
|
|
122
|
-
synchronized(presentedSheetStack) {
|
|
123
|
-
val index = presentedSheetStack.indexOf(sheetView)
|
|
124
|
-
return getParentSheetAt(index, sheetView.rootContainerView)
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
119
|
/**
|
|
129
120
|
* Returns true if the given sheet is the topmost presented sheet in its container.
|
|
130
121
|
*/
|
|
@@ -58,7 +58,8 @@ static jboolean updateStateImmediate(
|
|
|
58
58
|
static jboolean updateFooterStateImmediate(
|
|
59
59
|
jni::alias_ref<jclass> /*clazz*/,
|
|
60
60
|
jni::alias_ref<jobject> stateWrapper,
|
|
61
|
-
jfloat bottomInset
|
|
61
|
+
jfloat bottomInset,
|
|
62
|
+
jfloat translateY) {
|
|
62
63
|
auto state = getStateFromWrapper(stateWrapper);
|
|
63
64
|
if (!state) {
|
|
64
65
|
return JNI_FALSE;
|
|
@@ -67,15 +68,44 @@ static jboolean updateFooterStateImmediate(
|
|
|
67
68
|
auto concreteState =
|
|
68
69
|
std::static_pointer_cast<const ConcreteState<TrueSheetFooterViewState>>(state);
|
|
69
70
|
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
// Carries the translation too — RN's EventQueue drops a still-queued update
|
|
72
|
+
// for the same view (see updateFooterTranslation) when this one is enqueued
|
|
73
|
+
concreteState->updateState(
|
|
74
|
+
[bottomInset, translateY](const TrueSheetFooterViewState &oldData) -> StateData::Shared {
|
|
75
|
+
auto newData = oldData;
|
|
76
|
+
newData.bottomInset = bottomInset;
|
|
77
|
+
newData.translateY = translateY;
|
|
78
|
+
newData.initialized = true;
|
|
79
|
+
return std::make_shared<const TrueSheetFooterViewState>(newData);
|
|
80
|
+
},
|
|
81
|
+
EventQueue::UpdateMode::unstable_Immediate);
|
|
75
82
|
|
|
76
83
|
return JNI_TRUE;
|
|
77
84
|
}
|
|
78
85
|
|
|
86
|
+
// Mirrors the footer's resting translation into its state so measure()
|
|
87
|
+
// matches where it's drawn. Async — it only feeds measure().
|
|
88
|
+
static void updateFooterTranslation(
|
|
89
|
+
jni::alias_ref<jclass> /*clazz*/,
|
|
90
|
+
jni::alias_ref<jobject> stateWrapper,
|
|
91
|
+
jfloat translateY) {
|
|
92
|
+
auto state = getStateFromWrapper(stateWrapper);
|
|
93
|
+
if (!state) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
auto concreteState =
|
|
98
|
+
std::static_pointer_cast<const ConcreteState<TrueSheetFooterViewState>>(state);
|
|
99
|
+
|
|
100
|
+
concreteState->updateState(
|
|
101
|
+
[translateY](const TrueSheetFooterViewState &oldData) -> StateData::Shared {
|
|
102
|
+
auto newData = oldData;
|
|
103
|
+
newData.translateY = translateY;
|
|
104
|
+
return std::make_shared<const TrueSheetFooterViewState>(newData);
|
|
105
|
+
},
|
|
106
|
+
EventQueue::UpdateMode::Asynchronous);
|
|
107
|
+
}
|
|
108
|
+
|
|
79
109
|
// Publishes the precalculated bottom safe-area inset so a late-mounted
|
|
80
110
|
// footer's first layout is already padded (see TrueSheetFooterViewShadowNode).
|
|
81
111
|
static void setBottomSafeArea(jni::alias_ref<jclass> /*clazz*/, jfloat insetDp) {
|
|
@@ -90,6 +120,7 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void * /*reserved*/) {
|
|
|
90
120
|
->registerNatives({
|
|
91
121
|
makeNativeMethod("nativeUpdateState", facebook::react::updateStateImmediate),
|
|
92
122
|
makeNativeMethod("nativeUpdateFooterState", facebook::react::updateFooterStateImmediate),
|
|
123
|
+
makeNativeMethod("nativeUpdateFooterTranslation", facebook::react::updateFooterTranslation),
|
|
93
124
|
makeNativeMethod("nativeSetBottomSafeArea", facebook::react::setBottomSafeArea),
|
|
94
125
|
});
|
|
95
126
|
});
|
package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.cpp
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
#include "TrueSheetContentViewShadowNode.h"
|
|
2
2
|
|
|
3
|
-
#include <folly/ScopeGuard.h>
|
|
4
3
|
#include <react/renderer/components/view/conversions.h>
|
|
5
4
|
#include <react/renderer/core/ComponentDescriptor.h>
|
|
6
5
|
#include <react/renderer/core/LayoutConstraints.h>
|
|
@@ -95,7 +94,12 @@ void TrueSheetContentViewShadowNode::updateNaturalHeightIfNeeded(const LayoutCon
|
|
|
95
94
|
if (contentChanged_ || !measurement_ || !(measurement_->constraints == constraints) ||
|
|
96
95
|
!(measurement_->context == measurementContext) || measurement_->errata != errata) {
|
|
97
96
|
gIsMeasuringNaturalHeight = true;
|
|
98
|
-
|
|
97
|
+
// Not folly::makeGuard: its terminate() isn't linked into RN < 0.86 on Android.
|
|
98
|
+
struct RestoreMeasurement {
|
|
99
|
+
~RestoreMeasurement() {
|
|
100
|
+
gIsMeasuringNaturalHeight = false;
|
|
101
|
+
}
|
|
102
|
+
} restoreMeasurement;
|
|
99
103
|
auto measurementNode =
|
|
100
104
|
measurement_ ? cloneForMeasurement(*this, *measurementSourceChildren_, measurement_->root->getChildren())
|
|
101
105
|
: cloneForMeasurement(*this, {}, {});
|
package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.cpp
CHANGED
|
@@ -59,4 +59,16 @@ void TrueSheetFooterViewShadowNode::adjustLayoutWithState() {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
// Native moves the footer off its Yoga position without a layout pass. Folding
|
|
63
|
+
// that slide into the transform keeps measure() — and Pressability's press
|
|
64
|
+
// region — on the drawn footer.
|
|
65
|
+
Transform TrueSheetFooterViewShadowNode::getTransform() const {
|
|
66
|
+
auto transform = ConcreteViewShadowNode::getTransform();
|
|
67
|
+
auto translateY = getStateData().translateY;
|
|
68
|
+
if (translateY == 0) {
|
|
69
|
+
return transform;
|
|
70
|
+
}
|
|
71
|
+
return transform * Transform::Translate(0, translateY, 0);
|
|
72
|
+
}
|
|
73
|
+
|
|
62
74
|
} // namespace facebook::react
|
|
@@ -11,7 +11,8 @@ namespace facebook::react {
|
|
|
11
11
|
/*
|
|
12
12
|
* State for <TrueSheetFooterView> component.
|
|
13
13
|
* Set from native with the sheet's bottom safe-area inset so the shadow node
|
|
14
|
-
* pads the footer's bottom edge
|
|
14
|
+
* pads the footer's bottom edge, and with the footer's native translation so
|
|
15
|
+
* measure() matches where it's drawn (see TrueSheetFooterViewShadowNode).
|
|
15
16
|
*/
|
|
16
17
|
class TrueSheetFooterViewState final {
|
|
17
18
|
public:
|
|
@@ -19,11 +20,16 @@ class TrueSheetFooterViewState final {
|
|
|
19
20
|
|
|
20
21
|
#ifdef ANDROID
|
|
21
22
|
TrueSheetFooterViewState(TrueSheetFooterViewState const &previousState, folly::dynamic data)
|
|
22
|
-
: bottomInset(static_cast<float>(data["bottomInset"].getDouble())),
|
|
23
|
+
: bottomInset(static_cast<float>(data["bottomInset"].getDouble())),
|
|
24
|
+
translateY(static_cast<float>(data["translateY"].getDouble())),
|
|
25
|
+
initialized(true) {
|
|
23
26
|
}
|
|
24
27
|
#endif
|
|
25
28
|
|
|
26
29
|
float bottomInset{0};
|
|
30
|
+
// Native slide off the Yoga position — above the keyboard, or riding the
|
|
31
|
+
// sheet below its lowest detent (Android)
|
|
32
|
+
float translateY{0};
|
|
27
33
|
// True once native has pushed a real inset — until then the shadow node
|
|
28
34
|
// seeds the layout from TrueSheetInsets (see TrueSheetFooterViewShadowNode)
|
|
29
35
|
bool initialized{false};
|
|
@@ -287,6 +287,12 @@ using namespace facebook::react;
|
|
|
287
287
|
[self.delegate containerViewScrollViewDidChange];
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
+
// Footer controls can mount, unmount, or hide deep in its subtree without
|
|
291
|
+
// passing through its own mount hooks or changing its size.
|
|
292
|
+
- (void)contentViewDidMountTransaction {
|
|
293
|
+
[self.delegate containerViewDidMountTransaction];
|
|
294
|
+
}
|
|
295
|
+
|
|
290
296
|
#pragma mark - TrueSheetHeaderViewDelegate
|
|
291
297
|
|
|
292
298
|
- (void)headerViewDidChangeSize:(CGSize)newSize {
|
|
@@ -181,6 +181,8 @@ using namespace facebook::react;
|
|
|
181
181
|
// Layout has settled for the whole tree — the footer overlap (sheet height,
|
|
182
182
|
// scroll view frame) and the keyboard inset are current here
|
|
183
183
|
[self updateBottomInset];
|
|
184
|
+
|
|
185
|
+
[self.delegate contentViewDidMountTransaction];
|
|
184
186
|
}
|
|
185
187
|
|
|
186
188
|
#pragma mark - Scrollable
|