Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New LayoutContext API to access the frame and safe area of the root view #20999

Closed
wants to merge 10 commits into from
1 change: 1 addition & 0 deletions Libraries/ReactNative/AppRegistry.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ const AppRegistry = {
),
appParameters.initialProps,
appParameters.rootTag,
appParameters.initialLayoutContext,
wrapperComponentProvider && wrapperComponentProvider(appParameters),
appParameters.fabric,
false,
Expand Down
70 changes: 70 additions & 0 deletions Libraries/ReactNative/RootViewLayout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow local-strict
* @format
*/

const React = require('React');
const RCTDeviceEventEmitter = require('RCTDeviceEventEmitter');

import type {Layout} from 'CoreEventTypes';

export type LayoutContext = $ReadOnly<{|
layout: Layout,
safeAreaInsets: $ReadOnly<{|
top: number,
right: number,
bottom: number,
left: number,
|}>,
|}>;

/**
* Context used to provide layout metrics from the root view the component
* tree is being rendered in. This is useful when sync measurements are
* required.
*/
const Context: React.Context<LayoutContext> = React.createContext({
layout: {x: 0, y: 0, width: 0, height: 0},
safeAreaInsets: {top: 0, right: 0, bottom: 0, left: 0},
});

type Props = {|
children: React.Node,
janicduplessis marked this conversation as resolved.
Show resolved Hide resolved
initialLayoutContext: LayoutContext,
rootTag: number,
|};

function RootViewLayoutManager({
children,
initialLayoutContext,
rootTag,
}: Props) {
const [layoutContext, setLayoutContext] = React.useState<LayoutContext>(
initialLayoutContext,
);
React.useLayoutEffect(() => {
const subscription = RCTDeviceEventEmitter.addListener(
'didUpdateLayoutContext',
event => {
if (rootTag === event.rootTag) {
setLayoutContext(event.layoutContext);
}
},
);
return () => {
subscription.remove();
};
}, [rootTag]);

return <Context.Provider value={layoutContext}>{children}</Context.Provider>;
}

module.exports = {
Context,
Manager: RootViewLayoutManager,
};
20 changes: 14 additions & 6 deletions Libraries/ReactNative/renderApplication.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,20 @@ import type {IPerformanceLogger} from 'createPerformanceLogger';
import PerformanceLoggerContext from 'PerformanceLoggerContext';
const React = require('React');
const ReactFabricIndicator = require('ReactFabricIndicator');
const RootViewLayout = require('RootViewLayout');

const invariant = require('invariant');

import type {LayoutContext} from 'RootViewLayout';

// require BackHandler so it sets the default handler that exits the app if no listeners respond
require('BackHandler');

function renderApplication<Props: Object>(
RootComponent: React.ComponentType<Props>,
initialProps: Props,
rootTag: any,
initialLayoutContext: LayoutContext,
WrapperComponent?: ?React.ComponentType<*>,
fabric?: boolean,
showFabricIndicator?: boolean,
Expand All @@ -36,12 +40,16 @@ function renderApplication<Props: Object>(
let renderable = (
<PerformanceLoggerContext.Provider
value={scopedPerformanceLogger ?? GlobalPerformanceLogger}>
<AppContainer rootTag={rootTag} WrapperComponent={WrapperComponent}>
<RootComponent {...initialProps} rootTag={rootTag} />
{fabric === true && showFabricIndicator === true ? (
<ReactFabricIndicator />
) : null}
</AppContainer>
<RootViewLayout.Manager
initialLayoutContext={initialLayoutContext}
rootTag={rootTag}>
<AppContainer rootTag={rootTag} WrapperComponent={WrapperComponent}>
<RootComponent {...initialProps} rootTag={rootTag} />
{fabric === true && showFabricIndicator === true ? (
<ReactFabricIndicator />
) : null}
</AppContainer>
</RootViewLayout.Manager>
</PerformanceLoggerContext.Provider>
);

Expand Down
3 changes: 3 additions & 0 deletions Libraries/react-native/react-native-implementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ module.exports = {
get KeyboardAvoidingView() {
return require('KeyboardAvoidingView');
},
get LayoutContext() {
return require('RootViewLayout').Context.Consumer;
},
get MaskedViewIOS() {
warnOnce(
'maskedviewios-moved',
Expand Down
120 changes: 120 additions & 0 deletions RNTester/js/LayoutContextExample.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow
*/

'use strict';

const React = require('react');
const {
LayoutContext,
StyleSheet,
View,
Text,
StatusBar,
Button,
Platform,
} = require('react-native');

const BORDER_WIDTH = 2;

type Props = {
onExampleExit: () => void,
};

type State = {
hidden: boolean,
translucent: boolean,
};

class LayoutContextExample extends React.Component<Props, State> {
janicduplessis marked this conversation as resolved.
Show resolved Hide resolved
static title = '<LayoutContext>';
static description =
'LayoutContext allows getting layout metrics for the current root view.';
static external = true;

state = {
hidden: false,
translucent: false,
};

render() {
return (
<LayoutContext>
{ctx => {
return (
<>
<StatusBar
hidden={this.state.hidden}
translucent={this.state.translucent}
backgroundColor="rgba(0, 0, 0, 0.3)"
/>
<View
style={[
styles.container,
{
width: ctx.layout.width,
height: ctx.layout.height,
paddingTop: ctx.safeAreaInsets.top - BORDER_WIDTH,
paddingRight: ctx.safeAreaInsets.right - BORDER_WIDTH,
paddingBottom: ctx.safeAreaInsets.bottom - BORDER_WIDTH,
paddingLeft: ctx.safeAreaInsets.left - BORDER_WIDTH,
},
]}>
<View style={styles.content}>
<Text
style={{
marginBottom: 32,
backgroundColor: '#eee',
padding: 16,
}}>
{JSON.stringify(ctx, null, 2)}
</Text>
<Button
title="Toggle status bar hidden"
onPress={() =>
this.setState(state => ({hidden: !state.hidden}))
}
/>
{Platform.OS === 'android' && (
<Button
title="Toggle status bar translucent"
onPress={() =>
this.setState(state => ({
translucent: !state.translucent,
}))
}
/>
)}
<Button title="Close" onPress={this.props.onExampleExit} />
</View>
</View>
</>
);
}}
</LayoutContext>
);
}
}

const styles = StyleSheet.create({
container: {
borderWidth: BORDER_WIDTH,
borderColor: 'red',
backgroundColor: '#589c90',
},
content: {
flex: 1,
backgroundColor: 'white',
borderWidth: BORDER_WIDTH,
borderColor: 'blue',
padding: 32,
},
});

module.exports = LayoutContextExample;
5 changes: 2 additions & 3 deletions RNTester/js/RNTesterApp.android.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ class RNTesterApp extends React.Component<Props, RNTesterNavigationState> {
* found when making Flow check .android.js files. */
this.drawer = drawer;
}}
renderNavigationView={this._renderDrawerContent}
statusBarBackgroundColor="#589c90">
renderNavigationView={this._renderDrawerContent}>
<StatusBar backgroundColor="#589c90" />
{this._renderApp()}
</DrawerLayoutAndroid>
);
Expand Down Expand Up @@ -245,7 +245,6 @@ const styles = StyleSheet.create({
},
drawerContentWrapper: {
flex: 1,
paddingTop: StatusBar.currentHeight,
backgroundColor: 'white',
},
});
Expand Down
4 changes: 4 additions & 0 deletions RNTester/js/RNTesterList.android.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ const ComponentExamples: Array<RNTesterExample> = [
key: 'ImageExample',
module: require('./ImageExample'),
},
{
key: 'LayoutContextExample',
module: require('./LayoutContextExample'),
},
{
key: 'ModalExample',
module: require('./ModalExample'),
Expand Down
5 changes: 5 additions & 0 deletions RNTester/js/RNTesterList.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ const ComponentExamples: Array<RNTesterExample> = [
module: require('./KeyboardAvoidingViewExample'),
supportsTVOS: false,
},
{
key: 'LayoutContextExample',
module: require('./LayoutContextExample'),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cannot assign array literal to ComponentExamples because inexact class LayoutContextExample [1] is incompatible with exact RNTesterExampleModule [2] in property module of array element.

supportsTVOS: true,
},
{
key: 'LayoutEventsExample',
module: require('./LayoutEventsExample'),
Expand Down
16 changes: 9 additions & 7 deletions RNTester/js/Shared/RNTesterTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@ export type RNTesterExampleModuleItem = $ReadOnly<{|
render: () => React.Node,
|}>;

export type RNTesterExampleModule = $ReadOnly<{|
title: string,
description: string,
displayName?: ?string,
framework?: string,
examples: Array<RNTesterExampleModuleItem>,
|}>;
export type RNTesterExampleModule =
| $ReadOnly<{|
title: string,
description: string,
displayName?: ?string,
framework?: string,
examples: Array<RNTesterExampleModuleItem>,
|}>
| ComponentType<any>;

export type RNTesterExample = $ReadOnly<{|
key: string,
Expand Down
Loading