diff --git a/android/src/main/java/com/swmansion/rnscreens/Screen.kt b/android/src/main/java/com/swmansion/rnscreens/Screen.kt index eb3e7182a9..049d83055b 100644 --- a/android/src/main/java/com/swmansion/rnscreens/Screen.kt +++ b/android/src/main/java/com/swmansion/rnscreens/Screen.kt @@ -26,6 +26,7 @@ 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.BottomSheetMetrics import com.swmansion.rnscreens.bottomsheet.isSheetFitToContents import com.swmansion.rnscreens.bottomsheet.useSingleDetent import com.swmansion.rnscreens.bottomsheet.usesFormSheetPresentation @@ -142,6 +143,9 @@ class Screen( if (usesFormSheetPresentation()) { if (isSheetFitToContents()) { sheetBehavior?.useSingleDetent(height) + // During the initial call in `onCreateView`, insets are not yet available, + // so we need to request an additional layout pass later to account for them. + requestLayout() } if (!BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { @@ -530,6 +534,11 @@ class Screen( } } + fun isOverflowingStatusBar( + topInset: Int, + metrics: BottomSheetMetrics, + ): Boolean = metrics.maxSheetHeight >= metrics.availableHeight - topInset + enum class StackPresentation { PUSH, MODAL, 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 5e1d1b5683..dff322526e 100644 --- a/android/src/main/java/com/swmansion/rnscreens/bottomsheet/BottomSheetBehaviorExt.kt +++ b/android/src/main/java/com/swmansion/rnscreens/bottomsheet/BottomSheetBehaviorExt.kt @@ -4,7 +4,7 @@ import android.view.View import com.google.android.material.bottomsheet.BottomSheetBehavior internal fun BottomSheetBehavior.useSingleDetent( - height: Int? = null, + maxAllowedHeight: Int? = null, forceExpandedState: Boolean = true, ): BottomSheetBehavior { this.skipCollapsed = true @@ -12,8 +12,8 @@ internal fun BottomSheetBehavior.useSingleDetent( if (forceExpandedState) { this.state = BottomSheetBehavior.STATE_EXPANDED } - height?.let { - maxHeight = height + maxAllowedHeight?.let { + maxHeight = maxAllowedHeight } return this } @@ -21,19 +21,20 @@ internal fun BottomSheetBehavior.useSingleDetent( internal fun BottomSheetBehavior.useTwoDetents( @BottomSheetBehavior.StableState state: Int? = null, firstHeight: Int? = null, - secondHeight: Int? = null, + maxAllowedHeight: Int? = null, ): BottomSheetBehavior { skipCollapsed = false isFitToContents = true state?.let { this.state = state } firstHeight?.let { peekHeight = firstHeight } - secondHeight?.let { maxHeight = secondHeight } + maxAllowedHeight?.let { maxHeight = maxAllowedHeight } return this } internal fun BottomSheetBehavior.useThreeDetents( @BottomSheetBehavior.StableState state: Int? = null, firstHeight: Int? = null, + maxAllowedHeight: Int? = null, halfExpandedRatio: Float? = null, expandedOffsetFromTop: Int? = null, ): BottomSheetBehavior { @@ -43,5 +44,6 @@ internal fun BottomSheetBehavior.useThreeDetents( firstHeight?.let { this.peekHeight = firstHeight } halfExpandedRatio?.let { this.halfExpandedRatio = halfExpandedRatio } expandedOffsetFromTop?.let { this.expandedOffset = expandedOffsetFromTop } + maxAllowedHeight?.let { maxHeight = maxAllowedHeight } return this } diff --git a/android/src/main/java/com/swmansion/rnscreens/bottomsheet/BottomSheetMetrics.kt b/android/src/main/java/com/swmansion/rnscreens/bottomsheet/BottomSheetMetrics.kt new file mode 100644 index 0000000000..824c2bb8f9 --- /dev/null +++ b/android/src/main/java/com/swmansion/rnscreens/bottomsheet/BottomSheetMetrics.kt @@ -0,0 +1,33 @@ +package com.swmansion.rnscreens.bottomsheet + +import com.swmansion.rnscreens.Screen + +data class BottomSheetMetrics( + val availableHeight: Int, + val maxDetent: Double, + val maxSheetHeight: Int, +) + +fun getSheetMetrics( + screen: Screen, + availableHeight: Int, + sheetHeight: Int, +): BottomSheetMetrics { + require(screen.usesFormSheetPresentation()) { + "[RNScreens] Expected screen to use form sheet presentation" + } + + val maxDetent = screen.sheetDetents.lastOrNull() ?: 1.0 + + val maxSheetHeight = + when { + screen.isSheetFitToContents() -> sheetHeight + else -> (availableHeight * maxDetent).toInt() + } + + return BottomSheetMetrics( + availableHeight = availableHeight, + maxDetent = maxDetent, + maxSheetHeight = maxSheetHeight, + ) +} 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 df9b9d6aff..eb980892ec 100644 --- a/android/src/main/java/com/swmansion/rnscreens/bottomsheet/SheetDelegate.kt +++ b/android/src/main/java/com/swmansion/rnscreens/bottomsheet/SheetDelegate.kt @@ -154,7 +154,7 @@ class SheetDelegate( } else { (screen.sheetDetents.first() * containerHeight).toInt() } - useSingleDetent(height = height) + useSingleDetent(maxAllowedHeight = height) } 2 -> @@ -165,7 +165,7 @@ class SheetDelegate( screen.sheetDetents.count(), ), firstHeight = (screen.sheetDetents[0] * containerHeight).toInt(), - secondHeight = (screen.sheetDetents[1] * containerHeight).toInt(), + maxAllowedHeight = (screen.sheetDetents.last() * containerHeight).toInt(), ) 3 -> @@ -176,6 +176,7 @@ class SheetDelegate( screen.sheetDetents.count(), ), firstHeight = (screen.sheetDetents[0] * containerHeight).toInt(), + maxAllowedHeight = (screen.sheetDetents.last() * containerHeight).toInt(), halfExpandedRatio = (screen.sheetDetents[1] / screen.sheetDetents[2]).toFloat(), expandedOffsetFromTop = ((1 - screen.sheetDetents[2]) * containerHeight).toInt(), ) @@ -246,20 +247,21 @@ class SheetDelegate( } } } else { - (screen.sheetDetents.first() * containerHeight).toInt() + (screen.sheetDetents.last() * containerHeight).toInt() } - useSingleDetent(height = height, forceExpandedState = false) + useSingleDetent(maxAllowedHeight = height, forceExpandedState = false) } 2 -> behavior.useTwoDetents( firstHeight = (screen.sheetDetents[0] * containerHeight).toInt(), - secondHeight = (screen.sheetDetents[1] * containerHeight).toInt(), + maxAllowedHeight = (screen.sheetDetents.last() * containerHeight).toInt(), ) 3 -> behavior.useThreeDetents( firstHeight = (screen.sheetDetents[0] * containerHeight).toInt(), + maxAllowedHeight = (screen.sheetDetents.last() * containerHeight).toInt(), halfExpandedRatio = (screen.sheetDetents[1] / screen.sheetDetents[2]).toFloat(), expandedOffsetFromTop = ((1 - screen.sheetDetents[2]) * containerHeight).toInt(), ) @@ -309,6 +311,7 @@ class SheetDelegate( ): WindowInsetsCompat { val isImeVisible = insets.isVisible(WindowInsetsCompat.Type.ime()) val imeInset = insets.getInsets(WindowInsetsCompat.Type.ime()) + val prevInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()) if (isImeVisible) { isKeyboardVisible = true @@ -316,19 +319,6 @@ class SheetDelegate( sheetBehavior?.let { this.configureBottomSheetBehaviour(it, keyboardState) } - - val prevInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars()) - return WindowInsetsCompat - .Builder(insets) - .setInsets( - WindowInsetsCompat.Type.navigationBars(), - Insets.of( - prevInsets.left, - prevInsets.top, - prevInsets.right, - 0, - ), - ).build() } else { sheetBehavior?.let { if (isKeyboardVisible) { @@ -342,12 +332,30 @@ class SheetDelegate( isKeyboardVisible = false } - val prevInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars()) + // We're taking the metrics of the rootView, not screen to support nested navigators + val availableHeight = screen.rootView.height + + val metrics = + getSheetMetrics( + screen = screen, + availableHeight = availableHeight, + sheetHeight = screen.height, + ) + + val newTopInset = + if (screen.isOverflowingStatusBar(prevInsets.top, metrics) && !isImeVisible) { + prevInsets.top - (metrics.availableHeight - metrics.maxSheetHeight) + } else { + 0 + } + + val newBottomInset = if (!isImeVisible) prevInsets.bottom else 0 + return WindowInsetsCompat .Builder(insets) .setInsets( - WindowInsetsCompat.Type.navigationBars(), - Insets.of(prevInsets.left, prevInsets.top, prevInsets.right, 0), + WindowInsetsCompat.Type.systemBars(), + Insets.of(prevInsets.left, newTopInset, prevInsets.right, newBottomInset), ).build() } diff --git a/apps/src/tests/Test3336.tsx b/apps/src/tests/Test3336.tsx new file mode 100644 index 0000000000..7668284e97 --- /dev/null +++ b/apps/src/tests/Test3336.tsx @@ -0,0 +1,448 @@ +import React, { useState } from 'react'; +import { createNativeStackNavigator } from '@react-navigation/native-stack'; +import type { + NativeStackNavigationOptions, + NativeStackNavigationProp, +} from '@react-navigation/native-stack'; +import { NavigationContainer } from '@react-navigation/native'; +import { + Button, + Platform, + ScrollView, + Text, + TextInput, + View, + ViewStyle, +} from 'react-native'; +import PressableWithFeedback from '../shared/PressableWithFeedback'; +import { Spacer } from '../shared'; +import Colors from '../shared/styling/Colors'; +import { Edge, SafeAreaView } from 'react-native-screens/experimental'; + +type StackParamList = { + Main: undefined; + FormSheetWithFitToContents: undefined; + FormSheetWithSmallDetent: undefined; + FormSheetWithMediumDetent: undefined; + FormSheetWithLargeDetent: undefined; + FormSheetWithTwoDetents: undefined; + FormSheetWithThreeDetents: undefined; + FormSheetWithMaxDetent: undefined; + FormSheetOverStatusBar: undefined; + FormSheetWithFitToContentsWithTextInput: undefined; + FormSheetWithSmallDetentWithTextInput: undefined; + FormSheetWithMediumDetentWithTextInput: undefined; + FormSheetWithLargeDetentWithTextInput: undefined; + FormSheetWithTwoDetentsWithTextInput: undefined; + FormSheetWithThreeDetentsWithTextInput: undefined; + FormSheetWithMaxDetentWithTextInput: undefined; + FormSheetOverStatusBarWithTextInput: undefined; +}; + +const Stack = createNativeStackNavigator(); + +type MainProps = { + navigation: NativeStackNavigationProp; + useSafeArea: boolean; + edges: Partial>; + toggleSafeArea: () => void; + toggleTopEdge: () => void; + toggleBottomEdge: () => void; +}; + +const EXAMPLES = [ + ['Fit to contents', 'FormSheetWithFitToContents'], + ['1 small detent', 'FormSheetWithSmallDetent'], + ['1 medium detent', 'FormSheetWithMediumDetent'], + ['1 large detent', 'FormSheetWithLargeDetent'], + ['2 detents', 'FormSheetWithTwoDetents'], + ['3 detents', 'FormSheetWithThreeDetents'], + ['Max detent', 'FormSheetWithMaxDetent'], + ['Partially covered status bar', 'FormSheetOverStatusBar'], + ['Fit to contents (TextInput)', 'FormSheetWithFitToContentsWithTextInput'], + ['1 small detent (TextInput)', 'FormSheetWithSmallDetentWithTextInput'], + ['1 medium detent (TextInput)', 'FormSheetWithMediumDetentWithTextInput'], + ['1 large detent (TextInput)', 'FormSheetWithLargeDetentWithTextInput'], + ['2 detents (TextInput)', 'FormSheetWithTwoDetentsWithTextInput'], + ['3 detents (TextInput)', 'FormSheetWithThreeDetentsWithTextInput'], + ['Max detent (TextInput)', 'FormSheetWithMaxDetentWithTextInput'], + [ + 'Partially covered status bar (TextInput)', + 'FormSheetOverStatusBarWithTextInput', + ], +]; + +function Main({ + navigation, + useSafeArea, + edges, + toggleSafeArea, + toggleTopEdge, + toggleBottomEdge, +}: MainProps) { + return ( + + + [Android] Use SafeAreaView: {useSafeArea ? 'true' : 'false'} + + + [Android] Edges, top: {edges.top ? 'enabled' : 'disabled'}, bottom:{' '} + {edges.bottom ? 'enabled' : 'disabled'} + + +