Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f021352
Add maxAllowedHeight
t0maboro Oct 23, 2025
beec51a
Add test
t0maboro Oct 23, 2025
dbeda0b
Prevent insets consumption in sheet
t0maboro Oct 23, 2025
b5b3574
Add prop for disabling SAV for formSheet
t0maboro Oct 23, 2025
00602fb
Aggregate conditions in 1 place
t0maboro Oct 23, 2025
31e0276
Fix top inset consumption
t0maboro Oct 23, 2025
a8ecbf6
Remove duplicated SAV logic
t0maboro Oct 23, 2025
98d110a
Simplify
t0maboro Oct 23, 2025
08381a7
Recalculate layout for fitToContents
t0maboro Oct 24, 2025
8037bf5
Drop flex style for fitToContents
t0maboro Oct 24, 2025
d61bf89
Rename test
t0maboro Oct 24, 2025
da89057
Minor refactor for new prop
t0maboro Oct 27, 2025
b13b271
Correct insets consumption
t0maboro Oct 27, 2025
bae14db
Add doc
t0maboro Oct 27, 2025
f9d9dd6
Update doc
t0maboro Oct 27, 2025
80794df
Respect partial statusBar insets
t0maboro Oct 27, 2025
22c09f9
Metrics for fitToContents
t0maboro Oct 27, 2025
8119cf5
Move SAV to application
t0maboro Oct 30, 2025
e580e41
Override flex for fitToContents
t0maboro Oct 30, 2025
ee49be2
Make test more configurable
t0maboro Oct 30, 2025
7f03921
Add comment
t0maboro Oct 30, 2025
da56cb1
Create BottomSheetMetrics
t0maboro Oct 30, 2025
e5e80d9
Add comment
t0maboro Oct 30, 2025
a743c5f
Linter
t0maboro Oct 30, 2025
2ad5ff4
Update app
t0maboro Oct 30, 2025
7fe8965
Handle sheet metrics for non formSheet screen
t0maboro Nov 3, 2025
6eb0b4d
Update example
t0maboro Nov 3, 2025
b096edc
Require
t0maboro Nov 3, 2025
9c70d4b
Update app
t0maboro Nov 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions android/src/main/java/com/swmansion/rnscreens/Screen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -530,6 +534,11 @@ class Screen(
}
}

fun isOverflowingStatusBar(
topInset: Int,
metrics: BottomSheetMetrics,
): Boolean = metrics.maxSheetHeight >= metrics.availableHeight - topInset

enum class StackPresentation {
PUSH,
MODAL,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,37 @@ import android.view.View
import com.google.android.material.bottomsheet.BottomSheetBehavior

internal fun <T : View> BottomSheetBehavior<T>.useSingleDetent(
height: Int? = null,
maxAllowedHeight: Int? = null,
forceExpandedState: Boolean = true,
): BottomSheetBehavior<T> {
this.skipCollapsed = true
this.isFitToContents = true
if (forceExpandedState) {
this.state = BottomSheetBehavior.STATE_EXPANDED
}
height?.let {
maxHeight = height
maxAllowedHeight?.let {
maxHeight = maxAllowedHeight
}
return this
}

internal fun <T : View> BottomSheetBehavior<T>.useTwoDetents(
@BottomSheetBehavior.StableState state: Int? = null,
firstHeight: Int? = null,
secondHeight: Int? = null,
maxAllowedHeight: Int? = null,
): BottomSheetBehavior<T> {
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 <T : View> BottomSheetBehavior<T>.useThreeDetents(
@BottomSheetBehavior.StableState state: Int? = null,
firstHeight: Int? = null,
maxAllowedHeight: Int? = null,
halfExpandedRatio: Float? = null,
expandedOffsetFromTop: Int? = null,
): BottomSheetBehavior<T> {
Expand All @@ -43,5 +44,6 @@ internal fun <T : View> BottomSheetBehavior<T>.useThreeDetents(
firstHeight?.let { this.peekHeight = firstHeight }
halfExpandedRatio?.let { this.halfExpandedRatio = halfExpandedRatio }
expandedOffsetFromTop?.let { this.expandedOffset = expandedOffsetFromTop }
maxAllowedHeight?.let { maxHeight = maxAllowedHeight }
return this
}
Original file line number Diff line number Diff line change
@@ -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,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ class SheetDelegate(
} else {
(screen.sheetDetents.first() * containerHeight).toInt()
}
useSingleDetent(height = height)
useSingleDetent(maxAllowedHeight = height)
}

2 ->
Expand All @@ -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 ->
Expand All @@ -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(),
)
Expand Down Expand Up @@ -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(),
)
Expand Down Expand Up @@ -309,26 +311,14 @@ 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
keyboardState = KeyboardVisible(imeInset.bottom)
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) {
Expand All @@ -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()
}

Expand Down
Loading
Loading