Skip to content

Commit

Permalink
Add ScrollView.automaticallyAdjustsScrollIndicatorInsets prop (on iOS) (
Browse files Browse the repository at this point in the history
#29809)

Summary:
iOS 13 added a new property to `UIScrollView`: `automaticallyAdjustsScrollIndicatorInsets`, which is `YES` by default.  The property changes the meaning of the `scrollIndicatorInsets` property.  When `YES`, any such insets are **in addition to** whatever insets would be applied by the device's safe area.  When `NO`, the iOS <13 behavior is restored, which is for such insets to not account for safe area.

In other words, this effects ScrollViews that underlay the device's safe area (i.e. under the notch).  When `YES`, the OS "automatically" insets the scroll indicators, when `NO` it does not.

There are two problems with the default `YES` setting:

1. It means applying `scrollIndicatorInsets` to a `ScrollView` has a different effect on iOS 13 versus iOS 12.
2. It limits developers' control over `scrollIndicatorInsets`.  Since negative insets are not supported, if the insets the OS chooses are too large for your app, you cannot fix it.

Further explanation & sample code is available in issue #28140 .

This change sets the default for this property to `NO`, making the behavior consistent across iOS versions, and allowing developers full control.

## Changelog

<!-- Help reviewers and the release process by writing your own changelog entry. For an example, see:
https://github.com/facebook/react-native/wiki/Changelog
-->

[iOS] [Changed] - ScrollView scrollIndicatorInsets to not automatically add safe area on iOS13+

Pull Request resolved: #29809

Test Plan:
Updated the RNTester example to explain what to expect. Also removed the `pageScreen` modal example for now as mentioned in my Github comment.

{F628636466}

Here are screenshots of the demo app (from the original bug) before (with safe area applied to insets) & after (without safe area applied to insets):

![before](https://user-images.githubusercontent.com/428831/91644197-ea03a700-ea07-11ea-9489-be27820930eb.png)

![after](https://user-images.githubusercontent.com/428831/91644200-eff98800-ea07-11ea-8788-daf1e783639d.png)

Reviewed By: p-sun

Differential Revision: D28229603

Pulled By: lunaleaps

fbshipit-source-id: 2e774ae150b1dc41680b8b7886c7ceac8808136a
  • Loading branch information
justinwh authored and facebook-github-bot committed Jul 8, 2021
1 parent 4ad4426 commit bc1e602
Show file tree
Hide file tree
Showing 11 changed files with 158 additions and 0 deletions.
6 changes: 6 additions & 0 deletions Libraries/Components/ScrollView/ScrollView.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,12 @@ type IOSProps = $ReadOnly<{|
* @platform ios
*/
automaticallyAdjustContentInsets?: ?boolean,
/**
* Controls whether iOS should automatically adjust the scroll indicator
* insets. The default value is true. Available on iOS 13 and later.
* @platform ios
*/
automaticallyAdjustsScrollIndicatorInsets?: ?boolean,
/**
* The amount by which the scroll view content is inset from the edges
* of the scroll view. Defaults to `{top: 0, left: 0, bottom: 0, right: 0}`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const ScrollViewNativeComponent: HostComponent<Props> = NativeComponentRegistry.
alwaysBounceHorizontal: true,
alwaysBounceVertical: true,
automaticallyAdjustContentInsets: true,
automaticallyAdjustsScrollIndicatorInsets: true,
bounces: true,
bouncesZoom: true,
canCancelContentTouches: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type ScrollViewNativeProps = $ReadOnly<{
alwaysBounceHorizontal?: ?boolean,
alwaysBounceVertical?: ?boolean,
automaticallyAdjustContentInsets?: ?boolean,
automaticallyAdjustsScrollIndicatorInsets?: ?boolean,
bounces?: ?boolean,
bouncesZoom?: ?boolean,
canCancelContentTouches?: ?boolean,
Expand Down
1 change: 1 addition & 0 deletions Libraries/Components/ScrollView/ScrollViewViewConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const ScrollViewViewConfig = {
alwaysBounceHorizontal: true,
alwaysBounceVertical: true,
automaticallyAdjustContentInsets: true,
automaticallyAdjustsScrollIndicatorInsets: true,
bounces: true,
bouncesZoom: true,
canCancelContentTouches: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,14 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
scrollView.snapToOffsets = snapToOffsets;
}

if (@available(iOS 13.0, *)) {
if (oldScrollViewProps.automaticallyAdjustsScrollIndicatorInsets !=
newScrollViewProps.automaticallyAdjustsScrollIndicatorInsets) {
scrollView.automaticallyAdjustsScrollIndicatorInsets =
newScrollViewProps.automaticallyAdjustsScrollIndicatorInsets;
}
}

if (@available(iOS 11.0, *)) {
if (oldScrollViewProps.contentInsetAdjustmentBehavior != newScrollViewProps.contentInsetAdjustmentBehavior) {
auto const contentInsetAdjustmentBehavior = newScrollViewProps.contentInsetAdjustmentBehavior;
Expand Down
12 changes: 12 additions & 0 deletions React/Views/ScrollView/RCTScrollView.m
Original file line number Diff line number Diff line change
Expand Up @@ -928,6 +928,18 @@ -(type)getter \
RCT_SET_AND_PRESERVE_OFFSET(setZoomScale, zoomScale, CGFloat);
RCT_SET_AND_PRESERVE_OFFSET(setScrollIndicatorInsets, scrollIndicatorInsets, UIEdgeInsets);

#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 /* __IPHONE_13_0 */
- (void)setAutomaticallyAdjustsScrollIndicatorInsets:(BOOL)automaticallyAdjusts API_AVAILABLE(ios(13.0))
{
// `automaticallyAdjustsScrollIndicatorInsets` is available since iOS 13.
if ([_scrollView respondsToSelector:@selector(setAutomaticallyAdjustsScrollIndicatorInsets:)]) {
if (@available(iOS 13.0, *)) {
_scrollView.automaticallyAdjustsScrollIndicatorInsets = automaticallyAdjusts;
}
}
}
#endif

#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */
- (void)setContentInsetAdjustmentBehavior:(UIScrollViewContentInsetAdjustmentBehavior)behavior API_AVAILABLE(ios(11.0))
{
Expand Down
3 changes: 3 additions & 0 deletions React/Views/ScrollView/RCTScrollViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ - (UIView *)view
RCT_EXPORT_VIEW_PROPERTY(onMomentumScrollBegin, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onMomentumScrollEnd, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(inverted, BOOL)
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 /* __IPHONE_13_0 */
RCT_EXPORT_VIEW_PROPERTY(automaticallyAdjustsScrollIndicatorInsets, BOOL)
#endif
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */
RCT_EXPORT_VIEW_PROPERTY(contentInsetAdjustmentBehavior, UIScrollViewContentInsetAdjustmentBehavior)
#endif
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ ScrollViewProps::ScrollViewProps(
"automaticallyAdjustContentInsets",
sourceProps.automaticallyAdjustContentInsets,
{})),
automaticallyAdjustsScrollIndicatorInsets(convertRawProp(
rawProps,
"automaticallyAdjustsScrollIndicatorInsets",
sourceProps.automaticallyAdjustsScrollIndicatorInsets,
true)),
decelerationRate(convertRawProp(
rawProps,
"decelerationRate",
Expand Down Expand Up @@ -206,6 +211,10 @@ SharedDebugStringConvertibleList ScrollViewProps::getDebugProps() const {
"automaticallyAdjustContentInsets",
automaticallyAdjustContentInsets,
defaultScrollViewProps.automaticallyAdjustContentInsets),
debugStringConvertibleItem(
"automaticallyAdjustsScrollIndicatorInsets",
automaticallyAdjustsScrollIndicatorInsets,
defaultScrollViewProps.automaticallyAdjustsScrollIndicatorInsets),
debugStringConvertibleItem(
"decelerationRate",
decelerationRate,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class ScrollViewProps final : public ViewProps {
bool canCancelContentTouches{true};
bool centerContent{};
bool automaticallyAdjustContentInsets{};
bool automaticallyAdjustsScrollIndicatorInsets{true};
Float decelerationRate{0.998f};
bool directionalLockEnabled{};
ScrollViewIndicatorStyle indicatorStyle{};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/**
* 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.
*/

'use strict';

import * as React from 'react';

import {
Button,
Modal,
ScrollView,
StyleSheet,
Switch,
Text,
View,
useWindowDimensions,
} from 'react-native';

export function ScrollViewIndicatorInsetsExample() {
const [automaticallyAdjustsScrollIndicatorInsets, setAutomaticallyAdjustsScrollIndicatorInsets] = React.useState(true);
const [modalVisible, setModalVisible] = React.useState(false);
const { height, width } = useWindowDimensions();

return (
<View>
<Modal
animationType="slide"
visible={modalVisible}
onRequestClose={() => setModalVisible(false)}
presentationStyle="fullScreen"
statusBarTranslucent={false}
supportedOrientations={['portrait', 'landscape']}>
<View style={styles.modal}>
<ScrollView
contentContainerStyle={[
styles.scrollViewContent,
{
height: (height * 1.2),
width: (width * 1.2),
},
]}
automaticallyAdjustsScrollIndicatorInsets={automaticallyAdjustsScrollIndicatorInsets}
style={styles.scrollView}>
<View style={styles.description}>
<Text>When <Text style={styles.code}>automaticallyAdjustsScrollIndicatorInsets</Text> is true, the scrollbar is inset to the status bar. When false, it reaches the edge of the modal.</Text>
<Text>Check out the UIScrollView docs to learn more about <Text style={styles.code}>automaticallyAdjustsScrollIndicatorInsets</Text></Text>
</View>
<View style={styles.toggle}>
<Text><Text style={styles.code}>automaticallyAdjustsScrollIndicatorInsets</Text> is {automaticallyAdjustsScrollIndicatorInsets + ''}</Text>
<Switch
onValueChange={v => setAutomaticallyAdjustsScrollIndicatorInsets(v)}
value={automaticallyAdjustsScrollIndicatorInsets}
style={styles.switch}/>
</View>
<Button
onPress={() => setModalVisible(false)}
title="Close"/>
</ScrollView>
</View>
</Modal>
<Text />
<Button
onPress={() => setModalVisible(true)}
title="Present Fullscreen Modal with ScrollView"/>
</View>
);
}

const styles = StyleSheet.create({
modal: {
flex: 1,
},
scrollView: {
flex: 1,
height: 1000,
},
scrollViewContent: {
alignItems: 'center',
backgroundColor: '#ffaaaa',
justifyContent: 'flex-start',
paddingTop: 200,
},
switch: {
marginBottom: 40,
},
toggle:{
margin: 20,
alignItems: 'center',
},
description: {
marginHorizontal: 80,
},
code: {
fontSize: 10,
fontFamily: 'Courier',
},
});

exports.title = 'ScrollViewIndicatorInsets';
exports.category = 'iOS';
exports.description =
'ScrollView automaticallyAdjustsScrollIndicatorInsets adjusts scroll indicator insets using OS-defined logic on iOS 13+.';
exports.examples = [
{
title: '<ScrollView> automaticallyAdjustsScrollIndicatorInsets Example',
render: (): React.Node => <ScrollViewIndicatorInsetsExample/>,
},
];
4 changes: 4 additions & 0 deletions packages/rn-tester/js/utils/RNTesterList.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ const Components: Array<RNTesterModuleInfo> = [
module: require('../examples/ScrollView/ScrollViewAnimatedExample'),
supportsTVOS: true,
},
{
key: 'ScrollViewIndicatorInsetsExample',
module: require('../examples/ScrollView/ScrollViewIndicatorInsetsExample'),
},
{
key: 'SectionListExample',
module: require('../examples/SectionList/SectionListIndex'),
Expand Down

0 comments on commit bc1e602

Please sign in to comment.