@@ -16,13 +16,18 @@ 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 , {
29+ getSafeAreaContentInset ,
30+ } from '../../../src/private/components/scrollview/getSafeAreaContentPadding' ;
2631import {
2732 HScrollContentViewNativeComponent ,
2833 HScrollViewNativeComponent ,
@@ -386,8 +391,13 @@ export type ScrollViewPropsIOS = Readonly<{
386391 */
387392 zoomScale ?: ?number ,
388393 /**
389- * This property specifies how the safe area insets are used to modify the
390- * content area of the scroll view.
394+ * How the safe area insets modify the content area of the scroll view.
395+ *
396+ * Along the scrollable axis, the insets become content insets, so content
397+ * scrolls under the system UI. Across it, every value except `'never'` pads
398+ * the content container by the insets, so content is laid out clear of the
399+ * system UI, such as the rail on iPhone Duo or camera cutout in landscape,
400+ * while the container's background still extends edge to edge.
391401 *
392402 * @default `'never'`
393403 * @platform ios
@@ -883,6 +893,11 @@ class ScrollView extends React.Component<ScrollViewProps, ScrollViewState> {
883893 layoutHeight : null ,
884894 } ;
885895
896+ // The latest safe area insets of the native view. Only the pair across the
897+ // scrollable axis affects the content container, so a change to the other
898+ // pair is kept here without a render.
899+ _safeAreaInsets : ?SafeAreaInsets = null ;
900+
886901 componentDidMount ( ) {
887902 this . _keyboardMetrics = Keyboard . metrics ( ) ;
888903 this . _additionalScrollOffset = 0 ;
@@ -1247,6 +1262,21 @@ class ScrollView extends React.Component<ScrollViewProps, ScrollViewState> {
12471262 this . props . onScroll && this . props . onScroll ( e ) ;
12481263 } ;
12491264
1265+ _handleSafeAreaInsetsChange = ( e : SafeAreaInsetsChangeEvent ) => {
1266+ const { insets} = e . nativeEvent ;
1267+ const previous = this . _safeAreaInsets ;
1268+ this . _safeAreaInsets = insets ;
1269+ const changedAcrossScrollableAxis =
1270+ previous == null ||
1271+ ( this . props . horizontal === true
1272+ ? previous . top !== insets . top || previous . bottom !== insets . bottom
1273+ : previous . left !== insets . left || previous . right !== insets . right ) ;
1274+ if ( changedAcrossScrollableAxis ) {
1275+ this . forceUpdate ( ) ;
1276+ }
1277+ this . props . experimental_onSafeAreaInsetsChange ?. ( e ) ;
1278+ } ;
1279+
12501280 _handleLayout = ( e : LayoutChangeEvent ) => {
12511281 if ( this . props . invertStickyHeaders === true ) {
12521282 this . setState ( { layoutHeight : e . nativeEvent . layout . height } ) ;
@@ -1747,9 +1777,55 @@ class ScrollView extends React.Component<ScrollViewProps, ScrollViewState> {
17471777 ? HScrollContentViewNativeComponent
17481778 : VScrollContentViewNativeComponent ;
17491779
1780+ const alwaysBounceHorizontal =
1781+ this . props . alwaysBounceHorizontal !== undefined
1782+ ? this . props . alwaysBounceHorizontal
1783+ : this . props . horizontal ;
1784+
1785+ const alwaysBounceVertical =
1786+ this . props . alwaysBounceVertical !== undefined
1787+ ? this . props . alwaysBounceVertical
1788+ : ! this . props . horizontal ;
1789+
1790+ // Along the scrollable axis UIKit turns the safe area into content insets,
1791+ // so content scrolls under the system UI. Across it there is nothing to
1792+ // scroll, so the content container is padded instead and content is laid
1793+ // out clear of the system UI, such as the rail on iPhone Duo. UIKit insets
1794+ // that axis as well under 'always', and whenever the axis always bounces,
1795+ // which the contentInset passed to native cancels out. Native ignores that
1796+ // contentInset while it centers the content itself, so the safe area is
1797+ // left to UIKit there.
1798+ const behavior = this . props . contentInsetAdjustmentBehavior ;
1799+ const cancelsInsetAcrossScrollableAxis =
1800+ behavior === 'always' ||
1801+ ( horizontal ? alwaysBounceVertical : alwaysBounceHorizontal ) === true ;
1802+ const padsContentForSafeArea =
1803+ Platform . OS === 'ios' &&
1804+ behavior != null &&
1805+ behavior !== 'never' &&
1806+ ! ( cancelsInsetAcrossScrollableAxis && this . props . centerContent === true ) ;
1807+ const safeAreaInsets = this . _safeAreaInsets ;
1808+ const contentInset =
1809+ padsContentForSafeArea &&
1810+ safeAreaInsets != null &&
1811+ cancelsInsetAcrossScrollableAxis
1812+ ? getSafeAreaContentInset (
1813+ this . props . contentInset ,
1814+ safeAreaInsets ,
1815+ horizontal ,
1816+ )
1817+ : this . props . contentInset ;
1818+
17501819 const contentContainerStyle = [
17511820 horizontal && styles . contentContainerHorizontal ,
17521821 this . props . contentContainerStyle ,
1822+ padsContentForSafeArea && safeAreaInsets != null
1823+ ? getSafeAreaContentPadding (
1824+ this . props . contentContainerStyle ,
1825+ safeAreaInsets ,
1826+ horizontal ,
1827+ )
1828+ : null ,
17531829 ] ;
17541830 if ( __DEV__ && this . props . style !== undefined ) {
17551831 // $FlowFixMe[underconstrained-implicit-instantiation]
@@ -1846,16 +1922,6 @@ class ScrollView extends React.Component<ScrollViewProps, ScrollViewState> {
18461922 </ NativeScrollContentView >
18471923 ) ;
18481924
1849- const alwaysBounceHorizontal =
1850- this . props . alwaysBounceHorizontal !== undefined
1851- ? this . props . alwaysBounceHorizontal
1852- : this . props . horizontal ;
1853-
1854- const alwaysBounceVertical =
1855- this . props . alwaysBounceVertical !== undefined
1856- ? this . props . alwaysBounceVertical
1857- : ! this . props . horizontal ;
1858-
18591925 const baseStyle = horizontal ? styles . baseHorizontal : styles . baseVertical ;
18601926
18611927 const { experimental_endDraggingSensitivityMultiplier, ...otherProps } =
@@ -1864,6 +1930,7 @@ class ScrollView extends React.Component<ScrollViewProps, ScrollViewState> {
18641930 ...otherProps ,
18651931 alwaysBounceHorizontal,
18661932 alwaysBounceVertical,
1933+ contentInset,
18671934 style : StyleSheet . compose ( baseStyle , this . props . style ) ,
18681935 // Override the onContentSizeChange from props, since this event can
18691936 // bubble up from TextInputs
@@ -1886,6 +1953,14 @@ class ScrollView extends React.Component<ScrollViewProps, ScrollViewState> {
18861953 onTouchStart : this . _handleTouchStart ,
18871954 onTouchCancel : this . _handleTouchCancel ,
18881955 onScroll : this . _handleScroll ,
1956+ // Routed through the handler whenever the event is observed at all, so
1957+ // the insets it keeps are current when the behavior changes from
1958+ // 'never' and the content container starts padding for them.
1959+ experimental_onSafeAreaInsetsChange :
1960+ padsContentForSafeArea ||
1961+ this . props . experimental_onSafeAreaInsetsChange != null
1962+ ? this . _handleSafeAreaInsetsChange
1963+ : undefined ,
18891964 endDraggingSensitivityMultiplier :
18901965 experimental_endDraggingSensitivityMultiplier ,
18911966 scrollEventThrottle : hasStickyHeaders
0 commit comments