feat(Android, Stack v4): add disable*InsetApplication props#4220
Conversation
There was a problem hiding this comment.
Pull request overview
Adds per-edge opt-out controls for applying Android header insets in the Stack v4 implementation, extending the existing top-edge opt-out to left/right/bottom and wiring the decisions through JS context down to native toolbar padding.
Changes:
- Introduces new public props:
disableLeftInsetApplication,disableRightInsetApplication,disableBottomInsetApplication. - Adds new native props (
consumeLeftInset,consumeRightInset,consumeBottomInset) and Android-side plumbing to apply them. - Refactors Android header inset computation to evaluate each edge independently; adds an issue-test app screen (Test4220) to exercise the behavior.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/types.tsx | Adds the new public disable*InsetApplication props to the JS API types. |
| src/fabric/ScreenStackHeaderConfigNativeComponent.ts | Extends the Fabric native component spec with consumeLeft/Right/BottomInset. |
| src/components/ScreenStackItem.tsx | Adds edge-inset context providers to propagate opt-out through nested stacks. |
| src/components/ScreenStackHeaderConfig.tsx | Consumes the new edge-inset context and forwards consume*Inset to native. |
| src/components/contexts/EdgeInsetApplicationContext.ts | Introduces contexts + hook for propagating left/right/bottom opt-out down the subtree. |
| android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfigViewManager.kt | Adds setters for the new consume*Inset props. |
| android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.kt | Stores the new consume*Inset flags and requests inset re-application on change. |
| android/src/main/java/com/swmansion/rnscreens/CustomToolbar.kt | Reworks inset calculation so top/left/right/bottom are handled independently. |
| apps/src/tests/issue-tests/Test4220.tsx | Adds a manual issue test UI to toggle the new props and validate behavior. |
| apps/src/tests/issue-tests/index.ts | Exports the new Test4220 entry. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| * When set to `true`, disables left inset handling for this header and all headers in its | ||
| * subtree. | ||
| * | ||
| * Once disabled by an ancestor, it cannot be re-enabled by descendants. |
There was a problem hiding this comment.
once inset application is disabled == once flag is enabled is a bit confusing, maybe we should be more precise here. Applies to other jsdocs as well.
| <TopInsetApplicationContext.Provider value={nextContextValue}> | ||
| <DebugContainer | ||
| contentStyle={contentStyle} | ||
| style={debugContainerStyle} | ||
| stackPresentation={stackPresentationWithDefault}> | ||
| {shouldUseSafeAreaView ? ( | ||
| <SafeAreaView edges={getSafeAreaEdges(headerConfig)}> | ||
| {children} | ||
| </SafeAreaView> | ||
| ) : ( | ||
| children | ||
| )} | ||
| </DebugContainer> | ||
| <LeftInsetApplicationContext.Provider value={nextLeftContextValue}> | ||
| <RightInsetApplicationContext.Provider value={nextRightContextValue}> | ||
| <BottomInsetApplicationContext.Provider | ||
| value={nextBottomContextValue}> |
There was a problem hiding this comment.
Maybe we should use just one context with multiple props (or two - one for top, one for rest)?
| } else { | ||
| 0 | ||
| } | ||
| val bottomInset = if (shouldHandleBottomInset) cutoutInsets.bottom else 0 |
There was a problem hiding this comment.
To be honest, should the bottom inset be ever applied? But that's out of scope of this PR.
There was a problem hiding this comment.
Honestly, I don't know if there's any reasonable use case for that. Also it is probably applied very rarely and looks pretty weird (look at the video regarding bottom inset). It's certainly a thing to discuss.
There was a problem hiding this comment.
when the phone has a double cutout, the padding to the header will be applied and it won't look good; I think we should create a ticket to zero out the bottom padding
There was a problem hiding this comment.
I've created a ticket: https://github.com/software-mansion/react-native-screens-labs/issues/1585
t0maboro
left a comment
There was a problem hiding this comment.
runtime seems to be working fine, left some cleanup requests
| * The semantics differ between the top edge and the rest: | ||
| * - `topAlreadyApplied`: the top inset is coordinated "consume-once" across nested headers, | ||
| * so `true` means an ancestor header already applied it and the subtree should skip it. | ||
| * - `left/right/bottomDisabled`: these insets are applied per-header (every header spans the | ||
| * full width and needs the inset on its own edges, and a custom `SafeAreaView` may consume | ||
| * them for a part of the subtree). The flags therefore only propagate the opt-out down the | ||
| * subtree: `true` means an ancestor header opted out, so the whole subtree should skip it. |
## Description This PR adds per-edge control over header inset application on Android. Previously only the top inset could be opted out, via the already-existing `disableTopInsetApplication` prop. This PR introduces three new sibling props - `disableLeftInsetApplication`, `disableRightInsetApplication` and `disableBottomInsetApplication` — so that an app can disable inset handling on any edge of the native header. The top inset is a single shared space at the top of the screen, so in nested stacks it must be coordinated - only the topmost visible header consumes it (to avoid doubled spacing). The top inset is additionally anchored to the decor view in `applyDecorViewTopInsetIfNeeded()`, so it survives a custom `SafeAreaView` consuming the incoming `WindowInsets` somewhere in the subtree. Left/right/bottom are per-edge: each header applies them on its own edges, so there is nothing to coordinate, and — unlike the top — they have no decor-view fallback. They are read solely from the incoming `WindowInsets` and applied per-header, which is why a `SafeAreaView` in the subtree can consume them per-subtree. The context only propagates the opt-out down the subtree (set the prop once on the outer stack, inner headers inherit it). Closes software-mansion/react-native-screens-labs#1535 ## Changes - Added public props `disableLeftInsetApplication`, `disableRightInsetApplication` and `disableBottomInsetApplication` to `ScreenStackHeaderConfigProps` (`src/types.tsx`) - Added `consumeLeftInset` / `consumeRightInset` / `consumeBottomInset` to the native component spec (`ScreenStackHeaderConfigNativeComponent.ts`) and the corresponding setters on the Android view manager / config (`ScreenStackHeaderConfigViewManager.kt`, `ScreenStackHeaderConfig.kt`) - Introduced `EdgeInsetApplicationContext` carrying the left/right/bottom opt-out down the subtree, and wired the providers in `ScreenStackItem.tsx` and the consumer in `ScreenStackHeaderConfig.tsx` - Reworked the inset computation in `CustomToolbar.kt`: each edge (top/left/right/bottom) is now evaluated independently - Added an issue test ## After - visual documentation https://github.com/user-attachments/assets/1f747d3b-00d2-46f1-9058-b0bce3f02d61 https://github.com/user-attachments/assets/279e1b58-429d-4183-947a-ffa68e4129bb https://github.com/user-attachments/assets/5fee3e50-d519-4fc1-b694-9b0465ea28a0 https://github.com/user-attachments/assets/26b33fea-5486-4e82-9f8b-01c39ee8c9e9 ## Test plan Run the 4220 issue test on different devices and check if insets are disabled correctly. ## Checklist - [x] Included code example that can be used to test this change. - [x] For visual changes, included screenshots / GIFs / recordings documenting the change. - [ ] For API changes, updated relevant public types. - [ ] Ensured that CI passes
Description
This PR adds per-edge control over header inset application on Android. Previously only the
top inset could be opted out, via the already-existing
disableTopInsetApplicationprop.This PR introduces three new sibling props -
disableLeftInsetApplication,disableRightInsetApplicationanddisableBottomInsetApplication— so that an app can disableinset handling on any edge of the native header.
The top inset is a single shared space at the top of the screen, so in nested stacks it must
be coordinated - only the topmost visible header consumes it (to avoid doubled spacing). The top
inset is additionally anchored to the decor view in
applyDecorViewTopInsetIfNeeded(), so itsurvives a custom
SafeAreaViewconsuming the incomingWindowInsetssomewhere in the subtree.Left/right/bottom are per-edge: each header applies them on its own edges, so there is nothing
to coordinate, and — unlike the top — they have no decor-view fallback. They are read solely from
the incoming
WindowInsetsand applied per-header, which is why aSafeAreaViewin the subtreecan consume them per-subtree. The context only propagates the opt-out down the subtree (set the
prop once on the outer stack, inner headers inherit it).
Closes https://github.com/software-mansion/react-native-screens-labs/issues/1535
Changes
disableLeftInsetApplication,disableRightInsetApplicationanddisableBottomInsetApplicationtoScreenStackHeaderConfigProps(src/types.tsx)consumeLeftInset/consumeRightInset/consumeBottomInsetto the native componentspec (
ScreenStackHeaderConfigNativeComponent.ts) and the corresponding setters on the Androidview manager / config (
ScreenStackHeaderConfigViewManager.kt,ScreenStackHeaderConfig.kt)EdgeInsetApplicationContextcarrying the left/right/bottom opt-out down the subtree, and wired the providers inScreenStackItem.tsxand the consumer inScreenStackHeaderConfig.tsxCustomToolbar.kt: each edge (top/left/right/bottom) is nowevaluated independently
After - visual documentation
top.inset.mov
left.inset.mov
right.inset.mov
bottom.inset.mov
Test plan
Run the 4220 issue test on different devices and check if insets are disabled correctly.
Checklist