Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
25 changes: 21 additions & 4 deletions packages/react-native/Libraries/Utilities/Dimensions.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,16 @@ import NativeDeviceInfo, {
type DimensionsPayload,
type DisplayMetrics,
type DisplayMetricsAndroid,
type WindowSafeAreaInsets,
} from './NativeDeviceInfo';
import invariant from 'invariant';

export type {DimensionsPayload, DisplayMetrics, DisplayMetricsAndroid};
export type {
DimensionsPayload,
DisplayMetrics,
DisplayMetricsAndroid,
WindowSafeAreaInsets,
};

/** @deprecated Use DisplayMetrics */
export type ScaledSize = DisplayMetrics;
Expand Down Expand Up @@ -72,11 +78,22 @@ class Dimensions {
let {screen, window} = dims;
const {windowPhysicalPixels} = dims;
if (windowPhysicalPixels) {
const {scale, experimental_safeAreaInsets: safeAreaInsets} =
windowPhysicalPixels;
window = {
width: windowPhysicalPixels.width / windowPhysicalPixels.scale,
height: windowPhysicalPixels.height / windowPhysicalPixels.scale,
scale: windowPhysicalPixels.scale,
width: windowPhysicalPixels.width / scale,
height: windowPhysicalPixels.height / scale,
scale,
fontScale: windowPhysicalPixels.fontScale,
experimental_safeAreaInsets:
safeAreaInsets == null
? undefined
: {
top: safeAreaInsets.top / scale,
right: safeAreaInsets.right / scale,
bottom: safeAreaInsets.bottom / scale,
left: safeAreaInsets.left / scale,
},
};
}
const {screenPhysicalPixels} = dims;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,26 @@ describe('Dimensions', () => {
expect(Dimensions.get('window').fontScale).toEqual(3);
});

it('should scale window safe area insets from physical pixels', () => {
Dimensions.set({
windowPhysicalPixels: {
width: 400,
height: 800,
scale: 2,
densityDpi: 2,
fontScale: 3,
experimental_safeAreaInsets: {top: 96, right: 0, bottom: 48, left: 0},
},
});

expect(Dimensions.get('window').experimental_safeAreaInsets).toEqual({
top: 48,
right: 0,
bottom: 24,
left: 0,
});
});

it('should set screen dimensions on Android', () => {
// $FlowFixMe[incompatible-type] - `Platform.OS` needs to be read-only.
Platform.OS = 'android';
Expand Down
21 changes: 20 additions & 1 deletion packages/react-native/Libraries/Utilities/useWindowDimensions.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,21 @@ import {
} from './NativeDeviceInfo';
import {useEffect, useState} from 'react';

function safeAreaInsetsAreEqual(
a: DisplayMetrics['experimental_safeAreaInsets'],
b: DisplayMetrics['experimental_safeAreaInsets'],
): boolean {
if (a == null || b == null) {
return a == null && b == null;
}
return (
a.top === b.top &&
a.right === b.right &&
a.bottom === b.bottom &&
a.left === b.left
);
}

/**
* React hook that provides the application window's width, height, scale, and
* font scale. Automatically updates when screen size or font scale changes.
Expand All @@ -35,7 +50,11 @@ export default function useWindowDimensions():
dimensions.width !== window.width ||
dimensions.height !== window.height ||
dimensions.scale !== window.scale ||
dimensions.fontScale !== window.fontScale
dimensions.fontScale !== window.fontScale ||
!safeAreaInsetsAreEqual(
dimensions.experimental_safeAreaInsets,
window.experimental_safeAreaInsets,
)
) {
setDimensions(window);
}
Expand Down
18 changes: 15 additions & 3 deletions packages/react-native/React/CoreModules/RCTDeviceInfo.mm
Original file line number Diff line number Diff line change
Expand Up @@ -202,12 +202,24 @@ static BOOL RCTIsIPhoneNotched()
// We fallback to screen size if a key window is not found.
CGSize windowSize = mainWindow != nil ? mainWindow.bounds.size : screenSize;

NSDictionary<NSString *, NSNumber *> *dimsWindow = @{
NSMutableDictionary<NSString *, id> *dimsWindow = [@{
@"width" : @(windowSize.width),
@"height" : @(windowSize.height),
@"scale" : @(screen.scale),
@"fontScale" : @(fontScale)
};
@"fontScale" : @(fontScale),
} mutableCopy];
// The field is documented as absent when it cannot be measured; without a
// window there are no insets to report, and zero would read as a
// measurement.
if (mainWindow != nil) {
UIEdgeInsets safeAreaInsets = mainWindow.safeAreaInsets;
dimsWindow[@"experimental_safeAreaInsets"] = @{
@"top" : @(safeAreaInsets.top),
@"right" : @(safeAreaInsets.right),
@"bottom" : @(safeAreaInsets.bottom),
@"left" : @(safeAreaInsets.left)
};
}

NSDictionary<NSString *, NSNumber *> *dimsScreen = @{
@"width" : @(screenSize.width),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import com.facebook.react.bridge.WritableNativeMap
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.DisplayMetricsHolder.getScreenDisplayMetrics
import com.facebook.react.uimanager.DisplayMetricsHolder.initDisplayMetricsIfNotInitialized
import com.facebook.react.uimanager.internal.SafeAreaInsetsObserver
import com.facebook.react.views.view.isEdgeToEdgeFeatureFlagOn

/** Module that exposes Android Constants to JS. */
Expand Down Expand Up @@ -83,14 +84,33 @@ internal class DeviceInfoModule(reactContext: ReactApplicationContext) :
WritableNativeMap().apply {
putMap(
"windowPhysicalPixels",
getPhysicalPixelsWritableMap(getWindowDisplayMetrics()),
getPhysicalPixelsWritableMap(getWindowDisplayMetrics()).apply {
getWindowSafeAreaInsetsWritableMap()?.let {
putMap("experimental_safeAreaInsets", it)
}
},
)
putMap(
"screenPhysicalPixels",
getPhysicalPixelsWritableMap(getScreenDisplayMetrics()),
)
}

/**
* The part of the window that is covered by the system UI, in physical pixels. Uses the same
* computation as the `onSafeAreaInsetsChange` view prop, applied to the window's decor view.
*/
private fun getWindowSafeAreaInsetsWritableMap(): WritableMap? {
val decorView = reactApplicationContext.currentActivity?.window?.decorView ?: return null
val insets = SafeAreaInsetsObserver.getSafeAreaInsets(decorView) ?: return null
return WritableNativeMap().apply {
putInt("top", insets.top)
putInt("right", insets.right)
putInt("bottom", insets.bottom)
putInt("left", insets.left)
}
}

private fun getPhysicalPixelsWritableMap(
displayMetrics: DisplayMetrics,
): WritableMap =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@

namespace facebook::react {

using DisplayMetrics = NativeDeviceInfoDisplayMetrics<double, double, double, double>;
using WindowSafeAreaInsets = NativeDeviceInfoWindowSafeAreaInsets<double, double, double, double>;

using DisplayMetricsAndroid = NativeDeviceInfoDisplayMetricsAndroid<double, double, double, double, double>;
using DisplayMetrics =
NativeDeviceInfoDisplayMetrics<double, double, double, double, std::optional<WindowSafeAreaInsets>>;

using DisplayMetricsAndroid =
NativeDeviceInfoDisplayMetricsAndroid<double, double, double, double, double, std::optional<WindowSafeAreaInsets>>;

using DimensionsPayload = NativeDeviceInfoDimensionsPayload<
std::optional<DisplayMetrics>,
Expand All @@ -25,6 +29,9 @@ using DimensionsPayload = NativeDeviceInfoDimensionsPayload<
using DeviceInfoConstants =
NativeDeviceInfoDeviceInfoConstants<DimensionsPayload, std::optional<bool>, std::optional<bool>>;

template <>
struct Bridging<WindowSafeAreaInsets> : NativeDeviceInfoWindowSafeAreaInsetsBridging<WindowSafeAreaInsets> {};

template <>
struct Bridging<DisplayMetrics> : NativeDeviceInfoDisplayMetricsBridging<DisplayMetrics> {};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,40 @@ import type {TurboModule} from '../../../../Libraries/TurboModule/RCTExport';

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

export type WindowSafeAreaInsets = {
top: number,
right: number,
bottom: number,
left: number,
};

export type DisplayMetricsAndroid = {
width: number,
height: number,
scale: number,
fontScale: number,
densityDpi: number,
/**
* The part of the window that is covered by the system UI, in physical
* pixels. Absent on platforms and versions that cannot report it.
*
* @experimental
*/
readonly experimental_safeAreaInsets?: WindowSafeAreaInsets,
};

export type DisplayMetrics = {
width: number,
height: number,
scale: number,
fontScale: number,
/**
* The part of the window that is covered by the system UI, in physical
* pixels. Absent on platforms and versions that cannot report it.
*
* @experimental
*/
readonly experimental_safeAreaInsets?: WindowSafeAreaInsets,
};

export type DimensionsPayload = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
StyleSheet,
TextInput,
View,
useWindowDimensions,
} from 'react-native';

type Insets = SafeAreaInsetsChangeEvent['nativeEvent']['insets'];
Expand Down Expand Up @@ -273,6 +274,25 @@ function ScrollBenchmark(): React.Node {
);
}

function WindowInsetsExample(): React.Node {
const {
width,
height,
experimental_safeAreaInsets: safeAreaInsets,
} = useWindowDimensions();

return (
<View style={styles.readout}>
<RNTesterText>{`window: {width: ${width}, height: ${height}}`}</RNTesterText>
<RNTesterText>
{safeAreaInsets == null
? 'safeAreaInsets: not available'
: `safeAreaInsets: {top: ${safeAreaInsets.top}, right: ${safeAreaInsets.right}, bottom: ${safeAreaInsets.bottom}, left: ${safeAreaInsets.left}}`}
</RNTesterText>
</View>
);
}

const styles = StyleSheet.create({
readout: {
backgroundColor: '#ffaaaa',
Expand Down Expand Up @@ -356,4 +376,10 @@ exports.examples = [
'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.',
render: (): React.Node => <ScrollBenchmark />,
},
{
title: 'Window safe area insets from Dimensions',
description:
'The `Dimensions` module reports the safe area insets of the window, available synchronously at startup.',
render: (): React.Node => <WindowInsetsExample />,
},
] as Array<RNTesterModuleExample>;
38 changes: 29 additions & 9 deletions scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api
Original file line number Diff line number Diff line change
Expand Up @@ -8581,6 +8581,16 @@ struct facebook::react::NativeIntersectionObserverNativeIntersectionObserverEntr
public bool operator==(const facebook::react::NativeIntersectionObserverNativeIntersectionObserverEntry& other) const;
}

template <typename P0, typename P1, typename P2, typename P3, typename P4, typename P5>
struct facebook::react::NativeDeviceInfoDisplayMetricsAndroid {
public P0 width;
public P1 height;
public P2 scale;
public P3 fontScale;
public P4 densityDpi;
public bool operator==(const facebook::react::NativeDeviceInfoDisplayMetricsAndroid& other) const;
}

template <typename P0, typename P1, typename P2, typename P3, typename P4, typename P5>
struct facebook::react::NativeIntersectionObserverNativeIntersectionObserverObserveOptions {
public P0 intersectionObserverId;
Expand All @@ -8604,13 +8614,12 @@ struct facebook::react::NativeResizeObserverNativeResizeObserverEntry {
}

template <typename P0, typename P1, typename P2, typename P3, typename P4>
struct facebook::react::NativeDeviceInfoDisplayMetricsAndroid {
struct facebook::react::NativeDeviceInfoDisplayMetrics {
public P0 width;
public P1 height;
public P2 scale;
public P3 fontScale;
public P4 densityDpi;
public bool operator==(const facebook::react::NativeDeviceInfoDisplayMetricsAndroid& other) const;
public bool operator==(const facebook::react::NativeDeviceInfoDisplayMetrics& other) const;
}

template <typename P0, typename P1, typename P2, typename P3, typename P4>
Expand Down Expand Up @@ -8643,12 +8652,12 @@ struct facebook::react::NativeDeviceInfoDimensionsPayload {
}

template <typename P0, typename P1, typename P2, typename P3>
struct facebook::react::NativeDeviceInfoDisplayMetrics {
public P0 width;
public P1 height;
public P2 scale;
public P3 fontScale;
public bool operator==(const facebook::react::NativeDeviceInfoDisplayMetrics& other) const;
struct facebook::react::NativeDeviceInfoWindowSafeAreaInsets {
public P0 top;
public P1 right;
public P2 bottom;
public P3 left;
public bool operator==(const facebook::react::NativeDeviceInfoWindowSafeAreaInsets& other) const;
}

template <typename P0, typename P1, typename P2, typename P3>
Expand Down Expand Up @@ -9411,6 +9420,17 @@ struct facebook::react::NativeDeviceInfoDisplayMetricsBridging {
public static facebook::jsi::Object toJs(facebook::jsi::Runtime& rt, const T& value, const std::shared_ptr<facebook::react::CallInvoker>& jsInvoker);
}

template <typename T>
struct facebook::react::NativeDeviceInfoWindowSafeAreaInsetsBridging {
public static T fromJs(facebook::jsi::Runtime& rt, const facebook::jsi::Object& value, const std::shared_ptr<facebook::react::CallInvoker>& jsInvoker);
public static T types;
public static double bottomToJs(facebook::jsi::Runtime& rt, decltype(types.bottom) value);
public static double leftToJs(facebook::jsi::Runtime& rt, decltype(types.left) value);
public static double rightToJs(facebook::jsi::Runtime& rt, decltype(types.right) value);
public static double topToJs(facebook::jsi::Runtime& rt, decltype(types.top) value);
public static facebook::jsi::Object toJs(facebook::jsi::Runtime& rt, const T& value, const std::shared_ptr<facebook::react::CallInvoker>& jsInvoker);
}

template <typename T>
struct facebook::react::NativeDialogManagerAndroidDialogOptionsBridging {
public static T fromJs(facebook::jsi::Runtime& rt, const facebook::jsi::Object& value, const std::shared_ptr<facebook::react::CallInvoker>& jsInvoker);
Expand Down
Loading