Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
72d7ad6
Working, but why?
t0maboro Nov 17, 2025
350ffc6
Make function internal
t0maboro Nov 18, 2025
af5d90e
Make internal
t0maboro Nov 18, 2025
8ada358
Add some comments
t0maboro Nov 18, 2025
ca5a0f1
Remove attributes
t0maboro Nov 18, 2025
05fa419
Add comments
t0maboro Nov 18, 2025
cbc32b7
Add comment
t0maboro Nov 18, 2025
1db288e
Split logic between architectures
t0maboro Nov 18, 2025
b9db77e
Make private
t0maboro Nov 18, 2025
226fcaf
Add prop
t0maboro Nov 18, 2025
d0c3006
Fix after rebase
t0maboro Nov 18, 2025
d1c9b1f
Add prop support
t0maboro Nov 18, 2025
7c3bd30
Update defaults
t0maboro Nov 18, 2025
01492e4
Update defaults
t0maboro Nov 18, 2025
7ad6c48
Make keyboard state readonly
t0maboro Nov 18, 2025
669e8dd
Add comment
t0maboro Nov 18, 2025
6fe336b
Separate logic for resolving container height
t0maboro Nov 18, 2025
ad7e975
Remove paper annotation
t0maboro Nov 18, 2025
e30d8c6
Set proper default
t0maboro Nov 18, 2025
f863611
Add docs
t0maboro Nov 18, 2025
e506b80
Update test
t0maboro Nov 18, 2025
2cbbf01
Support for API levels <30
t0maboro Nov 18, 2025
9ad3841
Add todo
t0maboro Nov 18, 2025
5788373
Aggregate listeners
t0maboro Nov 18, 2025
96ad493
Rename
t0maboro Nov 18, 2025
6807084
Drop bottom inset support from prop
t0maboro Nov 19, 2025
125f19d
Fix keyboard flickering
t0maboro Nov 19, 2025
a20c1b7
Remove todo
t0maboro Nov 19, 2025
6cd5c35
Cleanup
t0maboro Nov 19, 2025
b2fee50
Fix after rebase
t0maboro Nov 19, 2025
31e7613
Update docs
t0maboro Nov 19, 2025
2df618b
Linter
t0maboro Nov 19, 2025
a46236f
Correct docs
t0maboro Nov 19, 2025
1301027
Deduplicate insets
t0maboro Nov 20, 2025
caa6100
Move some logic to ScreenStackFragment
t0maboro Nov 20, 2025
cad91e8
Remove extra args
t0maboro Nov 20, 2025
f72dcb4
Remove rolling insets
t0maboro Nov 20, 2025
ca1b308
Formatting
t0maboro Nov 20, 2025
81a227a
Add display cutout
t0maboro Nov 20, 2025
e6cfffa
Rename
t0maboro Nov 20, 2025
2ee4fb9
Fix default value
t0maboro Nov 20, 2025
3ca99f5
Take max
t0maboro Nov 21, 2025
bcc5240
Rename
t0maboro Nov 21, 2025
1cbdabc
Fixes after rebase
t0maboro Nov 21, 2025
f0547ce
Update android/src/main/java/com/swmansion/rnscreens/bottomsheet/Shee…
t0maboro Nov 27, 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
20 changes: 17 additions & 3 deletions android/src/main/java/com/swmansion/rnscreens/Screen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)

Expand Down Expand Up @@ -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!!
Expand All @@ -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 =
Expand Down Expand Up @@ -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)
Expand All @@ -471,4 +547,11 @@ class ScreenStackFragment :
}
return sheetDelegate!!
}

internal fun requireBottomSheetWindowInsetsListenerChain(): BottomSheetWindowInsetListenerChain {
if (bottomSheetWindowInsetListenerChain == null) {
bottomSheetWindowInsetListenerChain = BottomSheetWindowInsetListenerChain()
}
return bottomSheetWindowInsetListenerChain!!
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@ package com.swmansion.rnscreens.bottomsheet
import android.view.View
import com.google.android.material.bottomsheet.BottomSheetBehavior

internal fun <T : View> BottomSheetBehavior<T>.updateMetrics(
maxAllowedHeight: Int? = null,
expandedOffsetFromTop: Int? = null,
): BottomSheetBehavior<T> {
maxAllowedHeight?.let {
this.maxHeight = maxAllowedHeight
}
expandedOffsetFromTop?.let {
this.expandedOffset = expandedOffsetFromTop
}
return this
}

internal fun <T : View> BottomSheetBehavior<T>.useSingleDetent(
maxAllowedHeight: Int? = null,
forceExpandedState: Boolean = true,
Expand All @@ -13,7 +26,7 @@ internal fun <T : View> BottomSheetBehavior<T>.useSingleDetent(
this.state = BottomSheetBehavior.STATE_EXPANDED
}
maxAllowedHeight?.let {
maxHeight = maxAllowedHeight
this.maxHeight = maxAllowedHeight
}
return this
}
Expand All @@ -23,11 +36,11 @@ internal fun <T : View> BottomSheetBehavior<T>.useTwoDetents(
firstHeight: Int? = null,
maxAllowedHeight: Int? = null,
): BottomSheetBehavior<T> {
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
}

Expand All @@ -38,12 +51,12 @@ internal fun <T : View> BottomSheetBehavior<T>.useThreeDetents(
halfExpandedRatio: Float? = null,
expandedOffsetFromTop: Int? = null,
): BottomSheetBehavior<T> {
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
}
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
@@ -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<OnApplyWindowInsetsListener>()

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
}
}
Loading
Loading