@@ -16,13 +16,16 @@ import type {ColorValue} from '../../StyleSheet/StyleSheet';
1616import type {
1717 GestureResponderEvent ,
1818 LayoutChangeEvent ,
19+ SafeAreaInsets ,
20+ SafeAreaInsetsChangeEvent ,
1921 ScrollEvent ,
2022} from '../../Types/CoreEventTypes' ;
2123import type { EventSubscription } from '../../vendor/emitter/EventEmitter' ;
2224import type { KeyboardEvent , KeyboardMetrics } from '../Keyboard/Keyboard' ;
2325import type { ViewProps } from '../View/ViewPropTypes' ;
2426import type { ScrollViewStickyHeaderProps } from './ScrollViewStickyHeader' ;
2527
28+ import getSafeAreaContentPadding from '../../../src/private/components/scrollview/getSafeAreaContentPadding' ;
2629import {
2730 HScrollContentViewNativeComponent ,
2831 HScrollViewNativeComponent ,
@@ -386,8 +389,13 @@ export type ScrollViewPropsIOS = Readonly<{
386389 */
387390 zoomScale ?: ?number ,
388391 /**
389- * This property specifies how the safe area insets are used to modify the
390- * content area of the scroll view.
392+ * How the safe area insets modify the content area of the scroll view.
393+ *
394+ * Along the scrollable axis, the insets become content insets, so content
395+ * scrolls under the system UI. Across it, every value except `'never'` pads
396+ * the content container by the insets, so content is laid out clear of the
397+ * system UI, such as the rail on iPhone Duo or camera cutout in landscape,
398+ * while the container's background still extends edge to edge.
391399 *
392400 * @default `'never'`
393401 * @platform ios
@@ -883,6 +891,11 @@ class ScrollView extends React.Component<ScrollViewProps, ScrollViewState> {
883891 layoutHeight : null ,
884892 } ;
885893
894+ // The latest safe area insets of the native view. Only the pair across the
895+ // scrollable axis affects the content container, so a change to the other
896+ // pair is kept here without a render.
897+ _safeAreaInsets : ?SafeAreaInsets = null ;
898+
886899 componentDidMount ( ) {
887900 this . _keyboardMetrics = Keyboard . metrics ( ) ;
888901 this . _additionalScrollOffset = 0 ;
@@ -1247,6 +1260,30 @@ class ScrollView extends React.Component<ScrollViewProps, ScrollViewState> {
12471260 this . props . onScroll && this . props . onScroll ( e ) ;
12481261 } ;
12491262
1263+ _handleSafeAreaInsetsChange = ( e : SafeAreaInsetsChangeEvent ) => {
1264+ const { insets} = e . nativeEvent ;
1265+ const previous = this . _safeAreaInsets ;
1266+ this . _safeAreaInsets = insets ;
1267+ const changedAcrossScrollableAxis =
1268+ previous == null ||
1269+ ( this . props . horizontal === true
1270+ ? previous . top !== insets . top || previous . bottom !== insets . bottom
1271+ : previous . left !== insets . left || previous . right !== insets . right ) ;
1272+ if ( changedAcrossScrollableAxis && this . _padsContentForSafeArea ( ) ) {
1273+ this . forceUpdate ( ) ;
1274+ }
1275+ this . props . experimental_onSafeAreaInsetsChange ?. ( e ) ;
1276+ } ;
1277+
1278+ /**
1279+ * Whether the content container is padded by the safe area across the
1280+ * scrollable axis.
1281+ */
1282+ _padsContentForSafeArea ( ) : boolean {
1283+ const behavior = this . props . contentInsetAdjustmentBehavior ;
1284+ return Platform . OS === 'ios' && behavior != null && behavior !== 'never' ;
1285+ }
1286+
12501287 _handleLayout = ( e : LayoutChangeEvent ) => {
12511288 if ( this . props . invertStickyHeaders === true ) {
12521289 this . setState ( { layoutHeight : e . nativeEvent . layout . height } ) ;
@@ -1747,9 +1784,25 @@ class ScrollView extends React.Component<ScrollViewProps, ScrollViewState> {
17471784 ? HScrollContentViewNativeComponent
17481785 : VScrollContentViewNativeComponent ;
17491786
1787+ // Along the scrollable axis UIKit turns the safe area into content insets,
1788+ // so content scrolls under the system UI. Across it there is nothing to
1789+ // scroll, so the content container is padded instead and content is laid
1790+ // out clear of the system UI, such as the rail on iPhone Duo. Native
1791+ // removes whatever UIKit insets across that axis, so the safe area applies
1792+ // once.
1793+ const padsContentForSafeArea = this . _padsContentForSafeArea ( ) ;
1794+ const safeAreaInsets = this . _safeAreaInsets ;
1795+
17501796 const contentContainerStyle = [
17511797 horizontal && styles . contentContainerHorizontal ,
17521798 this . props . contentContainerStyle ,
1799+ padsContentForSafeArea && safeAreaInsets != null
1800+ ? getSafeAreaContentPadding (
1801+ this . props . contentContainerStyle ,
1802+ safeAreaInsets ,
1803+ horizontal ,
1804+ )
1805+ : null ,
17531806 ] ;
17541807 if ( __DEV__ && this . props . style !== undefined ) {
17551808 // $FlowFixMe[underconstrained-implicit-instantiation]
@@ -1886,6 +1939,14 @@ class ScrollView extends React.Component<ScrollViewProps, ScrollViewState> {
18861939 onTouchStart : this . _handleTouchStart ,
18871940 onTouchCancel : this . _handleTouchCancel ,
18881941 onScroll : this . _handleScroll ,
1942+ // Routed through the handler whenever the event is observed at all, so
1943+ // the insets it keeps are current when the behavior changes from
1944+ // 'never' and the content container starts padding for them.
1945+ experimental_onSafeAreaInsetsChange :
1946+ padsContentForSafeArea ||
1947+ this . props . experimental_onSafeAreaInsetsChange != null
1948+ ? this . _handleSafeAreaInsetsChange
1949+ : undefined ,
18891950 endDraggingSensitivityMultiplier :
18901951 experimental_endDraggingSensitivityMultiplier ,
18911952 scrollEventThrottle : hasStickyHeaders
0 commit comments