mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
feat: implement automicallyAdjustsKeyboardInsets for new architecture on iOS (#45939)
Summary: This PR implements the missing `automicallyAdjustsKeyboardInsets` for new architecture. It's a fixed version of reverted: https://github.com/facebook/react-native/issues/45819 We now check if the view intersects with the keyboard's end frame and if it doesn't we just do nothing. Here is the app running on new arch: https://github.com/user-attachments/assets/673f0587-6a67-47e3-8050-d6ee33a45724 ## Changelog: [IOS] [FIXED] - implement automicallyAdjustsKeyboardInsets for new arch Pull Request resolved: https://github.com/facebook/react-native/pull/45939 Test Plan: 1. Test out ScrollViewKeyboardInsets example 2. See if it works the same with old and new arch Reviewed By: cortinico Differential Revision: D60958475 Pulled By: cipolleschi fbshipit-source-id: 8650064af84bc79b6b89e07293640e5d010154c2
This commit is contained in:
committed by
Facebook GitHub Bot
parent
ee25081d20
commit
e4e461c9cf
+9
@@ -35,6 +35,9 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
*/
|
||||
@property (nonatomic, strong, readonly) UIScrollView *scrollView;
|
||||
|
||||
/** Focus area of newly-activated text input relative to the window to compare against UIKeyboardFrameBegin/End */
|
||||
@property (nonatomic, assign) CGRect firstResponderFocus;
|
||||
|
||||
/*
|
||||
* Returns the subview of the scroll view that the component uses to mount all subcomponents into. That's useful to
|
||||
* separate component views from auxiliary views to be able to reliably implement pull-to-refresh- and RTL-related
|
||||
@@ -59,4 +62,10 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@end
|
||||
|
||||
@interface UIView (RCTScrollViewComponentView)
|
||||
|
||||
- (void)reactUpdateResponderOffsetForScrollView:(RCTScrollViewComponentView *)scrollView;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
+120
@@ -98,6 +98,7 @@ RCTSendScrollEventForNativeAnimations_DEPRECATED(UIScrollView *scrollView, NSInt
|
||||
// some other part of the system scrolls scroll view.
|
||||
BOOL _isUserTriggeredScrolling;
|
||||
BOOL _shouldUpdateContentInsetAdjustmentBehavior;
|
||||
BOOL _automaticallyAdjustKeyboardInsets;
|
||||
|
||||
CGPoint _contentOffsetWhenClipped;
|
||||
|
||||
@@ -128,12 +129,16 @@ RCTSendScrollEventForNativeAnimations_DEPRECATED(UIScrollView *scrollView, NSInt
|
||||
((RCTEnhancedScrollView *)_scrollView).overridingDelegate = self;
|
||||
_isUserTriggeredScrolling = NO;
|
||||
_shouldUpdateContentInsetAdjustmentBehavior = YES;
|
||||
_automaticallyAdjustKeyboardInsets = NO;
|
||||
[self addSubview:_scrollView];
|
||||
|
||||
_containerView = [[UIView alloc] initWithFrame:CGRectZero];
|
||||
[_scrollView addSubview:_containerView];
|
||||
|
||||
[self.scrollViewDelegateSplitter addDelegate:self];
|
||||
#if TARGET_OS_IOS
|
||||
[self _registerKeyboardListener];
|
||||
#endif
|
||||
|
||||
_scrollEventThrottle = 0;
|
||||
_endDraggingSensitivityMultiplier = 1;
|
||||
@@ -149,6 +154,111 @@ RCTSendScrollEventForNativeAnimations_DEPRECATED(UIScrollView *scrollView, NSInt
|
||||
[self.scrollViewDelegateSplitter removeAllDelegates];
|
||||
}
|
||||
|
||||
#if TARGET_OS_IOS
|
||||
- (void)_registerKeyboardListener
|
||||
{
|
||||
// According to Apple docs, we don't need to explicitly unregister the observer, it's done automatically.
|
||||
// See the Apple documentation:
|
||||
// https://developer.apple.com/documentation/foundation/nsnotificationcenter/1413994-removeobserver?language=objc
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(_keyboardWillChangeFrame:)
|
||||
name:UIKeyboardWillChangeFrameNotification
|
||||
object:nil];
|
||||
}
|
||||
|
||||
- (void)_keyboardWillChangeFrame:(NSNotification *)notification
|
||||
{
|
||||
if (!_automaticallyAdjustKeyboardInsets) {
|
||||
return;
|
||||
}
|
||||
BOOL isHorizontal = _scrollView.contentSize.width > self.frame.size.width;
|
||||
if (isHorizontal) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool isInverted = [self isInverted];
|
||||
double duration = [notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
|
||||
|
||||
UIViewAnimationCurve curve =
|
||||
(UIViewAnimationCurve)[notification.userInfo[UIKeyboardAnimationCurveUserInfoKey] unsignedIntegerValue];
|
||||
CGRect keyboardEndFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
|
||||
|
||||
CGPoint absoluteViewOrigin = [self convertPoint:self.bounds.origin toView:nil];
|
||||
CGFloat scrollViewLowerY = isInverted ? absoluteViewOrigin.y : absoluteViewOrigin.y + self.bounds.size.height;
|
||||
|
||||
UIEdgeInsets newEdgeInsets = _scrollView.contentInset;
|
||||
CGFloat inset = MAX(scrollViewLowerY - keyboardEndFrame.origin.y, 0);
|
||||
if (isInverted) {
|
||||
newEdgeInsets.top = MAX(inset, _scrollView.contentInset.top);
|
||||
} else {
|
||||
newEdgeInsets.bottom = MAX(inset, _scrollView.contentInset.bottom);
|
||||
}
|
||||
|
||||
CGPoint newContentOffset = _scrollView.contentOffset;
|
||||
self.firstResponderFocus = CGRectNull;
|
||||
|
||||
CGFloat contentDiff = 0;
|
||||
if ([[UIApplication sharedApplication] sendAction:@selector(reactUpdateResponderOffsetForScrollView:)
|
||||
to:nil
|
||||
from:self
|
||||
forEvent:nil]) {
|
||||
if (CGRectEqualToRect(_firstResponderFocus, CGRectNull)) {
|
||||
// Text input view is outside of the scroll view.
|
||||
return;
|
||||
}
|
||||
|
||||
CGRect viewIntersection = CGRectIntersection(self.firstResponderFocus, keyboardEndFrame);
|
||||
|
||||
if (CGRectIsNull(viewIntersection)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Inner text field focused
|
||||
CGFloat focusEnd = CGRectGetMaxY(self.firstResponderFocus);
|
||||
if (focusEnd > keyboardEndFrame.origin.y) {
|
||||
// Text field active region is below visible area with keyboard - update diff to bring into view
|
||||
contentDiff = keyboardEndFrame.origin.y - focusEnd;
|
||||
}
|
||||
}
|
||||
|
||||
if (isInverted) {
|
||||
newContentOffset.y += contentDiff;
|
||||
} else {
|
||||
newContentOffset.y -= contentDiff;
|
||||
}
|
||||
|
||||
if (@available(iOS 14.0, *)) {
|
||||
// On iOS when Prefer Cross-Fade Transitions is enabled, the keyboard position
|
||||
// & height is reported differently (0 instead of Y position value matching height of frame)
|
||||
// Fixes similar issue we saw with https://github.com/facebook/react-native/pull/34503
|
||||
if (UIAccessibilityPrefersCrossFadeTransitions() && keyboardEndFrame.size.height == 0) {
|
||||
newContentOffset.y = 0;
|
||||
newEdgeInsets.bottom = 0;
|
||||
}
|
||||
}
|
||||
|
||||
[UIView animateWithDuration:duration
|
||||
delay:0.0
|
||||
options:animationOptionsWithCurve(curve)
|
||||
animations:^{
|
||||
self->_scrollView.contentInset = newEdgeInsets;
|
||||
self->_scrollView.verticalScrollIndicatorInsets = newEdgeInsets;
|
||||
[self scrollToOffset:newContentOffset animated:NO];
|
||||
}
|
||||
completion: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;
|
||||
}
|
||||
#endif
|
||||
|
||||
- (RCTGenericDelegateSplitter<id<UIScrollViewDelegate>> *)scrollViewDelegateSplitter
|
||||
{
|
||||
return ((RCTEnhancedScrollView *)_scrollView).delegateSplitter;
|
||||
@@ -190,6 +300,12 @@ RCTSendScrollEventForNativeAnimations_DEPRECATED(UIScrollView *scrollView, NSInt
|
||||
}
|
||||
}
|
||||
|
||||
- (bool)isInverted
|
||||
{
|
||||
// Look into the entry at position 2,2 to check if scaleY is applied
|
||||
return self.layer.transform.m22 == -1;
|
||||
}
|
||||
|
||||
- (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps
|
||||
{
|
||||
const auto &oldScrollViewProps = static_cast<const ScrollViewProps &>(*_props);
|
||||
@@ -225,6 +341,10 @@ RCTSendScrollEventForNativeAnimations_DEPRECATED(UIScrollView *scrollView, NSInt
|
||||
MAP_SCROLL_VIEW_PROP(showsHorizontalScrollIndicator);
|
||||
MAP_SCROLL_VIEW_PROP(showsVerticalScrollIndicator);
|
||||
|
||||
if (oldScrollViewProps.automaticallyAdjustKeyboardInsets != newScrollViewProps.automaticallyAdjustKeyboardInsets) {
|
||||
_automaticallyAdjustKeyboardInsets = newScrollViewProps.automaticallyAdjustKeyboardInsets;
|
||||
}
|
||||
|
||||
if (oldScrollViewProps.scrollIndicatorInsets != newScrollViewProps.scrollIndicatorInsets) {
|
||||
_scrollView.scrollIndicatorInsets = RCTUIEdgeInsetsFromEdgeInsets(newScrollViewProps.scrollIndicatorInsets);
|
||||
}
|
||||
|
||||
+28
@@ -12,6 +12,7 @@
|
||||
#import <react/renderer/textlayoutmanager/TextLayoutManager.h>
|
||||
|
||||
#import <React/RCTBackedTextInputViewProtocol.h>
|
||||
#import <React/RCTScrollViewComponentView.h>
|
||||
#import <React/RCTUITextField.h>
|
||||
#import <React/RCTUITextView.h>
|
||||
#import <React/RCTUtils.h>
|
||||
@@ -22,6 +23,9 @@
|
||||
|
||||
#import "RCTFabricComponentsPlugins.h"
|
||||
|
||||
/** Native iOS text field bottom keyboard offset amount */
|
||||
static const CGFloat kSingleLineKeyboardBottomOffset = 15.0;
|
||||
|
||||
using namespace facebook::react;
|
||||
|
||||
@interface RCTTextInputComponentView () <RCTBackedTextInputDelegate, RCTTextInputViewProtocol>
|
||||
@@ -96,6 +100,30 @@ static NSSet<NSNumber *> *returnKeyTypesSet;
|
||||
[self _restoreTextSelection];
|
||||
}
|
||||
|
||||
- (void)reactUpdateResponderOffsetForScrollView:(RCTScrollViewComponentView *)scrollView
|
||||
{
|
||||
if (![self isDescendantOfView:scrollView.scrollView] || !_backedTextInputView.isFirstResponder) {
|
||||
// View is outside scroll view or it's not a first responder.
|
||||
return;
|
||||
}
|
||||
|
||||
UITextRange *selectedTextRange = _backedTextInputView.selectedTextRange;
|
||||
UITextSelectionRect *selection = [_backedTextInputView selectionRectsForRange:selectedTextRange].firstObject;
|
||||
CGRect focusRect;
|
||||
if (selection == nil) {
|
||||
// No active selection or caret - fallback to entire input frame
|
||||
focusRect = self.bounds;
|
||||
} else {
|
||||
// Focus on text selection frame
|
||||
focusRect = selection.rect;
|
||||
BOOL isMultiline = [_backedTextInputView isKindOfClass:[UITextView class]];
|
||||
if (!isMultiline) {
|
||||
focusRect.size.height += kSingleLineKeyboardBottomOffset;
|
||||
}
|
||||
}
|
||||
scrollView.firstResponderFocus = [self convertRect:focusRect toView:nil];
|
||||
}
|
||||
|
||||
#pragma mark - RCTViewComponentView overrides
|
||||
|
||||
- (NSObject *)accessibilityElement
|
||||
|
||||
+9
@@ -91,6 +91,15 @@ ScrollViewProps::ScrollViewProps(
|
||||
"automaticallyAdjustsScrollIndicatorInsets",
|
||||
sourceProps.automaticallyAdjustsScrollIndicatorInsets,
|
||||
true)),
|
||||
automaticallyAdjustKeyboardInsets(
|
||||
CoreFeatures::enablePropIteratorSetter
|
||||
? sourceProps.automaticallyAdjustKeyboardInsets
|
||||
: convertRawProp(
|
||||
context,
|
||||
rawProps,
|
||||
"automaticallyAdjustKeyboardInsets",
|
||||
sourceProps.automaticallyAdjustKeyboardInsets,
|
||||
false)),
|
||||
decelerationRate(
|
||||
CoreFeatures::enablePropIteratorSetter
|
||||
? sourceProps.decelerationRate
|
||||
|
||||
+1
@@ -40,6 +40,7 @@ class ScrollViewProps final : public ViewProps {
|
||||
bool centerContent{};
|
||||
bool automaticallyAdjustContentInsets{};
|
||||
bool automaticallyAdjustsScrollIndicatorInsets{true};
|
||||
bool automaticallyAdjustKeyboardInsets{false};
|
||||
Float decelerationRate{0.998f};
|
||||
Float endDraggingSensitivityMultiplier{1};
|
||||
bool enableSyncOnScroll{false};
|
||||
|
||||
Reference in New Issue
Block a user