Skip to content

feat: respect window insets a parent adjusts for its subtree on Android - #791

Merged
janicduplessis merged 3 commits into
mainfrom
@janic/android-dispatched-insets
Oct 21, 2026
Merged

janicduplessis merged 3 commits into
mainfrom
@janic/android-dispatched-insets

Conversation

@janicduplessis

@janicduplessis janicduplessis commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

On Android, SafeAreaProvider (and so useSafeAreaInsets and SafeAreaView) now computes insets from the WindowInsets dispatched to the provider instead of the root window insets. A parent that adjusts insets for its subtree, by overriding dispatchApplyWindowInsets or with an OnApplyWindowInsetsListener that returns larger insets, is now respected. iOS already behaves this way, since the provider reads its own safeAreaInsets.

Motivation: software-mansion/react-native-screens#4998 adds an additionalSafeAreaInsets prop to Screen, mirroring UIKit, so a custom floating tab bar drawn in JS over the content can add its height to the bottom insets of the screen's subtree. On iOS it sets the screen controller's additionalSafeAreaInsets, which safe-area-context already picks up through UIKit with no library change. On Android the screen enlarges the WindowInsets it dispatches to its children, and safe-area-context needs this PR to see them.

The inset math is unchanged: the same inset types on API 30+, the same keyboard exclusion on API 23-29, and the same per-view clipping, so a provider that does not reach an edge still gets 0 there.

  • Dispatched insets can only add to the root insets. Each edge is the max of the root and dispatched value. Ancestors also lower the insets they dispatch without making that area safe: React Native's translucent StatusBar (setStatusBarTranslucency) dispatches a top inset of 0 so fitsSystemWindows adds no padding, and react-native-screens' ScreenWindowTraits does the same. Taking dispatched values as-is made the top inset 0 under a translucent status bar.

  • Provider inside the container is required. SafeAreaView still reads its nearest provider's insets, as on iOS, so a container between the provider and the SafeAreaView is only picked up when a SafeAreaProvider sits inside it. For react-native-screens, that means a provider inside each screen.

  • Fallback to root insets. When no dispatch has reached the provider, the root insets are used as before. That covers non edge-to-edge apps, where the decor view consumes the insets and nothing is dispatched to React Native views, and providers under an ancestor that consumes insets (React Native's own SafeAreaView returns CONSUMED). If the root insets change and no new dispatch reached the provider, the stored value is treated as stale and the root insets are used. API < 23 keeps the getWindowVisibleDisplayFrame path.

  • requestApplyInsets() on attach. Views added after the last dispatch only receive insets on the next one, so the provider requests a dispatch when it attaches. React Native core's ReactSafeAreaView does the same. A SafeAreaView without a provider records and requests its own dispatched insets.

The example app gets an "Additional Insets" screen: content in a react-native-screens Screen (through ScreenStack / ScreenStackItem) with additionalSafeAreaInsets={{ bottom: 80 }} and a toggle, holding a SafeAreaProvider, a SafeAreaView and a ScrollView, next to the root provider's values. software-mansion/react-native-screens#4998 is not released, so the example applies it to react-native-screens 4.25.2 with patch-package (example/patches/react-native-screens+4.25.2+001+additional-safe-area-insets-from-pr-4998.patch, from software-mansion/react-native-screens#4998 commits cc3654a and 6b0d209). The patch is temporary and goes away once software-mansion/react-native-screens#4998 ships.

Risks

  • Behavior change for apps where an ancestor adds to the dispatched insets. Such apps used to get the root insets and now get the larger dispatched ones. That is the point of the change, but an app that adds insets for its own native views would now see them in JS too.
  • Conflicts with fix: ignore a system bar hidden through the system UI visibility on Android 6-10 #785, which rewrites the API 23-29 inset function and getSafeAreaInsets.
  • First frame inside an adjusting container. The attach-time computation runs before the requested dispatch lands, so the provider can emit the root-based value once, then the dispatched one. I kept it so the first measurement is not delayed for everyone else.
  • Stale value if an ancestor stops adding insets and also stops dispatching to the provider while the root insets stay the same, until the root insets change or the provider re-attaches.
  • API 23-29. The bottom inset is still min(systemWindowInsetBottom, stableInsetBottom) to exclude the keyboard, so extra bottom insets added through replaceSystemWindowInsets are capped at the stable inset.
  • Cost. Each provider attach triggers one window-wide insets dispatch on the next traversal, which re-runs every insets listener in the window. Repeated requests within a frame coalesce. Worth watching with screens that mount many providers ([Android] Slow screen opening after react-native update #448 is an existing Android perf report).

Related: #603 (edge-to-edge), #738 and #758 (transient insets), #555, #634 and #667 (insets reported on Samsung devices). This PR does not claim to fix any of them.

Test Plan

Example app, new architecture. Open the dev menu and choose "Show Additional Insets Example". The switch toggles additionalSafeAreaInsets bottom 80 on the screen.

Bottom 80 Bottom 0
iOS, iPhone 18 Pro, iOS 27.0
Android API 36, this PR
Android API 36, main
  • iOS: the provider inside the screen reports bottom 114 (34 + 80), 34 with the switch off. The SafeAreaView padding and the scroll view's automatic content inset both end at the top of the JS tab bar. iOS native code in this repo is unchanged.
  • Android with this PR: bottom 104 (24 + 80), 24 with the switch off, updated live. The scroll view uses useSafeAreaInsets for its bottom padding on Android, since Android scroll views have no automatic insets.
  • Android on main, same patched react-native-screens: bottom stays 24 with the switch on.
  • Root provider: unchanged on both platforms (top 116 · bottom 34 on iOS, where react-native-test-app hosts the app in a navigation controller, and top 24 · bottom 24 on Android).

Regression checks on Android API 36 (edge-to-edge forced by targetSdk 36), gesture navigation:

  • Native Stack example: home screen 0/0/0/0, modal screen top 24 · bottom 24, same as main.
  • Hooks example: insets stay 24/24 with the keyboard open (Gboard), in landscape, and with <StatusBar translucent /> (temporary edit, not in the diff; the first commit alone gave top 0 there).
  • React Native Modal (separate dialog window, statusBarTranslucent and navigationBarTranslucent) with a provider inside: top 24 · bottom 24. This was checked with a temporary screen that is not in the diff.
Keyboard open Landscape RN Modal

Also run on an API 30 emulator, which is not edge-to-edge in this example app. Both providers report 0/0, because the decor consumes the insets and nothing is dispatched to the React Native views. The fallback gives the same values as main.

Not tested: the old architecture (same Kotlin classes, no arch-specific code), API 23-29 devices, and a SafeAreaView rendered without any provider. iOS native code is unchanged.

SafeAreaProvider and SafeAreaView now compute insets from the WindowInsets dispatched to the provider view, so a parent that adjusts insets for its subtree is respected. Root window insets remain the fallback when no dispatch reached the view. The example app gains a native test container that adds bottom insets for its children.
@vercel

vercel Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
react-native-safe-area-context Ignored Ignored Preview Oct 21, 2026 3:27am UTC

Request Review

React Native's translucent StatusBar lowers the dispatched top inset to 0 to skip fitsSystemWindows padding, which made the safe area top 0. Dispatched insets now only add to the root insets.
The Additional Insets example renders its content in a react-native-screens Screen with additionalSafeAreaInsets, applied to the example through a temporary patch-package patch from software-mansion/react-native-screens#4998. It replaces the custom native test container.
@janicduplessis
janicduplessis marked this pull request as ready for review October 21, 2026 04:20
@janicduplessis
janicduplessis merged commit e8fa330 into main Oct 21, 2026
8 checks passed
@janicduplessis
janicduplessis deleted the @janic/android-dispatched-insets branch October 21, 2026 04:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant