Skip to content

Commit e71752a

Browse files
Report the window safe area insets through Dimensions
`Dimensions.get('window').experimental_safeAreaInsets` (and `useWindowDimensions`) reports the safe area insets of the window, using the same native computation as the view prop applied to the window itself. Unlike the prop, this is available synchronously at startup — no event has to arrive first — and it updates through the existing `change` event. That is what the safe area context library needs `initialWindowMetrics` for, its last remaining native module, and it is what lets a component render padded on its very first frame instead of correcting itself once the first inset event lands. The field is absent on platforms and versions that cannot report it, rather than reported as zeros, so a consumer can tell "no insets" from "unknown".
1 parent dc6df95 commit e71752a

13 files changed

Lines changed: 336 additions & 54 deletions

File tree

packages/react-native/Libraries/Utilities/Dimensions.js

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,16 @@ import NativeDeviceInfo, {
1616
type DimensionsPayload,
1717
type DisplayMetrics,
1818
type DisplayMetricsAndroid,
19+
type WindowSafeAreaInsets,
1920
} from './NativeDeviceInfo';
2021
import invariant from 'invariant';
2122

22-
export type {DimensionsPayload, DisplayMetrics, DisplayMetricsAndroid};
23+
export type {
24+
DimensionsPayload,
25+
DisplayMetrics,
26+
DisplayMetricsAndroid,
27+
WindowSafeAreaInsets,
28+
};
2329

2430
/** @deprecated Use DisplayMetrics */
2531
export type ScaledSize = DisplayMetrics;
@@ -72,11 +78,22 @@ class Dimensions {
7278
let {screen, window} = dims;
7379
const {windowPhysicalPixels} = dims;
7480
if (windowPhysicalPixels) {
81+
const {scale, experimental_safeAreaInsets: safeAreaInsets} =
82+
windowPhysicalPixels;
7583
window = {
76-
width: windowPhysicalPixels.width / windowPhysicalPixels.scale,
77-
height: windowPhysicalPixels.height / windowPhysicalPixels.scale,
78-
scale: windowPhysicalPixels.scale,
84+
width: windowPhysicalPixels.width / scale,
85+
height: windowPhysicalPixels.height / scale,
86+
scale,
7987
fontScale: windowPhysicalPixels.fontScale,
88+
experimental_safeAreaInsets:
89+
safeAreaInsets == null
90+
? undefined
91+
: {
92+
top: safeAreaInsets.top / scale,
93+
right: safeAreaInsets.right / scale,
94+
bottom: safeAreaInsets.bottom / scale,
95+
left: safeAreaInsets.left / scale,
96+
},
8097
};
8198
}
8299
const {screenPhysicalPixels} = dims;

packages/react-native/Libraries/Utilities/__tests__/Dimensions-itest.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,26 @@ describe('Dimensions', () => {
3030
expect(Dimensions.get('window').fontScale).toEqual(3);
3131
});
3232

33+
it('should scale window safe area insets from physical pixels', () => {
34+
Dimensions.set({
35+
windowPhysicalPixels: {
36+
width: 400,
37+
height: 800,
38+
scale: 2,
39+
densityDpi: 2,
40+
fontScale: 3,
41+
experimental_safeAreaInsets: {top: 96, right: 0, bottom: 48, left: 0},
42+
},
43+
});
44+
45+
expect(Dimensions.get('window').experimental_safeAreaInsets).toEqual({
46+
top: 48,
47+
right: 0,
48+
bottom: 24,
49+
left: 0,
50+
});
51+
});
52+
3353
it('should set screen dimensions on Android', () => {
3454
// $FlowFixMe[incompatible-type] - `Platform.OS` needs to be read-only.
3555
Platform.OS = 'android';

packages/react-native/React/CoreModules/RCTDeviceInfo.mm

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,12 +201,19 @@ static BOOL RCTIsIPhoneNotched()
201201

202202
// We fallback to screen size if a key window is not found.
203203
CGSize windowSize = mainWindow != nil ? mainWindow.bounds.size : screenSize;
204+
UIEdgeInsets safeAreaInsets = mainWindow != nil ? mainWindow.safeAreaInsets : UIEdgeInsetsZero;
204205

205-
NSDictionary<NSString *, NSNumber *> *dimsWindow = @{
206+
NSDictionary<NSString *, id> *dimsWindow = @{
206207
@"width" : @(windowSize.width),
207208
@"height" : @(windowSize.height),
208209
@"scale" : @(screen.scale),
209-
@"fontScale" : @(fontScale)
210+
@"fontScale" : @(fontScale),
211+
@"experimental_safeAreaInsets" : @{
212+
@"top" : @(safeAreaInsets.top),
213+
@"right" : @(safeAreaInsets.right),
214+
@"bottom" : @(safeAreaInsets.bottom),
215+
@"left" : @(safeAreaInsets.left)
216+
}
210217
};
211218

212219
NSDictionary<NSString *, NSNumber *> *dimsScreen = @{

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/deviceinfo/DeviceInfoModule.kt

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import com.facebook.react.bridge.WritableNativeMap
2323
import com.facebook.react.module.annotations.ReactModule
2424
import com.facebook.react.uimanager.DisplayMetricsHolder.getScreenDisplayMetrics
2525
import com.facebook.react.uimanager.DisplayMetricsHolder.initDisplayMetricsIfNotInitialized
26+
import com.facebook.react.uimanager.internal.SafeAreaInsetsObserver
2627
import com.facebook.react.views.view.isEdgeToEdgeFeatureFlagOn
2728

2829
/** Module that exposes Android Constants to JS. */
@@ -83,14 +84,31 @@ internal class DeviceInfoModule(reactContext: ReactApplicationContext) :
8384
WritableNativeMap().apply {
8485
putMap(
8586
"windowPhysicalPixels",
86-
getPhysicalPixelsWritableMap(getWindowDisplayMetrics()),
87+
getPhysicalPixelsWritableMap(getWindowDisplayMetrics()).apply {
88+
getWindowSafeAreaInsetsWritableMap()?.let { putMap("experimental_safeAreaInsets", it) }
89+
},
8790
)
8891
putMap(
8992
"screenPhysicalPixels",
9093
getPhysicalPixelsWritableMap(getScreenDisplayMetrics()),
9194
)
9295
}
9396

97+
/**
98+
* The part of the window that is covered by the system UI, in physical pixels. Uses the same
99+
* computation as the `onSafeAreaInsetsChange` view prop, applied to the window's decor view.
100+
*/
101+
private fun getWindowSafeAreaInsetsWritableMap(): WritableMap? {
102+
val decorView = reactApplicationContext.currentActivity?.window?.decorView ?: return null
103+
val insets = SafeAreaInsetsObserver.getSafeAreaInsets(decorView) ?: return null
104+
return WritableNativeMap().apply {
105+
putInt("top", insets.top)
106+
putInt("right", insets.right)
107+
putInt("bottom", insets.bottom)
108+
putInt("left", insets.left)
109+
}
110+
}
111+
94112
private fun getPhysicalPixelsWritableMap(
95113
displayMetrics: DisplayMetrics,
96114
): WritableMap =

packages/react-native/ReactCxxPlatform/react/coremodules/DeviceInfoModule.h

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,18 @@
1212

1313
namespace facebook::react {
1414

15-
using DisplayMetrics = NativeDeviceInfoDisplayMetrics<double, double, double, double>;
15+
using WindowSafeAreaInsets = NativeDeviceInfoWindowSafeAreaInsets<double, double, double, double>;
1616

17-
using DisplayMetricsAndroid = NativeDeviceInfoDisplayMetricsAndroid<double, double, double, double, double>;
17+
using DisplayMetrics =
18+
NativeDeviceInfoDisplayMetrics<double, double, double, double, std::optional<WindowSafeAreaInsets>>;
19+
20+
using DisplayMetricsAndroid = NativeDeviceInfoDisplayMetricsAndroid<
21+
double,
22+
double,
23+
double,
24+
double,
25+
double,
26+
std::optional<WindowSafeAreaInsets>>;
1827

1928
using DimensionsPayload = NativeDeviceInfoDimensionsPayload<
2029
std::optional<DisplayMetrics>,
@@ -25,6 +34,9 @@ using DimensionsPayload = NativeDeviceInfoDimensionsPayload<
2534
using DeviceInfoConstants =
2635
NativeDeviceInfoDeviceInfoConstants<DimensionsPayload, std::optional<bool>, std::optional<bool>>;
2736

37+
template <>
38+
struct Bridging<WindowSafeAreaInsets> : NativeDeviceInfoWindowSafeAreaInsetsBridging<WindowSafeAreaInsets> {};
39+
2840
template <>
2941
struct Bridging<DisplayMetrics> : NativeDeviceInfoDisplayMetricsBridging<DisplayMetrics> {};
3042

packages/react-native/src/private/specs_DEPRECATED/modules/NativeDeviceInfo.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,40 @@ import type {TurboModule} from '../../../../Libraries/TurboModule/RCTExport';
1212

1313
import * as TurboModuleRegistry from '../../../../Libraries/TurboModule/TurboModuleRegistry';
1414

15+
export type WindowSafeAreaInsets = {
16+
top: number,
17+
right: number,
18+
bottom: number,
19+
left: number,
20+
};
21+
1522
export type DisplayMetricsAndroid = {
1623
width: number,
1724
height: number,
1825
scale: number,
1926
fontScale: number,
2027
densityDpi: number,
28+
/**
29+
* The part of the window that is covered by the system UI, in physical
30+
* pixels. Absent on platforms and versions that cannot report it.
31+
*
32+
* @experimental
33+
*/
34+
readonly experimental_safeAreaInsets?: WindowSafeAreaInsets,
2135
};
2236

2337
export type DisplayMetrics = {
2438
width: number,
2539
height: number,
2640
scale: number,
2741
fontScale: number,
42+
/**
43+
* The part of the window that is covered by the system UI, in physical
44+
* pixels. Absent on platforms and versions that cannot report it.
45+
*
46+
* @experimental
47+
*/
48+
readonly experimental_safeAreaInsets?: WindowSafeAreaInsets,
2849
};
2950

3051
export type DimensionsPayload = {

packages/rn-tester/js/examples/SafeAreaInsets/SafeAreaInsetsExample.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
StyleSheet,
2424
TextInput,
2525
View,
26+
useWindowDimensions,
2627
} from 'react-native';
2728

2829
type Insets = SafeAreaInsetsChangeEvent['nativeEvent']['insets'];
@@ -221,6 +222,25 @@ function ScrollBenchmark(): React.Node {
221222
);
222223
}
223224

225+
function WindowInsetsExample(): React.Node {
226+
const {
227+
width,
228+
height,
229+
experimental_safeAreaInsets: safeAreaInsets,
230+
} = useWindowDimensions();
231+
232+
return (
233+
<View style={styles.readout}>
234+
<RNTesterText>{`window: {width: ${width}, height: ${height}}`}</RNTesterText>
235+
<RNTesterText>
236+
{safeAreaInsets == null
237+
? 'safeAreaInsets: not available'
238+
: `safeAreaInsets: {top: ${safeAreaInsets.top}, right: ${safeAreaInsets.right}, bottom: ${safeAreaInsets.bottom}, left: ${safeAreaInsets.left}}`}
239+
</RNTesterText>
240+
</View>
241+
);
242+
}
243+
224244
const styles = StyleSheet.create({
225245
readout: {
226246
backgroundColor: '#ffaaaa',
@@ -293,4 +313,10 @@ exports.examples = [
293313
'Rows observing their own insets inside a scroll view. Scrolling moves the rows, so observing rows may emit inset events while crossing safe area boundaries.',
294314
render: (): React.Node => <ScrollBenchmark />,
295315
},
316+
{
317+
title: 'Window safe area insets from Dimensions',
318+
description:
319+
'The `Dimensions` module reports the safe area insets of the window, available synchronously at startup.',
320+
render: (): React.Node => <WindowInsetsExample />,
321+
},
296322
] as Array<RNTesterModuleExample>;

scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8553,6 +8553,16 @@ struct facebook::react::NativeIntersectionObserverNativeIntersectionObserverEntr
85538553
public bool operator==(const facebook::react::NativeIntersectionObserverNativeIntersectionObserverEntry& other) const;
85548554
}
85558555

8556+
template <typename P0, typename P1, typename P2, typename P3, typename P4, typename P5>
8557+
struct facebook::react::NativeDeviceInfoDisplayMetricsAndroid {
8558+
public P0 width;
8559+
public P1 height;
8560+
public P2 scale;
8561+
public P3 fontScale;
8562+
public P4 densityDpi;
8563+
public bool operator==(const facebook::react::NativeDeviceInfoDisplayMetricsAndroid& other) const;
8564+
}
8565+
85568566
template <typename P0, typename P1, typename P2, typename P3, typename P4, typename P5>
85578567
struct facebook::react::NativeIntersectionObserverNativeIntersectionObserverObserveOptions {
85588568
public P0 intersectionObserverId;
@@ -8576,13 +8586,12 @@ struct facebook::react::NativeResizeObserverNativeResizeObserverEntry {
85768586
}
85778587

85788588
template <typename P0, typename P1, typename P2, typename P3, typename P4>
8579-
struct facebook::react::NativeDeviceInfoDisplayMetricsAndroid {
8589+
struct facebook::react::NativeDeviceInfoDisplayMetrics {
85808590
public P0 width;
85818591
public P1 height;
85828592
public P2 scale;
85838593
public P3 fontScale;
8584-
public P4 densityDpi;
8585-
public bool operator==(const facebook::react::NativeDeviceInfoDisplayMetricsAndroid& other) const;
8594+
public bool operator==(const facebook::react::NativeDeviceInfoDisplayMetrics& other) const;
85868595
}
85878596

85888597
template <typename P0, typename P1, typename P2, typename P3, typename P4>
@@ -8615,12 +8624,12 @@ struct facebook::react::NativeDeviceInfoDimensionsPayload {
86158624
}
86168625

86178626
template <typename P0, typename P1, typename P2, typename P3>
8618-
struct facebook::react::NativeDeviceInfoDisplayMetrics {
8619-
public P0 width;
8620-
public P1 height;
8621-
public P2 scale;
8622-
public P3 fontScale;
8623-
public bool operator==(const facebook::react::NativeDeviceInfoDisplayMetrics& other) const;
8627+
struct facebook::react::NativeDeviceInfoWindowSafeAreaInsets {
8628+
public P0 top;
8629+
public P1 right;
8630+
public P2 bottom;
8631+
public P3 left;
8632+
public bool operator==(const facebook::react::NativeDeviceInfoWindowSafeAreaInsets& other) const;
86248633
}
86258634

86268635
template <typename P0, typename P1, typename P2, typename P3>
@@ -9383,6 +9392,17 @@ struct facebook::react::NativeDeviceInfoDisplayMetricsBridging {
93839392
public static facebook::jsi::Object toJs(facebook::jsi::Runtime& rt, const T& value, const std::shared_ptr<facebook::react::CallInvoker>& jsInvoker);
93849393
}
93859394

9395+
template <typename T>
9396+
struct facebook::react::NativeDeviceInfoWindowSafeAreaInsetsBridging {
9397+
public static T fromJs(facebook::jsi::Runtime& rt, const facebook::jsi::Object& value, const std::shared_ptr<facebook::react::CallInvoker>& jsInvoker);
9398+
public static T types;
9399+
public static double bottomToJs(facebook::jsi::Runtime& rt, decltype(types.bottom) value);
9400+
public static double leftToJs(facebook::jsi::Runtime& rt, decltype(types.left) value);
9401+
public static double rightToJs(facebook::jsi::Runtime& rt, decltype(types.right) value);
9402+
public static double topToJs(facebook::jsi::Runtime& rt, decltype(types.top) value);
9403+
public static facebook::jsi::Object toJs(facebook::jsi::Runtime& rt, const T& value, const std::shared_ptr<facebook::react::CallInvoker>& jsInvoker);
9404+
}
9405+
93869406
template <typename T>
93879407
struct facebook::react::NativeDialogManagerAndroidDialogOptionsBridging {
93889408
public static T fromJs(facebook::jsi::Runtime& rt, const facebook::jsi::Object& value, const std::shared_ptr<facebook::react::CallInvoker>& jsInvoker);

scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8313,6 +8313,16 @@ struct facebook::react::NativeIntersectionObserverNativeIntersectionObserverEntr
83138313
public bool operator==(const facebook::react::NativeIntersectionObserverNativeIntersectionObserverEntry& other) const;
83148314
}
83158315

8316+
template <typename P0, typename P1, typename P2, typename P3, typename P4, typename P5>
8317+
struct facebook::react::NativeDeviceInfoDisplayMetricsAndroid {
8318+
public P0 width;
8319+
public P1 height;
8320+
public P2 scale;
8321+
public P3 fontScale;
8322+
public P4 densityDpi;
8323+
public bool operator==(const facebook::react::NativeDeviceInfoDisplayMetricsAndroid& other) const;
8324+
}
8325+
83168326
template <typename P0, typename P1, typename P2, typename P3, typename P4, typename P5>
83178327
struct facebook::react::NativeIntersectionObserverNativeIntersectionObserverObserveOptions {
83188328
public P0 intersectionObserverId;
@@ -8336,13 +8346,12 @@ struct facebook::react::NativeResizeObserverNativeResizeObserverEntry {
83368346
}
83378347

83388348
template <typename P0, typename P1, typename P2, typename P3, typename P4>
8339-
struct facebook::react::NativeDeviceInfoDisplayMetricsAndroid {
8349+
struct facebook::react::NativeDeviceInfoDisplayMetrics {
83408350
public P0 width;
83418351
public P1 height;
83428352
public P2 scale;
83438353
public P3 fontScale;
8344-
public P4 densityDpi;
8345-
public bool operator==(const facebook::react::NativeDeviceInfoDisplayMetricsAndroid& other) const;
8354+
public bool operator==(const facebook::react::NativeDeviceInfoDisplayMetrics& other) const;
83468355
}
83478356

83488357
template <typename P0, typename P1, typename P2, typename P3, typename P4>
@@ -8375,12 +8384,12 @@ struct facebook::react::NativeDeviceInfoDimensionsPayload {
83758384
}
83768385

83778386
template <typename P0, typename P1, typename P2, typename P3>
8378-
struct facebook::react::NativeDeviceInfoDisplayMetrics {
8379-
public P0 width;
8380-
public P1 height;
8381-
public P2 scale;
8382-
public P3 fontScale;
8383-
public bool operator==(const facebook::react::NativeDeviceInfoDisplayMetrics& other) const;
8387+
struct facebook::react::NativeDeviceInfoWindowSafeAreaInsets {
8388+
public P0 top;
8389+
public P1 right;
8390+
public P2 bottom;
8391+
public P3 left;
8392+
public bool operator==(const facebook::react::NativeDeviceInfoWindowSafeAreaInsets& other) const;
83848393
}
83858394

83868395
template <typename P0, typename P1, typename P2, typename P3>
@@ -9103,6 +9112,13 @@ struct facebook::react::NativeDeviceInfoDisplayMetricsBridging {
91039112
public static facebook::jsi::Object toJs(facebook::jsi::Runtime& rt, const T& value, const std::shared_ptr<facebook::react::CallInvoker>& jsInvoker);
91049113
}
91059114

9115+
template <typename T>
9116+
struct facebook::react::NativeDeviceInfoWindowSafeAreaInsetsBridging {
9117+
public static T fromJs(facebook::jsi::Runtime& rt, const facebook::jsi::Object& value, const std::shared_ptr<facebook::react::CallInvoker>& jsInvoker);
9118+
public static T types;
9119+
public static facebook::jsi::Object toJs(facebook::jsi::Runtime& rt, const T& value, const std::shared_ptr<facebook::react::CallInvoker>& jsInvoker);
9120+
}
9121+
91069122
template <typename T>
91079123
struct facebook::react::NativeDialogManagerAndroidDialogOptionsBridging {
91089124
public static T fromJs(facebook::jsi::Runtime& rt, const facebook::jsi::Object& value, const std::shared_ptr<facebook::react::CallInvoker>& jsInvoker);

0 commit comments

Comments
 (0)