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

Feature: ScrollView automaticallyAdjustKeyboardInsets #31402

Closed
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 the ScrollView should automatically adjust it's contentInset
* and scrollViewInsets when the Keyboard changes it's size. The default value is false.
* @platform ios
*/
automaticallyAdjustKeyboardInsets?: ?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 @@ -21,6 +21,7 @@ export type ScrollViewNativeProps = $ReadOnly<{
alwaysBounceHorizontal?: ?boolean,
alwaysBounceVertical?: ?boolean,
automaticallyAdjustContentInsets?: ?boolean,
automaticallyAdjustKeyboardInsets?: ?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,
automaticallyAdjustKeyboardInsets: false,
mrousavy marked this conversation as resolved.
Show resolved Hide resolved
bounces: true,
bouncesZoom: true,
canCancelContentTouches: true,
Expand Down
1 change: 1 addition & 0 deletions React/Views/ScrollView/RCTScrollView.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@

@property (nonatomic, assign) UIEdgeInsets contentInset;
@property (nonatomic, assign) BOOL automaticallyAdjustContentInsets;
@property (nonatomic, assign) BOOL automaticallyAdjustKeyboardInsets;
@property (nonatomic, assign) BOOL DEPRECATED_sendUpdatedChildFrames;
@property (nonatomic, assign) NSTimeInterval scrollEventThrottle;
@property (nonatomic, assign) BOOL centerContent;
Expand Down
74 changes: 71 additions & 3 deletions React/Views/ScrollView/RCTScrollView.m
Original file line number Diff line number Diff line change
Expand Up @@ -275,11 +275,74 @@ @implementation RCTScrollView {
NSHashTable *_scrollListeners;
}

- (void)registerKeyboardListener
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardWillChangeFrame:)
name:UIKeyboardWillChangeFrameNotification
object:nil];
}

- (void)unregisterKeyboardListener
{
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UIKeyboardWillChangeFrameNotification
object:nil];
}

static inline UIViewAnimationOptions animationOptionsWithCurve(UIViewAnimationCurve curve)
{
// UIViewAnimationCurve #7 is used for keyboard and therefore private - so we can't use switch/case here.
// source: https://stackoverflow.com/a/7327374/5281431
RCTAssert(UIViewAnimationCurveLinear << 16 == UIViewAnimationOptionCurveLinear, @"Unexpected implementation of UIViewAnimationCurve");
return curve << 16;
}

- (void)keyboardWillChangeFrame:(NSNotification*)notification
{
if (![self automaticallyAdjustKeyboardInsets]) {
return;
}
if ([self isHorizontal:_scrollView]) {
return;
}

double duration = [notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
UIViewAnimationCurve curve = (UIViewAnimationCurve)[notification.userInfo[UIKeyboardAnimationCurveUserInfoKey] unsignedIntegerValue];
CGRect beginFrame = [notification.userInfo[UIKeyboardFrameBeginUserInfoKey] CGRectValue];
CGRect endFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];

UIEdgeInsets newEdgeInsets = _scrollView.contentInset;
CGFloat inset = endFrame.size.height;
if (self.inverted) {
newEdgeInsets.top = inset;
} else {
newEdgeInsets.bottom = inset;
}
CGPoint newContentOffset = _scrollView.contentOffset;
CGFloat contentDiff = endFrame.origin.y - beginFrame.origin.y;
if (self.inverted) {
newContentOffset.y += contentDiff;
} else {
newContentOffset.y -= contentDiff;
}

[UIView animateWithDuration:duration
delay:0.0
options:animationOptionsWithCurve(curve)
animations:^{
self->_scrollView.contentInset = newEdgeInsets;
self->_scrollView.scrollIndicatorInsets = newEdgeInsets;
[self scrollToOffset:newContentOffset animated:NO];
} completion:nil];
}

- (instancetype)initWithEventDispatcher:(id<RCTEventDispatcherProtocol>)eventDispatcher
{
RCTAssertParam(eventDispatcher);

if ((self = [super initWithFrame:CGRectZero])) {
[self registerKeyboardListener];
_eventDispatcher = eventDispatcher;

_scrollView = [[RCTCustomScrollView alloc] initWithFrame:CGRectZero];
Expand Down Expand Up @@ -404,6 +467,7 @@ - (void)dealloc
{
_scrollView.delegate = nil;
[_eventDispatcher.bridge.uiManager.observerCoordinator removeObserver:self];
[self unregisterKeyboardListener];
}

- (void)layoutSubviews
Expand Down Expand Up @@ -848,8 +912,10 @@ - (void)uiManagerWillPerformMounting:(RCTUIManager *)manager
// Find the first entirely visible view. This must be done after we update the content offset
// or it will tend to grab rows that were made visible by the shift in position
UIView *subview = self->_contentView.subviews[ii];
CGFloat bottomInset = self.inverted ? self->_scrollView.contentInset.top : self->_scrollView.contentInset.bottom;
CGFloat y = self->_scrollView.contentOffset.y + bottomInset;
if ((horz ? subview.frame.origin.x >= self->_scrollView.contentOffset.x
: subview.frame.origin.y >= self->_scrollView.contentOffset.y) ||
: subview.frame.origin.y >= y) ||
ii == self->_contentView.subviews.count - 1) {
self->_prevFirstVisibleFrame = subview.frame;
self->_firstVisibleView = subview;
Expand Down Expand Up @@ -879,12 +945,14 @@ - (void)uiManagerWillPerformMounting:(RCTUIManager *)manager
CGRect newFrame = self->_firstVisibleView.frame;
CGFloat deltaY = newFrame.origin.y - self->_prevFirstVisibleFrame.origin.y;
if (ABS(deltaY) > 0.1) {
CGFloat bottomInset = self.inverted ? self->_scrollView.contentInset.top : self->_scrollView.contentInset.bottom;
CGFloat y = self->_scrollView.contentOffset.y + bottomInset;
self->_scrollView.contentOffset =
CGPointMake(self->_scrollView.contentOffset.x, self->_scrollView.contentOffset.y + deltaY);
if (autoscrollThreshold != nil) {
// If the offset WAS within the threshold of the start, animate to the start.
if (self->_scrollView.contentOffset.y - deltaY <= [autoscrollThreshold integerValue]) {
[self scrollToOffset:CGPointMake(self->_scrollView.contentOffset.x, 0) animated:YES];
if (y - deltaY <= [autoscrollThreshold integerValue]) {
[self scrollToOffset:CGPointMake(self->_scrollView.contentOffset.x, -bottomInset) animated:YES];
}
}
}
Expand Down
1 change: 1 addition & 0 deletions React/Views/ScrollView/RCTScrollViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ - (UIView *)view
RCT_EXPORT_VIEW_PROPERTY(centerContent, BOOL)
RCT_EXPORT_VIEW_PROPERTY(maintainVisibleContentPosition, NSDictionary)
RCT_EXPORT_VIEW_PROPERTY(automaticallyAdjustContentInsets, BOOL)
RCT_EXPORT_VIEW_PROPERTY(automaticallyAdjustKeyboardInsets, BOOL)
RCT_EXPORT_VIEW_PROPERTY(decelerationRate, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(directionalLockEnabled, BOOL)
RCT_EXPORT_VIEW_PROPERTY(indicatorStyle, UIScrollViewIndicatorStyle)
Expand Down