diff --git a/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec.h b/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec.h index 21d5174c475538..5708b9d3eedd89 100644 --- a/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec.h +++ b/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec.h @@ -687,107 +687,13 @@ namespace facebook { } // namespace react } // namespace facebook -namespace JS { - namespace NativeDeviceInfo { - struct DisplayMetricsIOS { - - struct Builder { - struct Input { - RCTRequired width; - RCTRequired height; - RCTRequired scale; - RCTRequired fontScale; - }; - - /** Initialize with a set of values */ - Builder(const Input i); - /** Initialize with an existing DisplayMetricsIOS */ - Builder(DisplayMetricsIOS i); - /** Builds the object. Generally used only by the infrastructure. */ - NSDictionary *buildUnsafeRawValue() const { return _factory(); }; - private: - NSDictionary *(^_factory)(void); - }; - - static DisplayMetricsIOS fromUnsafeRawValue(NSDictionary *const v) { return {v}; } - NSDictionary *unsafeRawValue() const { return _v; } - private: - DisplayMetricsIOS(NSDictionary *const v) : _v(v) {} - NSDictionary *_v; - }; - } -} - -namespace JS { - namespace NativeDeviceInfo { - struct DisplayMetricsAndroid { - - struct Builder { - struct Input { - RCTRequired width; - RCTRequired height; - RCTRequired scale; - RCTRequired fontScale; - RCTRequired densityDpi; - }; - - /** Initialize with a set of values */ - Builder(const Input i); - /** Initialize with an existing DisplayMetricsAndroid */ - Builder(DisplayMetricsAndroid i); - /** Builds the object. Generally used only by the infrastructure. */ - NSDictionary *buildUnsafeRawValue() const { return _factory(); }; - private: - NSDictionary *(^_factory)(void); - }; - - static DisplayMetricsAndroid fromUnsafeRawValue(NSDictionary *const v) { return {v}; } - NSDictionary *unsafeRawValue() const { return _v; } - private: - DisplayMetricsAndroid(NSDictionary *const v) : _v(v) {} - NSDictionary *_v; - }; - } -} - -namespace JS { - namespace NativeDeviceInfo { - struct ConstantsDimensions { - - struct Builder { - struct Input { - folly::Optional window; - folly::Optional screen; - folly::Optional windowPhysicalPixels; - folly::Optional screenPhysicalPixels; - }; - - /** Initialize with a set of values */ - Builder(const Input i); - /** Initialize with an existing ConstantsDimensions */ - Builder(ConstantsDimensions i); - /** Builds the object. Generally used only by the infrastructure. */ - NSDictionary *buildUnsafeRawValue() const { return _factory(); }; - private: - NSDictionary *(^_factory)(void); - }; - - static ConstantsDimensions fromUnsafeRawValue(NSDictionary *const v) { return {v}; } - NSDictionary *unsafeRawValue() const { return _v; } - private: - ConstantsDimensions(NSDictionary *const v) : _v(v) {} - NSDictionary *_v; - }; - } -} - namespace JS { namespace NativeDeviceInfo { struct Constants { struct Builder { struct Input { - RCTRequired Dimensions; + RCTRequired> Dimensions; folly::Optional isIPhoneX_deprecated; }; @@ -2520,57 +2426,10 @@ inline JS::NativeBlobModule::Constants::Builder::Builder(const Input i) : _facto inline JS::NativeBlobModule::Constants::Builder::Builder(Constants i) : _factory(^{ return i.unsafeRawValue(); }) {} -inline JS::NativeDeviceInfo::DisplayMetricsIOS::Builder::Builder(const Input i) : _factory(^{ - NSMutableDictionary *d = [NSMutableDictionary new]; - auto width = i.width.get(); - d[@"width"] = @(width); - auto height = i.height.get(); - d[@"height"] = @(height); - auto scale = i.scale.get(); - d[@"scale"] = @(scale); - auto fontScale = i.fontScale.get(); - d[@"fontScale"] = @(fontScale); - return d; -}) {} -inline JS::NativeDeviceInfo::DisplayMetricsIOS::Builder::Builder(DisplayMetricsIOS i) : _factory(^{ - return i.unsafeRawValue(); -}) {} -inline JS::NativeDeviceInfo::DisplayMetricsAndroid::Builder::Builder(const Input i) : _factory(^{ - NSMutableDictionary *d = [NSMutableDictionary new]; - auto width = i.width.get(); - d[@"width"] = @(width); - auto height = i.height.get(); - d[@"height"] = @(height); - auto scale = i.scale.get(); - d[@"scale"] = @(scale); - auto fontScale = i.fontScale.get(); - d[@"fontScale"] = @(fontScale); - auto densityDpi = i.densityDpi.get(); - d[@"densityDpi"] = @(densityDpi); - return d; -}) {} -inline JS::NativeDeviceInfo::DisplayMetricsAndroid::Builder::Builder(DisplayMetricsAndroid i) : _factory(^{ - return i.unsafeRawValue(); -}) {} -inline JS::NativeDeviceInfo::ConstantsDimensions::Builder::Builder(const Input i) : _factory(^{ - NSMutableDictionary *d = [NSMutableDictionary new]; - auto window = i.window; - d[@"window"] = window.hasValue() ? window.value().buildUnsafeRawValue() : nil; - auto screen = i.screen; - d[@"screen"] = screen.hasValue() ? screen.value().buildUnsafeRawValue() : nil; - auto windowPhysicalPixels = i.windowPhysicalPixels; - d[@"windowPhysicalPixels"] = windowPhysicalPixels.hasValue() ? windowPhysicalPixels.value().buildUnsafeRawValue() : nil; - auto screenPhysicalPixels = i.screenPhysicalPixels; - d[@"screenPhysicalPixels"] = screenPhysicalPixels.hasValue() ? screenPhysicalPixels.value().buildUnsafeRawValue() : nil; - return d; -}) {} -inline JS::NativeDeviceInfo::ConstantsDimensions::Builder::Builder(ConstantsDimensions i) : _factory(^{ - return i.unsafeRawValue(); -}) {} inline JS::NativeDeviceInfo::Constants::Builder::Builder(const Input i) : _factory(^{ NSMutableDictionary *d = [NSMutableDictionary new]; auto Dimensions = i.Dimensions.get(); - d[@"Dimensions"] = Dimensions.buildUnsafeRawValue(); + d[@"Dimensions"] = Dimensions; auto isIPhoneX_deprecated = i.isIPhoneX_deprecated; d[@"isIPhoneX_deprecated"] = isIPhoneX_deprecated.hasValue() ? @((BOOL)isIPhoneX_deprecated.value()) : nil; return d; diff --git a/Libraries/Utilities/Dimensions.js b/Libraries/Utilities/Dimensions.js index 508f6b0403dae5..145f032347b00e 100644 --- a/Libraries/Utilities/Dimensions.js +++ b/Libraries/Utilities/Dimensions.js @@ -10,90 +10,84 @@ 'use strict'; -const EventEmitter = require('../vendor/emitter/EventEmitter'); -const Platform = require('./Platform'); -const RCTDeviceEventEmitter = require('../EventEmitter/RCTDeviceEventEmitter'); +import EventEmitter from '../vendor/emitter/EventEmitter'; +import RCTDeviceEventEmitter from '../EventEmitter/RCTDeviceEventEmitter'; +import NativeDeviceInfo, { + type DisplayMetrics, + type DimensionsPayload, +} from './NativeDeviceInfo'; +import invariant from 'invariant'; -import NativeDeviceInfo from './NativeDeviceInfo'; - -const invariant = require('invariant'); +type DimensionsValue = {window?: DisplayMetrics, screen?: DisplayMetrics}; const eventEmitter = new EventEmitter(); let dimensionsInitialized = false; -const dimensions = {}; +let dimensions: DimensionsValue; + class Dimensions { + /** + * NOTE: `useWindowDimensions` is the preffered API for React components. + * + * Initial dimensions are set before `runApplication` is called so they should + * be available before any other require's are run, but may be updated later. + * + * Note: Although dimensions are available immediately, they may change (e.g + * due to device rotation) so any rendering logic or styles that depend on + * these constants should try to call this function on every render, rather + * than caching the value (for example, using inline styles rather than + * setting a value in a `StyleSheet`). + * + * Example: `const {height, width} = Dimensions.get('window');` + * + * @param {string} dim Name of dimension as defined when calling `set`. + * @returns {Object?} Value for the dimension. + */ + static get(dim: string): Object { + invariant(dimensions[dim], 'No dimension set for key ' + dim); + return dimensions[dim]; + } + /** * This should only be called from native code by sending the * didUpdateDimensions event. * * @param {object} dims Simple string-keyed object of dimensions to set */ - static set(dims: {[key: string]: any}): void { + static set(dims: $ReadOnly<{[key: string]: any}>): void { // We calculate the window dimensions in JS so that we don't encounter loss of // precision in transferring the dimensions (which could be non-integers) over // the bridge. - if (dims && dims.windowPhysicalPixels) { - // parse/stringify => Clone hack - dims = JSON.parse(JSON.stringify(dims)); - - const windowPhysicalPixels = dims.windowPhysicalPixels; - dims.window = { + let {screen, window} = dims; + const {windowPhysicalPixels} = dims; + if (windowPhysicalPixels) { + window = { width: windowPhysicalPixels.width / windowPhysicalPixels.scale, height: windowPhysicalPixels.height / windowPhysicalPixels.scale, scale: windowPhysicalPixels.scale, fontScale: windowPhysicalPixels.fontScale, }; - if (Platform.OS === 'android') { - // Screen and window dimensions are different on android - const screenPhysicalPixels = dims.screenPhysicalPixels; - dims.screen = { - width: screenPhysicalPixels.width / screenPhysicalPixels.scale, - height: screenPhysicalPixels.height / screenPhysicalPixels.scale, - scale: screenPhysicalPixels.scale, - fontScale: screenPhysicalPixels.fontScale, - }; - - // delete so no callers rely on this existing - delete dims.screenPhysicalPixels; - } else { - dims.screen = dims.window; - } - // delete so no callers rely on this existing - delete dims.windowPhysicalPixels; + } + const {screenPhysicalPixels} = dims; + if (screenPhysicalPixels) { + screen = { + width: screenPhysicalPixels.width / screenPhysicalPixels.scale, + height: screenPhysicalPixels.height / screenPhysicalPixels.scale, + scale: screenPhysicalPixels.scale, + fontScale: screenPhysicalPixels.fontScale, + }; + } else if (screen == null) { + screen = window; } - Object.assign(dimensions, dims); + dimensions = {window, screen}; if (dimensionsInitialized) { // Don't fire 'change' the first time the dimensions are set. - eventEmitter.emit('change', { - window: dimensions.window, - screen: dimensions.screen, - }); + eventEmitter.emit('change', dimensions); } else { dimensionsInitialized = true; } } - /** - * Initial dimensions are set before `runApplication` is called so they should - * be available before any other require's are run, but may be updated later. - * - * Note: Although dimensions are available immediately, they may change (e.g - * due to device rotation) so any rendering logic or styles that depend on - * these constants should try to call this function on every render, rather - * than caching the value (for example, using inline styles rather than - * setting a value in a `StyleSheet`). - * - * Example: `var {height, width} = Dimensions.get('window');` - * - * @param {string} dim Name of dimension as defined when calling `set`. - * @returns {Object?} Value for the dimension. - */ - static get(dim: string): Object { - invariant(dimensions[dim], 'No dimension set for key ' + dim); - return dimensions[dim]; - } - /** * Add an event handler. Supported events: * @@ -102,7 +96,7 @@ class Dimensions { * are the same as the return values of `Dimensions.get('window')` and * `Dimensions.get('screen')`, respectively. */ - static addEventListener(type: string, handler: Function) { + static addEventListener(type: 'change', handler: Function) { invariant( type === 'change', 'Trying to subscribe to unknown event: "%s"', @@ -114,7 +108,7 @@ class Dimensions { /** * Remove an event handler. */ - static removeEventListener(type: string, handler: Function) { + static removeEventListener(type: 'change', handler: Function) { invariant( type === 'change', 'Trying to remove listener for unknown event: "%s"', @@ -124,25 +118,13 @@ class Dimensions { } } -let dims: ?{[key: string]: any} = - global.nativeExtensions && - global.nativeExtensions.DeviceInfo && - global.nativeExtensions.DeviceInfo.Dimensions; -let nativeExtensionsEnabled = true; -if (!dims) { - dims = NativeDeviceInfo.getConstants().Dimensions; - nativeExtensionsEnabled = false; -} - -invariant( - dims, - 'Either DeviceInfo native extension or DeviceInfo Native Module must be registered', -); -Dimensions.set(dims); -if (!nativeExtensionsEnabled) { - RCTDeviceEventEmitter.addListener('didUpdateDimensions', function(update) { +// Subscribe before calling getConstants to make sure we don't miss any updates in between. +RCTDeviceEventEmitter.addListener( + 'didUpdateDimensions', + (update: DimensionsPayload) => { Dimensions.set(update); - }); -} + }, +); +Dimensions.set(NativeDeviceInfo.getConstants().Dimensions); module.exports = Dimensions; diff --git a/Libraries/Utilities/NativeDeviceInfo.js b/Libraries/Utilities/NativeDeviceInfo.js index 8e0b785a6fbc4f..d0cf2eb88087e7 100644 --- a/Libraries/Utilities/NativeDeviceInfo.js +++ b/Libraries/Utilities/NativeDeviceInfo.js @@ -13,29 +13,31 @@ import type {TurboModule} from '../TurboModule/RCTExport'; import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry'; -type DisplayMetricsAndroid = {| +type DisplayMetricsAndroid = $ReadOnly<{| width: number, height: number, scale: number, fontScale: number, densityDpi: number, -|}; +|}>; -type DisplayMetricsIOS = {| +export type DisplayMetrics = $ReadOnly<{| width: number, height: number, scale: number, fontScale: number, -|}; +|}>; + +export type DimensionsPayload = $ReadOnly<{| + window?: DisplayMetrics, + screen?: DisplayMetrics, + windowPhysicalPixels?: DisplayMetricsAndroid, + screenPhysicalPixels?: DisplayMetricsAndroid, +|}>; export interface Spec extends TurboModule { +getConstants: () => {| - +Dimensions: { - window?: DisplayMetricsIOS, - screen?: DisplayMetricsIOS, - windowPhysicalPixels?: DisplayMetricsAndroid, - screenPhysicalPixels?: DisplayMetricsAndroid, - }, + +Dimensions: DimensionsPayload, +isIPhoneX_deprecated?: boolean, |}; } diff --git a/Libraries/Utilities/useWindowDimensions.js b/Libraries/Utilities/useWindowDimensions.js new file mode 100644 index 00000000000000..42be11d75c1a99 --- /dev/null +++ b/Libraries/Utilities/useWindowDimensions.js @@ -0,0 +1,35 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow strict-local + */ + +'use strict'; + +import Dimensions from './Dimensions'; +import {type DisplayMetrics} from './NativeDeviceInfo'; +import * as React from 'react'; + +export default function useWindowDimensions(): DisplayMetrics { + const dims = Dimensions.get('window'); // always read the latest value + const forceUpdate = React.useState()[1]; + const initialDims = React.useState(dims)[0]; + React.useEffect(() => { + Dimensions.addEventListener('change', forceUpdate); + + const latestDims = Dimensions.get('window'); + if (latestDims !== initialDims) { + // We missed an update between calling `get` in render and + // `addEventListener` in this handler... + forceUpdate(); + } + return () => { + Dimensions.removeEventListener('change', forceUpdate); + }; + }, [forceUpdate, initialDims]); + return dims; +} diff --git a/Libraries/react-native/react-native-implementation.js b/Libraries/react-native/react-native-implementation.js index 10b92a98b31399..8c6f23b86b3a7f 100644 --- a/Libraries/react-native/react-native-implementation.js +++ b/Libraries/react-native/react-native-implementation.js @@ -299,6 +299,9 @@ module.exports = { get unstable_batchedUpdates() { return require('../Renderer/shims/ReactNative').unstable_batchedUpdates; }, + get useWindowDimensions() { + return require('../Utilities/useWindowDimensions').default; + }, get UTFSequence() { return require('../UTFSequence'); }, diff --git a/RNTester/js/examples/Dimensions/DimensionsExample.js b/RNTester/js/examples/Dimensions/DimensionsExample.js index c7c21063e15103..6ba52411f05bfe 100644 --- a/RNTester/js/examples/Dimensions/DimensionsExample.js +++ b/RNTester/js/examples/Dimensions/DimensionsExample.js @@ -10,8 +10,8 @@ 'use strict'; -const React = require('react'); -const {Dimensions, Text, View} = require('react-native'); +import * as React from 'react'; +import {Dimensions, Text, useWindowDimensions} from 'react-native'; class DimensionsSubscription extends React.Component< {dim: string}, @@ -36,11 +36,7 @@ class DimensionsSubscription extends React.Component< }; render() { - return ( - - {JSON.stringify(this.state.dims)} - - ); + return {JSON.stringify(this.state.dims, null, 2)}; } } @@ -48,13 +44,23 @@ exports.title = 'Dimensions'; exports.description = 'Dimensions of the viewport'; exports.examples = [ { - title: 'window', + title: 'useWindowDimensions hook', + render() { + const DimensionsViaHook = () => { + const dims = useWindowDimensions(); + return {JSON.stringify(dims, null, 2)}; + }; + return ; + }, + }, + { + title: 'Non-component `get` API: window', render(): React.Element { return ; }, }, { - title: 'screen', + title: 'Non-component `get` API: screen', render(): React.Element { return ; },