* Copyright (c) 2024 Huawei Technologies Co., Ltd.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type { TurboModule } from '../../TurboModule/RCTExport';
import * as TurboModuleRegistry from '../../TurboModule/TurboModuleRegistry';
import { ViewProps } from '../View/ViewPropTypes';
import Dimensions from '../../Utilities/Dimensions';
import View from '../View/View';
import { useEffect, useLayoutEffect, useState, useCallback } from 'react';
import React from 'react';
import RCTDeviceEventEmitter from '../../EventEmitter/RCTDeviceEventEmitter.js';
type SafeAreaInsets = {
top: number;
left: number;
right: number;
bottom: number;
};
interface SafeAreaTurboModuleProtocol {
getInitialInsets(): SafeAreaInsets;
}
interface Spec extends TurboModule, SafeAreaTurboModuleProtocol {}
const safeAreaTurboModule = TurboModuleRegistry.get<Spec>(
'SafeAreaTurboModule'
)!;
let cachedInitialInsets: SafeAreaInsets | null = null;
const getCachedInitialInsets = (): SafeAreaInsets => {
if (!cachedInitialInsets) {
cachedInitialInsets = safeAreaTurboModule.getInitialInsets();
}
return cachedInitialInsets;
};
const getPaddingTop = (inset: number, pageY: number) => {
return Math.max(0, inset - (pageY < 0 ? pageY * -1 : pageY));
};
const getPaddingBottom = (
insetBottom: number,
insetTop: number,
paddingTop: number,
height: number,
windowHeight: number,
pageY: number,
positionY: number
): number => {
if (height === 0 || (pageY === 0 && height < windowHeight)) {
return Math.max(
0,
insetBottom - (Math.round(windowHeight) - Math.round(height))
);
}
if (pageY < windowHeight && pageY > height && positionY === 0) {
return 0;
}
if (Math.round(height) >= Math.round(windowHeight) && pageY === 0) {
return positionY === 0 ? insetBottom : 0;
}
if (height < windowHeight && positionY === 0 && pageY <= insetTop) {
return Math.max(0, insetBottom - (windowHeight - (height + pageY)));
}
if (
height < windowHeight &&
pageY < windowHeight &&
pageY > 0 &&
positionY > 0
) {
return Math.max(0, insetBottom - (windowHeight - pageY));
}
if (
height < windowHeight &&
pageY > 0 &&
pageY > windowHeight &&
positionY >= 0
) {
return 0;
}
if (height < windowHeight && pageY > 0 && pageY > windowHeight) {
return insetBottom;
}
return Math.max(0, insetBottom - (windowHeight - height + paddingTop));
};
export default React.forwardRef<View, ViewProps>(
({ children, style, ...otherProps }, ref) => {
const safeAreaViewRef = React.useRef<View>(null);
const mergedRef = useCallback(
(node: View | null) => {
safeAreaViewRef.current = node;
if (typeof ref === 'function') {
ref(node);
} else if (ref) {
ref.current = node;
}
},
[ref]
);
const [initialInsets] = useState(() => getCachedInitialInsets());
const [topInset, setTopInset] = useState(initialInsets.top);
const [leftInset, setLeftInset] = useState(initialInsets.left);
const [rightInset, setRightInset] = useState(initialInsets.right);
const [bottomInset, setBottomInset] = useState(initialInsets.bottom);
const [measurement, setMeasurement] = useState({
x: 0,
y: 0,
width: 0,
height: 0,
pageX: 0,
pageY: -1,
});
const [layout, setLayout] = useState({ x: 0, y: 0, width: 0, height: 0 });
const measureView = () => {
safeAreaViewRef?.current?.measure((x, y, width, height, pageX, pageY) => {
setMeasurement({ x, y, width, height, pageX, pageY });
});
};
useEffect(
function subscribeToSafeAreaChanges() {
const subscription = (RCTDeviceEventEmitter as any).addListener(
'SAFE_AREA_INSETS_CHANGE',
(insets: SafeAreaInsets) => {
cachedInitialInsets = insets;
setTopInset(insets.top);
setBottomInset(insets.bottom);
setLeftInset(insets.left);
setRightInset(insets.right);
measureView();
}
);
return () => {
subscription.remove();
};
},
[]
);
useLayoutEffect(() => {
measureView();
}, []);
const isPaddingBottomExplicit = style?.paddingBottom !== undefined;
const isPaddingTopExplicit = style?.paddingTop !== undefined;
const isPaddingLeftExplicit = style?.paddingLeft !== undefined;
const isPaddingRightExplicit = style?.paddingRight !== undefined;
const windowHeight = Dimensions.get('window').height;
const paddingTop = getPaddingTop(topInset, measurement.pageY);
const paddingBottom = getPaddingBottom(
bottomInset,
topInset,
paddingTop,
measurement.height,
windowHeight,
measurement.pageY,
layout.y
);
return (
<View
ref={mergedRef}
style={[
style,
{
paddingTop: isPaddingTopExplicit ? style.paddingBottom : paddingTop,
paddingLeft: isPaddingLeftExplicit ? style.paddingLeft : leftInset,
paddingRight: isPaddingRightExplicit
? style.paddingRight
: rightInset,
paddingBottom: isPaddingBottomExplicit
? style.paddingBottom
: paddingBottom,
},
]}
onLayout={(event) => {
setLayout(event.nativeEvent.layout);
otherProps?.onLayout && otherProps.onLayout(event);
}}
{...otherProps}
>
{children}
</View>
);
}
);