diff --git a/android/src/main/java/com/swmansion/rnscreens/Screen.kt b/android/src/main/java/com/swmansion/rnscreens/Screen.kt index 257439e63c..46357a0971 100644 --- a/android/src/main/java/com/swmansion/rnscreens/Screen.kt +++ b/android/src/main/java/com/swmansion/rnscreens/Screen.kt @@ -87,11 +87,16 @@ class Screen( var sheetInitialDetentIndex: Int = 0 var sheetClosesOnTouchOutside = true var sheetElevation: Float = 24F + var sheetShouldOverflowTopInset = false /** - * When using form sheet presentation we want to delay enter transition **on Paper** in order + * On Paper, when using form sheet presentation we want to delay enter transition in order * to wait for initial layout from React, otherwise the animator-based animation will look - * glitchy. *This is not needed on Fabric*. + * glitchy. + * + * On Fabric, the view layout is completed before window insets are applied. + * To ensure the BottomSheet correctly respects insets during its enter transition, + * we delay the transition until both layout and insets have been applied. */ var shouldTriggerPostponedTransitionAfterLayout = false @@ -212,7 +217,16 @@ class Screen( } } - private fun triggerPostponedEnterTransitionIfNeeded() { + // On Fabric, the view layout is completed before window insets are applied. + // To ensure the BottomSheet correctly respects insets during its enter transition, + // we delay the transition until both layout and insets have been applied. + internal fun requestTriggeringPostponedEnterTransition() { + if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED && !sheetShouldOverflowTopInset) { + shouldTriggerPostponedTransitionAfterLayout = true + } + } + + internal fun triggerPostponedEnterTransitionIfNeeded() { if (shouldTriggerPostponedTransitionAfterLayout) { shouldTriggerPostponedTransitionAfterLayout = false // This will trigger enter transition only if one was requested by ScreenStack diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.kt b/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.kt index f7f654ac98..2d9080a879 100644 --- a/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.kt +++ b/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.kt @@ -27,6 +27,8 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.shape.CornerFamily import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.shape.ShapeAppearanceModel +import com.swmansion.rnscreens.bottomsheet.BottomSheetTransitionCoordinator +import com.swmansion.rnscreens.bottomsheet.BottomSheetWindowInsetListenerChain import com.swmansion.rnscreens.bottomsheet.DimmingViewManager import com.swmansion.rnscreens.bottomsheet.SheetDelegate import com.swmansion.rnscreens.bottomsheet.usesFormSheetPresentation @@ -55,6 +57,8 @@ class ScreenStackFragment : private var isToolbarShadowHidden = false private var isToolbarTranslucent = false + private lateinit var sheetTransitionCoordinator: BottomSheetTransitionCoordinator + private var lastFocusedChild: View? = null var searchView: CustomSearchView? = null @@ -73,6 +77,10 @@ class ScreenStackFragment : internal var sheetDelegate: SheetDelegate? = null + internal var bottomSheetWindowInsetListenerChain: BottomSheetWindowInsetListenerChain? = null + + private var lastInsetsCompat: WindowInsetsCompat? = null + @SuppressLint("ValidFragment") constructor(screenView: Screen) : super(screenView) @@ -229,6 +237,13 @@ class ScreenStackFragment : dimmingDelegate.onViewHierarchyCreated(screen, coordinatorLayout) dimmingDelegate.onBehaviourAttached(screen, screen.sheetBehavior!!) + if (!screen.sheetShouldOverflowTopInset) { + sheetTransitionCoordinator = BottomSheetTransitionCoordinator() + attachInsetsAndLayoutListenersToBottomSheet( + sheetTransitionCoordinator, + ) + } + // Pre-layout the content for the sake of enter transition. val container = screen.container!! @@ -239,10 +254,12 @@ class ScreenStackFragment : coordinatorLayout.layout(0, 0, container.width, container.height) if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { - ViewCompat.setOnApplyWindowInsetsListener(screen) { _, windowInsets -> + val bottomSheetWindowInsetListenerChain = requireBottomSheetWindowInsetsListenerChain() + bottomSheetWindowInsetListenerChain.addListener { _, windowInsets -> sheetDelegate.handleKeyboardInsetsProgress(windowInsets) windowInsets } + ViewCompat.setOnApplyWindowInsetsListener(screen, bottomSheetWindowInsetListenerChain) } val insetsAnimationCallback = @@ -457,6 +474,65 @@ class ScreenStackFragment : screenStack.dismiss(this) } + // Mark: Avoiding top inset by BottomSheet + + private fun attachInsetsAndLayoutListenersToBottomSheet(sheetTransitionCoordinator: BottomSheetTransitionCoordinator) { + screen.container?.apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + setOnApplyWindowInsetsListener { _, insets -> + val insetsCompat = WindowInsetsCompat.toWindowInsetsCompat(insets, this) + handleInsetsUpdateAndNotifyTransition( + insetsCompat, + ) + insets + } + } else { + val bottomSheetWindowInsetListenerChain = requireBottomSheetWindowInsetsListenerChain() + bottomSheetWindowInsetListenerChain.addListener { _, windowInsets -> + handleInsetsUpdateAndNotifyTransition( + windowInsets, + ) + windowInsets + } + } + } + + screen.container?.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> + sheetTransitionCoordinator.onScreenContainerLayoutChanged(screen) + } + } + + private fun handleInsetsUpdateAndNotifyTransition(insetsCompat: WindowInsetsCompat) { + if (lastInsetsCompat == insetsCompat) { + return + } + lastInsetsCompat = insetsCompat + + // Reconfigure BottomSheetBehavior with the same state and updated maxHeight. + // When insets are available, we can factor them in to update the maximum height accordingly. + val sheetDelegate = requireSheetDelegate() + sheetDelegate.updateBottomSheetMetrics(screen.sheetBehavior!!) + + screen.container?.let { container -> + // Needs to be highlighted that nothing changes at the container level. + // However, calling additional measure will trigger BottomSheetBehavior's `onMeasureChild` logic. + // This method ensures that the bottom sheet respects the maxHeight we update in `configureBottomSheetBehavior`. + coordinatorLayout.forceLayout() + coordinatorLayout.measure( + View.MeasureSpec.makeMeasureSpec(container.width, View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(container.height, View.MeasureSpec.EXACTLY), + ) + coordinatorLayout.layout(0, 0, container.width, container.height) + } + + // Although the layout of the screen container and CoordinatorLayout hasn't changed, + // the BottomSheetBehavior has updated the maximum height. + // We manually trigger the callback to notify that the bottom sheet layout has been applied. + screen.onBottomSheetBehaviorDidLayout(true) + + sheetTransitionCoordinator.onScreenContainerInsetsApplied(screen) + } + private fun requireDimmingDelegate(forceCreation: Boolean = false): DimmingViewManager { if (dimmingDelegate == null || forceCreation) { dimmingDelegate?.invalidate(screen.sheetBehavior) @@ -471,4 +547,11 @@ class ScreenStackFragment : } return sheetDelegate!! } + + internal fun requireBottomSheetWindowInsetsListenerChain(): BottomSheetWindowInsetListenerChain { + if (bottomSheetWindowInsetListenerChain == null) { + bottomSheetWindowInsetListenerChain = BottomSheetWindowInsetListenerChain() + } + return bottomSheetWindowInsetListenerChain!! + } } diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenViewManager.kt b/android/src/main/java/com/swmansion/rnscreens/ScreenViewManager.kt index e52831e4fe..269162412a 100644 --- a/android/src/main/java/com/swmansion/rnscreens/ScreenViewManager.kt +++ b/android/src/main/java/com/swmansion/rnscreens/ScreenViewManager.kt @@ -269,6 +269,14 @@ open class ScreenViewManager : view?.sheetElevation = value.toFloat() } + @ReactProp(name = "sheetShouldOverflowTopInset") + override fun setSheetShouldOverflowTopInset( + view: Screen?, + sheetShouldOverflowTopInset: Boolean, + ) { + view?.sheetShouldOverflowTopInset = sheetShouldOverflowTopInset + } + // mark: iOS-only // these props are not available on Android, however we must override their setters override fun setFullScreenSwipeEnabled( diff --git a/android/src/main/java/com/swmansion/rnscreens/bottomsheet/BottomSheetBehaviorExt.kt b/android/src/main/java/com/swmansion/rnscreens/bottomsheet/BottomSheetBehaviorExt.kt index dff322526e..3fa6541245 100644 --- a/android/src/main/java/com/swmansion/rnscreens/bottomsheet/BottomSheetBehaviorExt.kt +++ b/android/src/main/java/com/swmansion/rnscreens/bottomsheet/BottomSheetBehaviorExt.kt @@ -3,6 +3,19 @@ package com.swmansion.rnscreens.bottomsheet import android.view.View import com.google.android.material.bottomsheet.BottomSheetBehavior +internal fun BottomSheetBehavior.updateMetrics( + maxAllowedHeight: Int? = null, + expandedOffsetFromTop: Int? = null, +): BottomSheetBehavior { + maxAllowedHeight?.let { + this.maxHeight = maxAllowedHeight + } + expandedOffsetFromTop?.let { + this.expandedOffset = expandedOffsetFromTop + } + return this +} + internal fun BottomSheetBehavior.useSingleDetent( maxAllowedHeight: Int? = null, forceExpandedState: Boolean = true, @@ -13,7 +26,7 @@ internal fun BottomSheetBehavior.useSingleDetent( this.state = BottomSheetBehavior.STATE_EXPANDED } maxAllowedHeight?.let { - maxHeight = maxAllowedHeight + this.maxHeight = maxAllowedHeight } return this } @@ -23,11 +36,11 @@ internal fun BottomSheetBehavior.useTwoDetents( firstHeight: Int? = null, maxAllowedHeight: Int? = null, ): BottomSheetBehavior { - skipCollapsed = false - isFitToContents = true + this.skipCollapsed = false + this.isFitToContents = true state?.let { this.state = state } - firstHeight?.let { peekHeight = firstHeight } - maxAllowedHeight?.let { maxHeight = maxAllowedHeight } + firstHeight?.let { this.peekHeight = firstHeight } + maxAllowedHeight?.let { this.maxHeight = maxAllowedHeight } return this } @@ -38,12 +51,12 @@ internal fun BottomSheetBehavior.useThreeDetents( halfExpandedRatio: Float? = null, expandedOffsetFromTop: Int? = null, ): BottomSheetBehavior { - skipCollapsed = false - isFitToContents = false + this.skipCollapsed = false + this.isFitToContents = false state?.let { this.state = state } firstHeight?.let { this.peekHeight = firstHeight } halfExpandedRatio?.let { this.halfExpandedRatio = halfExpandedRatio } expandedOffsetFromTop?.let { this.expandedOffset = expandedOffsetFromTop } - maxAllowedHeight?.let { maxHeight = maxAllowedHeight } + maxAllowedHeight?.let { this.maxHeight = maxAllowedHeight } return this } diff --git a/android/src/main/java/com/swmansion/rnscreens/bottomsheet/BottomSheetTransitionCoordinator.kt b/android/src/main/java/com/swmansion/rnscreens/bottomsheet/BottomSheetTransitionCoordinator.kt new file mode 100644 index 0000000000..8404d63683 --- /dev/null +++ b/android/src/main/java/com/swmansion/rnscreens/bottomsheet/BottomSheetTransitionCoordinator.kt @@ -0,0 +1,25 @@ +package com.swmansion.rnscreens.bottomsheet + +import com.swmansion.rnscreens.Screen + +class BottomSheetTransitionCoordinator { + private var isLayoutComplete = false + private var areInsetsApplied = false + + internal fun onScreenContainerLayoutChanged(screen: Screen) { + isLayoutComplete = true + triggerSheetEnterTransitionIfReady(screen) + } + + internal fun onScreenContainerInsetsApplied(screen: Screen) { + areInsetsApplied = true + triggerSheetEnterTransitionIfReady(screen) + } + + private fun triggerSheetEnterTransitionIfReady(screen: Screen) { + if (!isLayoutComplete || !areInsetsApplied) return + + screen.requestTriggeringPostponedEnterTransition() + screen.triggerPostponedEnterTransitionIfNeeded() + } +} diff --git a/android/src/main/java/com/swmansion/rnscreens/bottomsheet/BottomSheetWindowInsetListenerChain.kt b/android/src/main/java/com/swmansion/rnscreens/bottomsheet/BottomSheetWindowInsetListenerChain.kt new file mode 100644 index 0000000000..1bbc03aaec --- /dev/null +++ b/android/src/main/java/com/swmansion/rnscreens/bottomsheet/BottomSheetWindowInsetListenerChain.kt @@ -0,0 +1,36 @@ +package com.swmansion.rnscreens.bottomsheet + +import android.view.View +import androidx.core.view.OnApplyWindowInsetsListener +import androidx.core.view.WindowInsetsCompat + +/** + * Aggregates and sequentially invokes many instances of OnApplyWindowInsetsListener + * + * In Android, only a single ViewCompat.setOnApplyWindowInsetsListener can be set on a view, + * which leads to listeners overwriting each other. This class solves the listener conflict + * by allowing different components to a common chain. As we do not consume or modify insets, the + * order is not important and the chain may work. + * + * In our case it's crucial, because we need to react on insets for both: + * - avoiding bottom/top insets by BottomSheet + * - keyboard handling + */ +class BottomSheetWindowInsetListenerChain : OnApplyWindowInsetsListener { + private val listeners = mutableListOf() + + fun addListener(listener: OnApplyWindowInsetsListener) { + listeners.add(listener) + } + + override fun onApplyWindowInsets( + v: View, + insets: WindowInsetsCompat, + ): WindowInsetsCompat { + for (listener in listeners) { + listener.onApplyWindowInsets(v, insets) + } + + return insets + } +} diff --git a/android/src/main/java/com/swmansion/rnscreens/bottomsheet/SheetDelegate.kt b/android/src/main/java/com/swmansion/rnscreens/bottomsheet/SheetDelegate.kt index ac86a9641a..9f94007343 100644 --- a/android/src/main/java/com/swmansion/rnscreens/bottomsheet/SheetDelegate.kt +++ b/android/src/main/java/com/swmansion/rnscreens/bottomsheet/SheetDelegate.kt @@ -37,6 +37,7 @@ class SheetDelegate( private var isSheetAnimationInProgress: Boolean = false + private var lastTopInset: Int = 0 private var lastKeyboardBottomOffset: Int = 0 var lastStableDetentIndex: Int = screen.sheetInitialDetentIndex @@ -113,12 +114,45 @@ class SheetDelegate( } } + internal fun updateBottomSheetMetrics(behavior: BottomSheetBehavior) { + val containerHeight = if (screen.sheetShouldOverflowTopInset) tryResolveContainerHeight() else tryResolveSafeAreaSpaceForSheet() + check(containerHeight != null) { + "[RNScreens] Failed to find window height during bottom sheet behaviour configuration" + } + + val maxAllowedHeight = + when (screen.isSheetFitToContents()) { + true -> + screen.contentWrapper?.let { contentWrapper -> + contentWrapper.height.takeIf { + // subtree might not be laid out, e.g. after fragment reattachment + // and view recreation, however since it is retained by + // react-native it has its height cached. We want to use it. + // Otherwise we would have to trigger RN layout manually. + contentWrapper.isLaidOutOrHasCachedLayout() + } + } + false -> (screen.sheetDetents.highest() * containerHeight).toInt() + } + + // For 3 detents, we need to add the top inset back here because we are calculating the offset + // from the absolute top of the view, but our calculated max height (containerHeight) + // has been reduced by this inset. + val expandedOffsetFromTop = + when (screen.sheetDetents.count) { + 3 -> screen.sheetDetents.expandedOffsetFromTop(containerHeight, lastTopInset) + else -> null + } + + behavior.updateMetrics(maxAllowedHeight, expandedOffsetFromTop) + } + internal fun configureBottomSheetBehaviour( behavior: BottomSheetBehavior, keyboardState: KeyboardState = KeyboardNotVisible, selectedDetentIndex: Int = lastStableDetentIndex, ): BottomSheetBehavior { - val containerHeight = tryResolveContainerHeight() + val containerHeight = if (screen.sheetShouldOverflowTopInset) tryResolveContainerHeight() else tryResolveSafeAreaSpaceForSheet() check(containerHeight != null) { "[RNScreens] Failed to find window height during bottom sheet behaviour configuration" } @@ -166,7 +200,7 @@ class SheetDelegate( firstHeight = screen.sheetDetents.firstHeight(containerHeight), halfExpandedRatio = screen.sheetDetents.halfExpandedRatio(), maxAllowedHeight = screen.sheetDetents.maxAllowedHeight(containerHeight), - expandedOffsetFromTop = screen.sheetDetents.expandedOffsetFromTop(containerHeight), + expandedOffsetFromTop = screen.sheetDetents.expandedOffsetFromTop(containerHeight, lastTopInset), ) else -> throw IllegalStateException( @@ -243,7 +277,7 @@ class SheetDelegate( firstHeight = screen.sheetDetents.firstHeight(containerHeight), halfExpandedRatio = screen.sheetDetents.halfExpandedRatio(), maxAllowedHeight = screen.sheetDetents.maxAllowedHeight(containerHeight), - expandedOffsetFromTop = screen.sheetDetents.maxAllowedHeight(containerHeight), + expandedOffsetFromTop = screen.sheetDetents.expandedOffsetFromTop(containerHeight, lastTopInset), ) else -> throw IllegalStateException( @@ -261,7 +295,7 @@ class SheetDelegate( // Otherwise, it shifts the sheet as high as possible, even if it means part of its content // will remain hidden behind the keyboard. internal fun computeSheetOffsetYWithIMEPresent(keyboardHeight: Int): Int { - val containerHeight = tryResolveContainerHeight() + val containerHeight = if (screen.sheetShouldOverflowTopInset) tryResolveContainerHeight() else tryResolveSafeAreaSpaceForSheet() check(containerHeight != null) { "[RNScreens] Failed to find window height during bottom sheet behaviour configuration" } @@ -288,7 +322,13 @@ class SheetDelegate( ): WindowInsetsCompat { val isImeVisible = insets.isVisible(WindowInsetsCompat.Type.ime()) val imeInset = insets.getInsets(WindowInsetsCompat.Type.ime()) - val prevSystemBarsInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + val systemBarsInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + val displayCutoutInsets = insets.getInsets(WindowInsetsCompat.Type.displayCutout()) + + // We save the top inset (status bar height or display cutout) to later + // subtract it from the window height during sheet size calculations. + // This ensures the sheet respects the safe area. + lastTopInset = maxOf(systemBarsInsets.top, displayCutoutInsets.top) if (isImeVisible) { isKeyboardVisible = true @@ -309,7 +349,7 @@ class SheetDelegate( isKeyboardVisible = false } - val newBottomInset = if (!isImeVisible) prevSystemBarsInsets.bottom else 0 + val newBottomInset = if (!isImeVisible) systemBarsInsets.bottom else 0 // Note: We do not manipulate the top inset manually. Therefore, if SafeAreaView has top insets enabled, // we must retain the top inset even if the formSheet does not currently overflow into the status bar. @@ -321,7 +361,7 @@ class SheetDelegate( .Builder(insets) .setInsets( WindowInsetsCompat.Type.systemBars(), - Insets.of(prevSystemBarsInsets.left, prevSystemBarsInsets.top, prevSystemBarsInsets.right, newBottomInset), + Insets.of(systemBarsInsets.left, systemBarsInsets.top, systemBarsInsets.right, newBottomInset), ).build() } @@ -329,6 +369,12 @@ class SheetDelegate( @BottomSheetBehavior.State state: Int, ) = state == BottomSheetBehavior.STATE_HIDDEN + /** + * This method tries to resolve the maximum height available for the sheet content, + * accounting for the system top inset. + */ + private fun tryResolveSafeAreaSpaceForSheet(): Int? = tryResolveContainerHeight()?.let { it - lastTopInset } + /** * This method might return slightly different values depending on code path, * but during testing I've found this effect negligible. For practical purposes diff --git a/android/src/main/java/com/swmansion/rnscreens/bottomsheet/SheetUtils.kt b/android/src/main/java/com/swmansion/rnscreens/bottomsheet/SheetUtils.kt index 8981cd2718..26e5056ece 100644 --- a/android/src/main/java/com/swmansion/rnscreens/bottomsheet/SheetUtils.kt +++ b/android/src/main/java/com/swmansion/rnscreens/bottomsheet/SheetUtils.kt @@ -143,6 +143,16 @@ fun Screen.requiresEnterTransitionPostponing(): Boolean { // This is used only for formSheet presentation, because we use value animators // there. Tween animations have some magic way to make this work (maybe they // postpone the transition internally, dunno). + // + // On Fabric, system insets are applied after the initial layout pass. However, + // the BottomSheet height might be measured earlier due to internal BottomSheet logic + // or layout callbacks, before those insets are applied. + // To ensure the BottomSheet height respects the top inset we delay starting the enter + // transition until both layout and insets are fully applied. + + if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED && !this.sheetShouldOverflowTopInset && this.usesFormSheetPresentation()) { + return true + } if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED || !this.usesFormSheetPresentation()) { return false diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenManagerDelegate.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenManagerDelegate.java index c33cb6e318..f8481a2f46 100644 --- a/android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenManagerDelegate.java +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenManagerDelegate.java @@ -50,6 +50,9 @@ public void setProperty(T view, String propName, @Nullable Object value) { case "sheetElevation": mViewManager.setSheetElevation(view, value == null ? 24 : ((Double) value).intValue()); break; + case "sheetShouldOverflowTopInset": + mViewManager.setSheetShouldOverflowTopInset(view, value == null ? false : (boolean) value); + break; case "customAnimationOnSwipe": mViewManager.setCustomAnimationOnSwipe(view, value == null ? false : (boolean) value); break; diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenManagerInterface.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenManagerInterface.java index beb5ef140d..28245c546b 100644 --- a/android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenManagerInterface.java +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenManagerInterface.java @@ -24,6 +24,7 @@ public interface RNSScreenManagerInterface { void setSheetExpandsWhenScrolledToEdge(T view, boolean value); void setSheetInitialDetent(T view, int value); void setSheetElevation(T view, int value); + void setSheetShouldOverflowTopInset(T view, boolean value); void setCustomAnimationOnSwipe(T view, boolean value); void setFullScreenSwipeEnabled(T view, @Nullable String value); void setFullScreenSwipeShadowEnabled(T view, boolean value); diff --git a/apps/src/tests/Test3336.tsx b/apps/src/tests/Test3336.tsx index 7668284e97..1c6049653b 100644 --- a/apps/src/tests/Test3336.tsx +++ b/apps/src/tests/Test3336.tsx @@ -129,6 +129,7 @@ const formSheetBaseOptions: NativeStackNavigationOptions = { contentStyle: { backgroundColor: Colors.GreenLight100, }, + // TODO(@t0maboro) - add `sheetShouldOverflowTopInset` prop here when possible }; function PressableBase() { diff --git a/guides/GUIDE_FOR_LIBRARY_AUTHORS.md b/guides/GUIDE_FOR_LIBRARY_AUTHORS.md index 40397636ec..9eaf19ee77 100644 --- a/guides/GUIDE_FOR_LIBRARY_AUTHORS.md +++ b/guides/GUIDE_FOR_LIBRARY_AUTHORS.md @@ -233,6 +233,16 @@ corresponding legacy prop values for `sheetAllowedDetents` prop. Defaults to `none`, indicating that the dimming view should be always present. +### `sheetShouldOverflowTopInset` (Android only) + +Whether the sheet content should be rendered behind the Status Bar or display cutouts. + +When set to `true`, the sheet will extend to the physical edges of the stack, allowing content to be visible behind the status bar or display cutouts. Detent ratios in sheetAllowedDetents will be measured relative to the full stack height. + +When set to `false`, the sheet's layout will be constrained by the inset from the top and the detent ratios will then be measured relative to the adjusted height (excluding the top inset). This means that sheetAllowedDetents will result in different sheet heights depending on this prop. + +Defaults to `false`. + ### `stackAnimation` Allows for the customization of how the given screen should appear/disappear when pushed or popped at the top of the stack. The following values are currently supported: diff --git a/src/components/Screen.tsx b/src/components/Screen.tsx index 1fb3a3745b..f6da11b52b 100644 --- a/src/components/Screen.tsx +++ b/src/components/Screen.tsx @@ -98,6 +98,7 @@ export const InnerScreen = React.forwardRef( sheetExpandsWhenScrolledToEdge = true, sheetElevation = 24, sheetInitialDetentIndex = 0, + sheetShouldOverflowTopInset = false, // Other screenId, stackPresentation, @@ -229,6 +230,7 @@ export const InnerScreen = React.forwardRef( sheetAllowedDetents={resolvedSheetAllowedDetents} sheetLargestUndimmedDetent={resolvedSheetLargestUndimmedDetent} sheetElevation={sheetElevation} + sheetShouldOverflowTopInset={sheetShouldOverflowTopInset} sheetGrabberVisible={sheetGrabberVisible} sheetCornerRadius={sheetCornerRadius} sheetExpandsWhenScrolledToEdge={sheetExpandsWhenScrolledToEdge} diff --git a/src/fabric/ModalScreenNativeComponent.ts b/src/fabric/ModalScreenNativeComponent.ts index c67e3251e5..c6284a8abf 100644 --- a/src/fabric/ModalScreenNativeComponent.ts +++ b/src/fabric/ModalScreenNativeComponent.ts @@ -89,6 +89,7 @@ export interface NativeProps extends ViewProps { sheetExpandsWhenScrolledToEdge?: WithDefault; sheetInitialDetent?: WithDefault; sheetElevation?: WithDefault; + sheetShouldOverflowTopInset?: WithDefault; customAnimationOnSwipe?: boolean; fullScreenSwipeEnabled?: WithDefault; fullScreenSwipeShadowEnabled?: WithDefault; diff --git a/src/fabric/ScreenNativeComponent.ts b/src/fabric/ScreenNativeComponent.ts index 6186b34b07..c173026b50 100644 --- a/src/fabric/ScreenNativeComponent.ts +++ b/src/fabric/ScreenNativeComponent.ts @@ -91,6 +91,7 @@ export interface NativeProps extends ViewProps { sheetExpandsWhenScrolledToEdge?: WithDefault; sheetInitialDetent?: WithDefault; sheetElevation?: WithDefault; + sheetShouldOverflowTopInset?: WithDefault; customAnimationOnSwipe?: boolean; fullScreenSwipeEnabled?: WithDefault; fullScreenSwipeShadowEnabled?: WithDefault; diff --git a/src/types.tsx b/src/types.tsx index 4d71750f5d..0454dc7d90 100644 --- a/src/types.tsx +++ b/src/types.tsx @@ -472,6 +472,22 @@ export interface ScreenProps extends ViewProps { * Defaults to `0` - which represents first detent in the detents array. */ sheetInitialDetentIndex?: number | 'last'; + /** + * Whether the sheet content should be rendered behind the Status Bar or display cutouts. + * + * When set to `true`, the sheet will extend to the physical edges of the stack, + * allowing content to be visible behind the status bar or display cutouts. + * Detent ratios in sheetAllowedDetents will be measured relative to the full stack height. + * + * When set to `false`, the sheet's layout will be constrained by the inset from the top + * and the detent ratios will then be measured relative to the adjusted height (excluding the top inset). + * This means that sheetAllowedDetents will result in different sheet heights depending on this prop. + * + * Defaults to `false`. + * + * @platform android + */ + sheetShouldOverflowTopInset?: boolean; /** * How the screen should appear/disappear when pushed or popped at the top of the stack. * The following values are currently supported: