Skip to content

Commit acfaaf4

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 8d658f4 commit acfaaf4

14 files changed

Lines changed: 342 additions & 55 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/Libraries/Utilities/useWindowDimensions.js‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,21 @@ import {
1515
} from './NativeDeviceInfo';
1616
import {useEffect, useState} from 'react';
1717

18+
function safeAreaInsetsAreEqual(
19+
a: DisplayMetrics['experimental_safeAreaInsets'],
20+
b: DisplayMetrics['experimental_safeAreaInsets'],
21+
): boolean {
22+
if (a == null || b == null) {
23+
return a == null && b == null;
24+
}
25+
return (
26+
a.top === b.top &&
27+
a.right === b.right &&
28+
a.bottom === b.bottom &&
29+
a.left === b.left
30+
);
31+
}
32+
1833
/**
1934
* React hook that provides the application window's width, height, scale, and
2035
* font scale. Automatically updates when screen size or font scale changes.
@@ -35,7 +50,11 @@ export default function useWindowDimensions():
3550
dimensions.width !== window.width ||
3651
dimensions.height !== window.height ||
3752
dimensions.scale !== window.scale ||
38-
dimensions.fontScale !== window.fontScale
53+
dimensions.fontScale !== window.fontScale ||
54+
!safeAreaInsetsAreEqual(
55+
dimensions.experimental_safeAreaInsets,
56+
window.experimental_safeAreaInsets,
57+
)
3958
) {
4059
setDimensions(window);
4160
}

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,11 +202,19 @@ static BOOL RCTIsIPhoneNotched()
202202
// We fallback to screen size if a key window is not found.
203203
CGSize windowSize = mainWindow != nil ? mainWindow.bounds.size : screenSize;
204204

205-
NSDictionary<NSString *, NSNumber *> *dimsWindow = @{
205+
UIEdgeInsets insets = mainWindow.safeAreaInsets;
206+
id safeAreaInsets = mainWindow != nil
207+
? @{@"top" : @(insets.top),
208+
@"right" : @(insets.right),
209+
@"bottom" : @(insets.bottom),
210+
@"left" : @(insets.left)}
211+
: (id)kCFNull;
212+
NSDictionary<NSString *, id> *dimsWindow = @{
206213
@"width" : @(windowSize.width),
207214
@"height" : @(windowSize.height),
208215
@"scale" : @(screen.scale),
209-
@"fontScale" : @(fontScale)
216+
@"fontScale" : @(fontScale),
217+
@"experimental_safeAreaInsets" : safeAreaInsets
210218
};
211219

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

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

Lines changed: 21 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,33 @@ internal class DeviceInfoModule(reactContext: ReactApplicationContext) :
8384
WritableNativeMap().apply {
8485
putMap(
8586
"windowPhysicalPixels",
86-
getPhysicalPixelsWritableMap(getWindowDisplayMetrics()),
87+
getPhysicalPixelsWritableMap(getWindowDisplayMetrics()).apply {
88+
getWindowSafeAreaInsetsWritableMap()?.let {
89+
putMap("experimental_safeAreaInsets", it)
90+
}
91+
},
8792
)
8893
putMap(
8994
"screenPhysicalPixels",
9095
getPhysicalPixelsWritableMap(getScreenDisplayMetrics()),
9196
)
9297
}
9398

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

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,13 @@
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 =
21+
NativeDeviceInfoDisplayMetricsAndroid<double, double, double, double, double, std::optional<WindowSafeAreaInsets>>;
1822

1923
using DimensionsPayload = NativeDeviceInfoDimensionsPayload<
2024
std::optional<DisplayMetrics>,
@@ -25,6 +29,9 @@ using DimensionsPayload = NativeDeviceInfoDimensionsPayload<
2529
using DeviceInfoConstants =
2630
NativeDeviceInfoDeviceInfoConstants<DimensionsPayload, std::optional<bool>, std::optional<bool>>;
2731

32+
template <>
33+
struct Bridging<WindowSafeAreaInsets> : NativeDeviceInfoWindowSafeAreaInsetsBridging<WindowSafeAreaInsets> {};
34+
2835
template <>
2936
struct Bridging<DisplayMetrics> : NativeDeviceInfoDisplayMetricsBridging<DisplayMetrics> {};
3037

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,28 @@ 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+
readonly experimental_safeAreaInsets?: WindowSafeAreaInsets,
2129
};
2230

2331
export type DisplayMetrics = {
2432
width: number,
2533
height: number,
2634
scale: number,
2735
fontScale: number,
36+
readonly experimental_safeAreaInsets?: ?WindowSafeAreaInsets,
2837
};
2938

3039
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'];
@@ -273,6 +274,25 @@ function ScrollBenchmark(): React.Node {
273274
);
274275
}
275276

277+
function WindowInsetsExample(): React.Node {
278+
const {
279+
width,
280+
height,
281+
experimental_safeAreaInsets: safeAreaInsets,
282+
} = useWindowDimensions();
283+
284+
return (
285+
<View style={styles.readout}>
286+
<RNTesterText>{`window: {width: ${width}, height: ${height}}`}</RNTesterText>
287+
<RNTesterText>
288+
{safeAreaInsets == null
289+
? 'safeAreaInsets: not available'
290+
: `safeAreaInsets: {top: ${safeAreaInsets.top}, right: ${safeAreaInsets.right}, bottom: ${safeAreaInsets.bottom}, left: ${safeAreaInsets.left}}`}
291+
</RNTesterText>
292+
</View>
293+
);
294+
}
295+
276296
const styles = StyleSheet.create({
277297
readout: {
278298
backgroundColor: '#ffaaaa',
@@ -356,4 +376,10 @@ exports.examples = [
356376
'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.',
357377
render: (): React.Node => <ScrollBenchmark />,
358378
},
379+
{
380+
title: 'Window safe area insets from Dimensions',
381+
description:
382+
'The `Dimensions` module reports the safe area insets of the window, available synchronously at startup.',
383+
render: (): React.Node => <WindowInsetsExample />,
384+
},
359385
] as Array<RNTesterModuleExample>;

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

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

3993+
template <typename P0, typename P1, typename P2, typename P3, typename P4, typename P5>
3994+
struct facebook::react::NativeDeviceInfoDisplayMetricsAndroid {
3995+
public P0 width;
3996+
public P1 height;
3997+
public P2 scale;
3998+
public P3 fontScale;
3999+
public P4 densityDpi;
4000+
public bool operator==(const facebook::react::NativeDeviceInfoDisplayMetricsAndroid& other) const;
4001+
}
4002+
39934003
template <typename P0, typename P1, typename P2, typename P3, typename P4, typename P5>
39944004
struct facebook::react::NativeIntersectionObserverNativeIntersectionObserverObserveOptions {
39954005
public P0 intersectionObserverId;
@@ -4013,13 +4023,12 @@ struct facebook::react::NativeResizeObserverNativeResizeObserverEntry {
40134023
}
40144024

40154025
template <typename P0, typename P1, typename P2, typename P3, typename P4>
4016-
struct facebook::react::NativeDeviceInfoDisplayMetricsAndroid {
4026+
struct facebook::react::NativeDeviceInfoDisplayMetrics {
40174027
public P0 width;
40184028
public P1 height;
40194029
public P2 scale;
40204030
public P3 fontScale;
4021-
public P4 densityDpi;
4022-
public bool operator==(const facebook::react::NativeDeviceInfoDisplayMetricsAndroid& other) const;
4031+
public bool operator==(const facebook::react::NativeDeviceInfoDisplayMetrics& other) const;
40234032
}
40244033

40254034
template <typename P0, typename P1, typename P2, typename P3, typename P4>
@@ -4052,12 +4061,12 @@ struct facebook::react::NativeDeviceInfoDimensionsPayload {
40524061
}
40534062

40544063
template <typename P0, typename P1, typename P2, typename P3>
4055-
struct facebook::react::NativeDeviceInfoDisplayMetrics {
4056-
public P0 width;
4057-
public P1 height;
4058-
public P2 scale;
4059-
public P3 fontScale;
4060-
public bool operator==(const facebook::react::NativeDeviceInfoDisplayMetrics& other) const;
4064+
struct facebook::react::NativeDeviceInfoWindowSafeAreaInsets {
4065+
public P0 top;
4066+
public P1 right;
4067+
public P2 bottom;
4068+
public P3 left;
4069+
public bool operator==(const facebook::react::NativeDeviceInfoWindowSafeAreaInsets& other) const;
40614070
}
40624071

40634072
template <typename P0, typename P1, typename P2, typename P3>
@@ -4762,6 +4771,17 @@ struct facebook::react::NativeDeviceInfoDisplayMetricsBridging {
47624771
public static facebook::jsi::Object toJs(facebook::jsi::Runtime& rt, const T& value, const std::shared_ptr<facebook::react::CallInvoker>& jsInvoker);
47634772
}
47644773

4774+
template <typename T>
4775+
struct facebook::react::NativeDeviceInfoWindowSafeAreaInsetsBridging {
4776+
public static T fromJs(facebook::jsi::Runtime& rt, const facebook::jsi::Object& value, const std::shared_ptr<facebook::react::CallInvoker>& jsInvoker);
4777+
public static T types;
4778+
public static double bottomToJs(facebook::jsi::Runtime& rt, decltype(types.bottom) value);
4779+
public static double leftToJs(facebook::jsi::Runtime& rt, decltype(types.left) value);
4780+
public static double rightToJs(facebook::jsi::Runtime& rt, decltype(types.right) value);
4781+
public static double topToJs(facebook::jsi::Runtime& rt, decltype(types.top) value);
4782+
public static facebook::jsi::Object toJs(facebook::jsi::Runtime& rt, const T& value, const std::shared_ptr<facebook::react::CallInvoker>& jsInvoker);
4783+
}
4784+
47654785
template <typename T>
47664786
struct facebook::react::NativeDialogManagerAndroidDialogOptionsBridging {
47674787
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)