diff --git a/packages/react-native/Libraries/Components/Keyboard/KeyboardAvoidingView.js b/packages/react-native/Libraries/Components/Keyboard/KeyboardAvoidingView.js index d2fff116cc32..0f848c61be6d 100644 --- a/packages/react-native/Libraries/Components/Keyboard/KeyboardAvoidingView.js +++ b/packages/react-native/Libraries/Components/Keyboard/KeyboardAvoidingView.js @@ -16,9 +16,13 @@ import type { } from '../View/ViewPropTypes'; import type {KeyboardEvent, KeyboardMetrics} from './Keyboard'; +import KeyboardAvoidingViewNativeComponent from '../../../src/private/components/keyboardavoidingview/specs/KeyboardAvoidingViewNativeComponent'; +import * as ReactNativeFeatureFlags from '../../../src/private/featureflags/ReactNativeFeatureFlags'; import LayoutAnimation from '../../LayoutAnimation/LayoutAnimation'; import StyleSheet from '../../StyleSheet/StyleSheet'; +import NativeDeviceInfo from '../../Utilities/NativeDeviceInfo'; import Platform from '../../Utilities/Platform'; +import warnOnce from '../../Utilities/warnOnce'; import {type EventSubscription} from '../../vendor/emitter/EventEmitter'; import AccessibilityInfo from '../AccessibilityInfo/AccessibilityInfo'; import View from '../View/View'; @@ -44,6 +48,15 @@ type KeyboardAvoidingViewPropsCore = Readonly<{ */ enabled?: ?boolean, + /** + * Follow the keyboard animation natively, frame by frame. + * Requires the `useSharedAnimatedBackend` feature flag and an edge-to-edge + * app; otherwise, this prop has no effect. + * + * @default `false` + */ + experimental_native?: ?boolean, + /** * Distance between the top of the user screen and the React Native view, * may be non-zero in some use cases. @@ -63,6 +76,46 @@ type KeyboardAvoidingViewState = { bottom: number, }; +function getIsNativeSupported(): boolean { + // eslint-disable-next-line react-hooks/rules-of-hooks + if (!ReactNativeFeatureFlags.useSharedAnimatedBackend()) { + if (__DEV__) { + warnOnce( + 'KeyboardAvoidingView-experimental_native-flag', + 'KeyboardAvoidingView: `experimental_native` has no effect because ' + + 'the `useSharedAnimatedBackend` feature flag is disabled.', + ); + } + return false; + } + if ( + Platform.OS === 'android' && + NativeDeviceInfo.getConstants().isEdgeToEdge !== true + ) { + if (__DEV__) { + warnOnce( + 'KeyboardAvoidingView-experimental_native-edge-to-edge', + 'KeyboardAvoidingView: `experimental_native` has no effect because ' + + 'the app is not edge-to-edge.', + ); + } + return false; + } + return true; +} + +let isNativeSupported: ?boolean = null; + +function shouldUseNative(props: KeyboardAvoidingViewProps): boolean { + if (props.experimental_native !== true || props.behavior == null) { + return false; + } + if (isNativeSupported == null) { + isNativeSupported = getIsNativeSupported(); + } + return isNativeSupported; +} + /** * Automatically adjusts its height, position, or bottom padding based on the * keyboard height to remain visible while the virtual keyboard is displayed. @@ -161,6 +214,10 @@ class KeyboardAvoidingView extends React.Component< }; _updateBottomIfNecessary = async () => { + if (shouldUseNative(this.props)) { + return; + } + if (this._keyboardEvent == null) { this._setBottom(0); return; @@ -196,6 +253,7 @@ class KeyboardAvoidingView extends React.Component< if (enabled && this._bottom !== prevState.bottom) { this.setState({bottom: this._bottom}); } + this._updateSubscriptions(); } componentDidMount(): void { @@ -203,6 +261,29 @@ class KeyboardAvoidingView extends React.Component< this._keyboardEvent = null; this._setBottom(0); } + this._updateSubscriptions(); + } + + // Only the JavaScript implementation needs keyboard events. + _updateSubscriptions(): void { + const shouldSubscribe = !shouldUseNative(this.props); + + if (shouldSubscribe === this._subscriptions.length > 0) { + return; + } + + if (!shouldSubscribe) { + this._subscriptions.forEach(subscription => { + subscription.remove(); + }); + this._subscriptions = []; + // The last event goes stale while unsubscribed. + this._keyboardEvent = null; + if (this._bottom !== 0) { + this._setBottom(0); + } + return; + } if (Platform.OS === 'ios') { this._subscriptions = [ @@ -235,12 +316,47 @@ class KeyboardAvoidingView extends React.Component< children, contentContainerStyle, enabled = true, - // eslint-disable-next-line no-unused-vars + experimental_native, keyboardVerticalOffset = 0, style, onLayout, ...props } = this.props; + + if (shouldUseNative(this.props)) { + if (behavior === 'position') { + return ( + + + {children} + + + ); + } + + return ( + + {children} + + ); + } + const bottomHeight = enabled === true ? this.state.bottom : 0; switch (behavior) { case 'height': diff --git a/packages/react-native/Package.swift b/packages/react-native/Package.swift index ac1611c59c07..b052da345682 100644 --- a/packages/react-native/Package.swift +++ b/packages/react-native/Package.swift @@ -490,6 +490,7 @@ let reactFabric = RNTarget( "consistency", "uimanager/consistency/tests", "components/inputaccessory", + "components/keyboardavoidingview", "components/modal", "components/rncore", "components/safeareaview", @@ -513,6 +514,12 @@ let reactFabricInputAccessory = RNTarget( dependencies: [.reactNativeDependencies, .reactCore, .reactJsiExecutor, .reactTurboModuleCore, .jsi, .logger, .reactDebug, .reactFeatureFlags, .reactUtils, .reactRuntimeScheduler, .reactCxxReact, .yoga, .reactRendererDebug, .reactGraphics, .reactFabric, .reactTurboModuleBridging] ) +let reactFabricKeyboardAvoidingView = RNTarget( + name: .reactFabricKeyboardAvoidingView, + path: "ReactCommon/react/renderer/components/keyboardavoidingview", + dependencies: [.reactNativeDependencies, .reactCore, .reactJsiExecutor, .reactTurboModuleCore, .jsi, .logger, .reactDebug, .reactFeatureFlags, .reactUtils, .reactRuntimeScheduler, .reactCxxReact, .yoga, .reactRendererDebug, .reactGraphics, .reactFabric, .reactTurboModuleBridging] +) + let reactFabricModal = RNTarget( name: .reactFabricModal, path: "ReactCommon/react/renderer/components/modal", @@ -588,7 +595,7 @@ let reactRCTFabric = RNTarget( name: .reactRCTFabric, path: "React/Fabric", searchPaths: [virtualViewPath], - dependencies: [.reactNativeDependencies, .reactCore, .reactRCTImage, .yoga, .reactRCTText, .jsi, .reactFabricInputAccessory, .reactFabricModal, .reactFabricSafeAreaView, .reactFabricSwitch, .reactFabricText, .reactFabricTextInput, .reactFabricUnimplementedView, .reactFabricTextLayoutManager, .reactGraphics, .reactImageManager, .reactDebug, .reactUtils, .reactPerformanceTimeline, .reactRendererDebug, .reactRendererConsistency, .reactRuntimeScheduler, .reactRCTAnimation, .reactJsInspector, .reactJsInspectorNetwork, .reactJsInspectorTracing, .reactFabric, .reactFabricImage, .rctSwiftUIWrapper] + dependencies: [.reactNativeDependencies, .reactCore, .reactRCTImage, .yoga, .reactRCTText, .jsi, .reactFabricInputAccessory, .reactFabricKeyboardAvoidingView, .reactFabricModal, .reactFabricSafeAreaView, .reactFabricSwitch, .reactFabricText, .reactFabricTextInput, .reactFabricUnimplementedView, .reactFabricTextLayoutManager, .reactGraphics, .reactImageManager, .reactDebug, .reactUtils, .reactPerformanceTimeline, .reactRendererDebug, .reactRendererConsistency, .reactRuntimeScheduler, .reactRCTAnimation, .reactJsInspector, .reactJsInspectorNetwork, .reactJsInspectorTracing, .reactFabric, .reactFabricImage, .rctSwiftUIWrapper] ) /// React-ImageManagerApple.podspec @@ -710,6 +717,7 @@ let targets = [ reactRCTFabric, reactFabricImage, reactFabricInputAccessory, + reactFabricKeyboardAvoidingView, reactFabricModal, reactFabricSafeAreaView, reactFabricSwitch, @@ -897,6 +905,7 @@ extension String { static let reactFabricImage = "React-FabricImage" static let reactFabricInputAccessory = "React-FabricInputAccessory" + static let reactFabricKeyboardAvoidingView = "React-FabricKeyboardAvoidingView" static let reactFabricModal = "React-FabricModal" static let reactFabricSafeAreaView = "React-FabricSafeAreaView" static let reactFabricSwitch = "React-FabricSwitch" diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/KeyboardAvoidingView/RCTKeyboardAvoidingViewComponentView.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/KeyboardAvoidingView/RCTKeyboardAvoidingViewComponentView.h new file mode 100644 index 000000000000..c537302bde37 --- /dev/null +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/KeyboardAvoidingView/RCTKeyboardAvoidingViewComponentView.h @@ -0,0 +1,24 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * UIView class for component. + * + * Follows the keyboard with `UIKeyboardLayoutGuide` and sends the keyboard + * inset to the shared animation backend on each frame. + */ +@interface RCTKeyboardAvoidingViewComponentView : RCTViewComponentView + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/KeyboardAvoidingView/RCTKeyboardAvoidingViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/KeyboardAvoidingView/RCTKeyboardAvoidingViewComponentView.mm new file mode 100644 index 000000000000..db4f5b7bd623 --- /dev/null +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/KeyboardAvoidingView/RCTKeyboardAvoidingViewComponentView.mm @@ -0,0 +1,401 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RCTKeyboardAvoidingViewComponentView.h" + +#import +#import + +#import "RCTFabricComponentsPlugins.h" + +using namespace facebook::react; + +/* + * Hidden view between the keyboard layout guide and the bottom of the guide + * owner. Kept out of the guide owner, whose subviews Fabric can manage. + */ +@interface RCTKeyboardTrackingView : UIView + +@property (nonatomic, copy, nullable) void (^onLayout)(void); + +@end + +@implementation RCTKeyboardTrackingView + +- (void)layoutSubviews +{ + [super layoutSubviews]; + if (self.onLayout != nil) { + self.onLayout(); + } +} + +@end + +@interface RCTKeyboardAvoidingViewDisplayLinkTarget : NSObject + +@property (nonatomic, weak) RCTKeyboardAvoidingViewComponentView *view; + +@end + +@interface RCTKeyboardAvoidingViewComponentView () + +- (void)_displayLinkDidFire; + +@end + +@implementation RCTKeyboardAvoidingViewDisplayLinkTarget + +- (void)displayLinkDidFire:(CADisplayLink *)displayLink +{ + [self.view _displayLinkDidFire]; +} + +@end + +@implementation RCTKeyboardAvoidingViewComponentView { + KeyboardAvoidingViewShadowNode::ConcreteState::Shared _state; + RCTKeyboardTrackingView *_trackingView; + // Holds the tracking view. A direct subview of the window, so no other code + // manages its subviews. + UIView *_trackingContainerView; + // Owns the keyboard layout guide. The guide of the window itself is not + // updated, and the guide of this view moves when the view resizes. + __weak UIView *_guideOwnerView; + CADisplayLink *_displayLink; + BOOL _isUpdateScheduled; + + // Height of the tracking view when the keyboard is hidden. + CGFloat _keyboardBaseHeight; + // Smallest height of the tracking view since the guide owner got its size. + CGFloat _minTrackingHeight; + CGSize _guideOwnerSize; + // The tracking view has no size until its first layout. + BOOL _isTrackingViewLaidOut; + CGFloat _targetKeyboardHeight; + // Keyboard height and inset when the keyboard started to move to its target. + CGFloat _startKeyboardHeight; + CGFloat _startInset; + CGFloat _restingHeight; + CGFloat _appliedInset; + CGFloat _appliedRestingHeight; +} + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + _props = KeyboardAvoidingViewShadowNode::defaultSharedProps(); + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(_keyboardWillChangeFrame:) + name:UIKeyboardWillChangeFrameNotification + object:nil]; + } + + return self; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [_displayLink invalidate]; + [_trackingContainerView removeFromSuperview]; +} + +- (void)didMoveToWindow +{ + [super didMoveToWindow]; + + [self _stopDisplayLink]; + [_trackingContainerView removeFromSuperview]; + _trackingContainerView = nil; + _trackingView = nil; + + UIWindow *window = self.window; + if (window == nil) { + return; + } + + UIView *guideOwnerView = window.rootViewController.view; + for (UIResponder *responder = self.nextResponder; responder != nil; responder = responder.nextResponder) { + if ([responder isKindOfClass:[UIViewController class]]) { + guideOwnerView = ((UIViewController *)responder).view; + break; + } + } + if (guideOwnerView == nil) { + return; + } + _guideOwnerView = guideOwnerView; + _guideOwnerSize = CGSizeZero; + _isTrackingViewLaidOut = NO; + + _trackingContainerView = [[UIView alloc] initWithFrame:window.bounds]; + _trackingContainerView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + _trackingContainerView.hidden = YES; + _trackingContainerView.userInteractionEnabled = NO; + [window addSubview:_trackingContainerView]; + + _trackingView = [RCTKeyboardTrackingView new]; + _trackingView.translatesAutoresizingMaskIntoConstraints = NO; + __weak __typeof(self) weakSelf = self; + _trackingView.onLayout = ^{ + __typeof(self) strongSelf = weakSelf; + if (strongSelf == nil) { + return; + } + strongSelf->_isTrackingViewLaidOut = YES; + [strongSelf _startDisplayLink]; + }; + [_trackingContainerView addSubview:_trackingView]; + + [NSLayoutConstraint activateConstraints:@[ + [_trackingView.leadingAnchor constraintEqualToAnchor:_trackingContainerView.leadingAnchor], + [_trackingView.trailingAnchor constraintEqualToAnchor:_trackingContainerView.trailingAnchor], + [_trackingView.topAnchor constraintEqualToAnchor:guideOwnerView.keyboardLayoutGuide.topAnchor], + [_trackingView.bottomAnchor constraintEqualToAnchor:guideOwnerView.bottomAnchor], + ]]; + + [self _scheduleUpdate]; +} + +- (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics + oldLayoutMetrics:(const LayoutMetrics &)oldLayoutMetrics +{ + [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics]; + // A commit can not start inside a mount, so the update runs on the next loop. + [self _scheduleUpdate]; +} + +#pragma mark - Keyboard tracking + +/* + * UIKit animates the guide only while it lays out the guide owner. The tracking + * view is outside of it, so it is laid out here with the keyboard animation. + * The guide already has its final position when this notification is posted. + */ +- (void)_keyboardWillChangeFrame:(NSNotification *)notification +{ + UIView *trackingContainerView = _trackingContainerView; + if (trackingContainerView == nil) { + return; + } + + NSTimeInterval duration = [notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue]; + UIViewAnimationCurve curve = + (UIViewAnimationCurve)[notification.userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue]; + [UIView animateWithDuration:duration + delay:0 + options:(UIViewAnimationOptions)curve << 16 | UIViewAnimationOptionBeginFromCurrentState + animations:^{ + [trackingContainerView layoutIfNeeded]; + } + completion:nil]; +} + +- (CGFloat)_currentKeyboardBaseHeight +{ + // The tracking view is at its smallest height when the keyboard is hidden. + // Capped at the bottom safe area, in case the keyboard was never hidden. + UIView *guideOwnerView = _guideOwnerView; + if (guideOwnerView == nil) { + return 0; + } + if (!CGSizeEqualToSize(guideOwnerView.bounds.size, _guideOwnerSize)) { + _guideOwnerSize = guideOwnerView.bounds.size; + _minTrackingHeight = CGFLOAT_MAX; + } + _minTrackingHeight = MIN(_minTrackingHeight, _trackingView.layer.bounds.size.height); + CGFloat bottomSafeArea = + MAX(CGRectGetMaxY(guideOwnerView.bounds) - CGRectGetMaxY(guideOwnerView.safeAreaLayoutGuide.layoutFrame), 0); + return MIN(bottomSafeArea, _minTrackingHeight); +} + +- (void)_startDisplayLink +{ + if (_displayLink != nil) { + return; + } + auto target = [RCTKeyboardAvoidingViewDisplayLinkTarget new]; + target.view = self; + _displayLink = [CADisplayLink displayLinkWithTarget:target selector:@selector(displayLinkDidFire:)]; + [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; +} + +- (void)_stopDisplayLink +{ + [_displayLink invalidate]; + _displayLink = nil; +} + +- (void)_displayLinkDidFire +{ + if (_trackingView == nil || !_isTrackingViewLaidOut) { + [self _stopDisplayLink]; + return; + } + + CALayer *layer = _trackingView.layer; + CALayer *presentationLayer = layer.presentationLayer ?: layer; + BOOL isAnimating = layer.animationKeys.count > 0; + + _keyboardBaseHeight = [self _currentKeyboardBaseHeight]; + CGFloat keyboardHeight = MAX(presentationLayer.bounds.size.height - _keyboardBaseHeight, 0); + CGFloat targetHeight = MAX(layer.bounds.size.height - _keyboardBaseHeight, 0); + + [self _keyboardFrameDidChange:keyboardHeight targetHeight:targetHeight]; + + if (!isAnimating) { + [self _stopDisplayLink]; + } +} + +- (void)_scheduleUpdate +{ + if (_isUpdateScheduled) { + return; + } + _isUpdateScheduled = YES; + __weak __typeof(self) weakSelf = self; + dispatch_async(dispatch_get_main_queue(), ^{ + __typeof(self) strongSelf = weakSelf; + if (strongSelf == nil) { + return; + } + strongSelf->_isUpdateScheduled = NO; + // A running display link sends the next update itself. + if (strongSelf->_displayLink == nil) { + [strongSelf _startDisplayLink]; + } + }); +} + +/* + * Overlap between the view at rest and a keyboard of the given height, + * measured like the JavaScript implementation. + */ +- (CGFloat)_relativeKeyboardHeight:(CGFloat)keyboardHeight +{ + if (keyboardHeight <= 0 || self.window == nil) { + return 0; + } + + const auto &props = static_cast(*_props); + + CGFloat frameY; + CGFloat frameHeight; + if (props.behavior == KeyboardAvoidingViewBehavior::Position) { + // `position` moves a content container: measure the view that holds it. + UIView *container = self.superview; + if (container == nil) { + return 0; + } + frameHeight = container.bounds.size.height; + frameY = container.center.y - frameHeight * container.layer.anchorPoint.y; + } else { + frameY = _layoutMetrics.frame.origin.y; + frameHeight = + props.behavior == KeyboardAvoidingViewBehavior::Height ? _restingHeight : _layoutMetrics.frame.size.height; + } + + CGFloat keyboardY = self.window.bounds.size.height - keyboardHeight - props.keyboardVerticalOffset; + return MAX(frameY + frameHeight - keyboardY, 0); +} + +- (void)_keyboardFrameDidChange:(CGFloat)keyboardHeight targetHeight:(CGFloat)targetHeight +{ + if (targetHeight != _targetKeyboardHeight) { + _targetKeyboardHeight = targetHeight; + _startKeyboardHeight = keyboardHeight; + _startInset = _appliedInset; + } + + if (!_state) { + return; + } + + const auto &props = static_cast(*_props); + if (props.behavior == KeyboardAvoidingViewBehavior::None && _appliedInset == 0) { + return; + } + + if (_appliedInset == 0) { + _restingHeight = _layoutMetrics.frame.size.height; + } + + // The tracking view stops at the bottom of the guide owner, which can be + // above the bottom of the window. + CGFloat ownerBottomInset = 0; + if (UIView *guideOwnerView = _guideOwnerView) { + ownerBottomInset = MAX( + self.window.bounds.size.height - CGRectGetMaxY([guideOwnerView convertRect:guideOwnerView.bounds toView:nil]), + 0); + } + CGFloat targetInset = + targetHeight > 0 ? [self _relativeKeyboardHeight:targetHeight + _keyboardBaseHeight + ownerBottomInset] : 0; + CGFloat distance = targetHeight - _startKeyboardHeight; + CGFloat progress = distance != 0 ? MIN(MAX((keyboardHeight - _startKeyboardHeight) / distance, 0), 1) : 1; + BOOL isActive = props.enabled && props.behavior != KeyboardAvoidingViewBehavior::None; + CGFloat inset = isActive ? _startInset + (targetInset - _startInset) * progress : 0; + + if (inset == _appliedInset && (inset == 0 || _restingHeight == _appliedRestingHeight)) { + return; + } + + _appliedInset = inset; + _appliedRestingHeight = _restingHeight; + _state->getData().update(inset, _restingHeight); +} + +#pragma mark - RCTComponentViewProtocol + ++ (ComponentDescriptorProvider)componentDescriptorProvider +{ + return concreteComponentDescriptorProvider(); +} + +- (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps +{ + const auto &oldKeyboardAvoidingViewProps = static_cast(*_props); + const auto &newKeyboardAvoidingViewProps = static_cast(*props); + + BOOL needsUpdate = oldKeyboardAvoidingViewProps.behavior != newKeyboardAvoidingViewProps.behavior || + oldKeyboardAvoidingViewProps.enabled != newKeyboardAvoidingViewProps.enabled || + oldKeyboardAvoidingViewProps.keyboardVerticalOffset != newKeyboardAvoidingViewProps.keyboardVerticalOffset; + + [super updateProps:props oldProps:oldProps]; + + if (needsUpdate) { + [self _scheduleUpdate]; + } +} + +- (void)updateState:(const State::Shared &)state oldState:(const State::Shared &)oldState +{ + _state = std::static_pointer_cast(state); + [self _scheduleUpdate]; +} + +- (void)prepareForRecycle +{ + [super prepareForRecycle]; + [self _stopDisplayLink]; + _state.reset(); + _targetKeyboardHeight = 0; + _startKeyboardHeight = 0; + _startInset = 0; + _restingHeight = 0; + _appliedInset = 0; + _appliedRestingHeight = 0; +} + +@end + +Class RCTKeyboardAvoidingViewCls(void) +{ + return RCTKeyboardAvoidingViewComponentView.class; +} diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/RCTFabricComponentsPlugins.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/RCTFabricComponentsPlugins.h index 6b8a6ff6d6dc..4d5beec3b0a1 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/RCTFabricComponentsPlugins.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/RCTFabricComponentsPlugins.h @@ -32,6 +32,7 @@ Class RCTFabricComponentsProvider(const char *name); Class RCTActivityIndicatorViewCls(void) __attribute__((used)); Class RCTDebuggingOverlayCls(void) __attribute__((used)); Class RCTInputAccessoryCls(void) __attribute__((used)); +Class RCTKeyboardAvoidingViewCls(void) __attribute__((used)); Class RCTParagraphCls(void) __attribute__((used)); Class RCTPullToRefreshViewCls(void) __attribute__((used)); Class RCTSafeAreaViewCls(void) __attribute__((used)); diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/RCTFabricComponentsPlugins.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/RCTFabricComponentsPlugins.mm index 442c2e6e3962..b6f46c1e4250 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/RCTFabricComponentsPlugins.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/RCTFabricComponentsPlugins.mm @@ -21,6 +21,7 @@ {"ActivityIndicatorView", RCTActivityIndicatorViewCls}, {"DebuggingOverlay", RCTDebuggingOverlayCls}, {"InputAccessoryView", RCTInputAccessoryCls}, + {"KeyboardAvoidingView", RCTKeyboardAvoidingViewCls}, {"Paragraph", RCTParagraphCls}, {"PullToRefreshView", RCTPullToRefreshViewCls}, {"SafeAreaView", RCTSafeAreaViewCls}, diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.kt index 14b053b4196f..6311b9a5506b 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.kt @@ -48,6 +48,7 @@ import com.facebook.react.modules.websocket.WebSocketModule import com.facebook.react.uimanager.ViewManager import com.facebook.react.views.drawer.ReactDrawerLayoutManager import com.facebook.react.views.image.ReactImageManager +import com.facebook.react.views.keyboardavoidingview.ReactKeyboardAvoidingViewManager import com.facebook.react.views.modal.ReactModalHostManager import com.facebook.react.views.progressbar.ReactProgressBarViewManager import com.facebook.react.views.safeareaview.ReactSafeAreaViewManager @@ -144,6 +145,7 @@ constructor(private val config: MainPackageConfig? = null) : ReactDrawerLayoutManager(), ReactHorizontalScrollViewManager(), ReactHorizontalScrollContainerViewManager(), + ReactKeyboardAvoidingViewManager(), ReactProgressBarViewManager(), if (ReactNativeFeatureFlags.useNestedScrollViewAndroid()) ReactNestedScrollViewManager() else ReactScrollViewManager(), @@ -176,6 +178,8 @@ constructor(private val config: MainPackageConfig? = null) : ModuleSpec.viewManagerSpec { ReactHorizontalScrollViewManager() }, ReactHorizontalScrollContainerViewManager.REACT_CLASS to ModuleSpec.viewManagerSpec { ReactHorizontalScrollContainerViewManager() }, + ReactKeyboardAvoidingViewManager.REACT_CLASS to + ModuleSpec.viewManagerSpec { ReactKeyboardAvoidingViewManager() }, ReactProgressBarViewManager.REACT_CLASS to ModuleSpec.viewManagerSpec { ReactProgressBarViewManager() }, ReactSafeAreaViewManager.REACT_CLASS to diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/keyboardavoidingview/KeyboardAvoidingViewBinding.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/keyboardavoidingview/KeyboardAvoidingViewBinding.kt new file mode 100644 index 000000000000..130a073af55a --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/keyboardavoidingview/KeyboardAvoidingViewBinding.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.views.keyboardavoidingview + +import com.facebook.proguard.annotations.DoNotStripAny +import com.facebook.react.fabric.FabricSoLoader +import com.facebook.react.fabric.StateWrapperImpl + +/** + * Sends keyboard avoidance values to the shared animation backend, through the C++ state of a + * `KeyboardAvoidingView` shadow node. Must be called on the UI thread. + */ +@DoNotStripAny +internal object KeyboardAvoidingViewBinding { + init { + FabricSoLoader.staticInit() + } + + @JvmStatic + external fun update( + stateWrapper: StateWrapperImpl, + keyboardInset: Float, + keyboardRestingHeight: Float, + ) +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/keyboardavoidingview/ReactKeyboardAvoidingView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/keyboardavoidingview/ReactKeyboardAvoidingView.kt new file mode 100644 index 000000000000..1409056caac9 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/keyboardavoidingview/ReactKeyboardAvoidingView.kt @@ -0,0 +1,220 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.views.keyboardavoidingview + +import android.content.Context +import android.view.View +import androidx.annotation.UiThread +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsAnimationCompat +import androidx.core.view.WindowInsetsCompat +import com.facebook.react.fabric.StateWrapperImpl +import com.facebook.react.uimanager.PixelUtil.pxToDp +import com.facebook.react.uimanager.StateWrapper +import com.facebook.react.views.view.ReactViewGroup + +/** + * Follows the keyboard with [WindowInsetsAnimationCompat] and sends the keyboard inset to the + * shared animation backend on each frame. The shadow node applies the inset to its layout. + */ +internal class ReactKeyboardAvoidingView(context: Context) : ReactViewGroup(context) { + + internal enum class Behavior { + NONE, + HEIGHT, + PADDING, + POSITION, + } + + internal var stateWrapper: StateWrapper? = null + set(value) { + field = value + scheduleUpdate() + } + + internal var behavior: Behavior = Behavior.NONE + set(value) { + if (field != value) { + field = value + scheduleUpdate() + } + } + + internal var isAvoidanceEnabled: Boolean = true + set(value) { + if (field != value) { + field = value + scheduleUpdate() + } + } + + /** In dp. */ + internal var keyboardVerticalOffset: Float = 0f + set(value) { + if (field != value) { + field = value + scheduleUpdate() + } + } + + private var isAnimating = false + private var isUpdateScheduled = false + + // All values below are in dp. + private var lastKeyboardHeight = 0f + private var targetKeyboardHeight = 0f + private var lastTargetHeight = 0f + // Keyboard height and inset when the keyboard started to move to its target. + private var startKeyboardHeight = 0f + private var startInset = 0f + private var restingHeight = 0f + private var appliedInset = 0f + private var appliedRestingHeight = 0f + + private val updateRunnable = Runnable { + isUpdateScheduled = false + if (!isAnimating) { + val keyboardHeight = currentKeyboardHeight() + onKeyboardFrame(keyboardHeight, keyboardHeight) + } + } + + private val insetsAnimationCallback = + object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) { + override fun onPrepare(animation: WindowInsetsAnimationCompat) { + if (animation.isImeAnimation()) { + isAnimating = true + } + } + + override fun onStart( + animation: WindowInsetsAnimationCompat, + bounds: WindowInsetsAnimationCompat.BoundsCompat, + ): WindowInsetsAnimationCompat.BoundsCompat { + if (animation.isImeAnimation()) { + // Insets are already in their end state when the animation starts. + targetKeyboardHeight = currentKeyboardHeight() + onKeyboardFrame(lastKeyboardHeight, targetKeyboardHeight) + } + return bounds + } + + override fun onProgress( + insets: WindowInsetsCompat, + runningAnimations: MutableList, + ): WindowInsetsCompat { + if (runningAnimations.any { it.isImeAnimation() }) { + onKeyboardFrame( + insets.getInsets(WindowInsetsCompat.Type.ime()).bottom.toFloat().pxToDp(), + targetKeyboardHeight, + ) + } + return insets + } + + override fun onEnd(animation: WindowInsetsAnimationCompat) { + if (animation.isImeAnimation()) { + isAnimating = false + val keyboardHeight = currentKeyboardHeight() + onKeyboardFrame(keyboardHeight, keyboardHeight) + } + } + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + ViewCompat.setWindowInsetsAnimationCallback(this, insetsAnimationCallback) + ViewCompat.setOnApplyWindowInsetsListener(this) { _, insets -> + scheduleUpdate() + insets + } + scheduleUpdate() + } + + override fun onDetachedFromWindow() { + ViewCompat.setWindowInsetsAnimationCallback(this, null) + ViewCompat.setOnApplyWindowInsetsListener(this, null) + removeCallbacks(updateRunnable) + isUpdateScheduled = false + isAnimating = false + super.onDetachedFromWindow() + } + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + super.onLayout(changed, left, top, right, bottom) + // A commit can not start inside a mount, so the update runs on the next loop. + if (changed) { + scheduleUpdate() + } + } + + private fun scheduleUpdate() { + if (!isUpdateScheduled && isAttachedToWindow) { + isUpdateScheduled = true + post(updateRunnable) + } + } + + private fun WindowInsetsAnimationCompat.isImeAnimation(): Boolean = + (typeMask and WindowInsetsCompat.Type.ime()) != 0 + + private fun currentKeyboardHeight(): Float { + val insets = ViewCompat.getRootWindowInsets(this) ?: return 0f + return insets.getInsets(WindowInsetsCompat.Type.ime()).bottom.toFloat().pxToDp() + } + + /** Overlap between the view (at rest) and a keyboard of the given height, in dp. */ + private fun relativeKeyboardHeight(keyboardHeight: Float): Float { + if (keyboardHeight <= 0f) { + return 0f + } + // `position` moves a content container: measure the view that holds it. + val frameView: View = if (behavior == Behavior.POSITION) parent as? View ?: return 0f else this + val frameY = frameView.top.toFloat().pxToDp() + val frameHeight = + if (behavior == Behavior.HEIGHT) restingHeight else frameView.height.toFloat().pxToDp() + val keyboardY = rootView.height.toFloat().pxToDp() - keyboardHeight - keyboardVerticalOffset + return maxOf(frameY + frameHeight - keyboardY, 0f) + } + + @UiThread + private fun onKeyboardFrame(keyboardHeight: Float, targetHeight: Float) { + lastKeyboardHeight = keyboardHeight + if (targetHeight != lastTargetHeight) { + lastTargetHeight = targetHeight + startKeyboardHeight = keyboardHeight + startInset = appliedInset + } + + // The mounting manager destroys the state when it deletes the view. + val stateWrapper = (stateWrapper as? StateWrapperImpl)?.takeIf { it.isValid } ?: return + if (behavior == Behavior.NONE && appliedInset == 0f) { + return + } + + if (appliedInset == 0f) { + restingHeight = height.toFloat().pxToDp() + } + + val targetInset = relativeKeyboardHeight(targetHeight) + val distance = targetHeight - startKeyboardHeight + val progress = + if (distance != 0f) ((keyboardHeight - startKeyboardHeight) / distance).coerceIn(0f, 1f) + else 1f + val isActive = isAvoidanceEnabled && behavior != Behavior.NONE + val inset = if (isActive) startInset + (targetInset - startInset) * progress else 0f + + if (inset == appliedInset && (inset == 0f || restingHeight == appliedRestingHeight)) { + return + } + + appliedInset = inset + appliedRestingHeight = restingHeight + KeyboardAvoidingViewBinding.update(stateWrapper, inset, restingHeight) + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/keyboardavoidingview/ReactKeyboardAvoidingViewManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/keyboardavoidingview/ReactKeyboardAvoidingViewManager.kt new file mode 100644 index 000000000000..7a2f77bbbc4b --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/keyboardavoidingview/ReactKeyboardAvoidingViewManager.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.views.keyboardavoidingview + +import com.facebook.react.module.annotations.ReactModule +import com.facebook.react.uimanager.ReactStylesDiffMap +import com.facebook.react.uimanager.StateWrapper +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.annotations.ReactProp +import com.facebook.react.views.view.ReactViewGroup +import com.facebook.react.views.view.ReactViewManager + +/** View manager for [ReactKeyboardAvoidingView] components. */ +@ReactModule(name = ReactKeyboardAvoidingViewManager.REACT_CLASS) +internal class ReactKeyboardAvoidingViewManager : ReactViewManager() { + + override fun getName(): String = REACT_CLASS + + override fun createViewInstance(context: ThemedReactContext): ReactViewGroup = + ReactKeyboardAvoidingView(context) + + override fun updateState( + view: ReactViewGroup, + props: ReactStylesDiffMap, + stateWrapper: StateWrapper, + ): Any? { + (view as ReactKeyboardAvoidingView).stateWrapper = stateWrapper + return null + } + + @ReactProp(name = "behavior") + fun setBehavior(view: ReactViewGroup, behavior: String?) { + (view as ReactKeyboardAvoidingView).behavior = + when (behavior) { + "height" -> ReactKeyboardAvoidingView.Behavior.HEIGHT + "padding" -> ReactKeyboardAvoidingView.Behavior.PADDING + "position" -> ReactKeyboardAvoidingView.Behavior.POSITION + else -> ReactKeyboardAvoidingView.Behavior.NONE + } + } + + @ReactProp(name = "enabled", defaultBoolean = true) + fun setEnabled(view: ReactViewGroup, enabled: Boolean) { + (view as ReactKeyboardAvoidingView).isAvoidanceEnabled = enabled + } + + @ReactProp(name = "keyboardVerticalOffset", defaultFloat = 0f) + fun setKeyboardVerticalOffset(view: ReactViewGroup, offset: Float) { + (view as ReactKeyboardAvoidingView).keyboardVerticalOffset = offset + } + + internal companion object { + const val REACT_CLASS: String = "KeyboardAvoidingView" + } +} diff --git a/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt b/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt index 4c5be342c0bd..ac64a9bc22b1 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt +++ b/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt @@ -119,6 +119,7 @@ add_react_common_subdir(react/renderer/components/legacyviewmanagerinterop) add_react_common_subdir(react/renderer/componentregistry/native) add_react_common_subdir(react/renderer/components/text) add_react_common_subdir(react/renderer/components/unimplementedview) +add_react_common_subdir(react/renderer/components/keyboardavoidingview) add_react_common_subdir(react/renderer/components/modal) add_react_common_subdir(react/renderer/components/scrollview) add_react_common_subdir(react/renderer/components/safeareaview) @@ -253,6 +254,7 @@ add_library(reactnative $ $ $ + $ $ $ $ @@ -357,6 +359,7 @@ target_include_directories(reactnative $ $ $ + $ $ $ $ diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/CMakeLists.txt b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/CMakeLists.txt index 557b7a14b23a..aa037171bbe9 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/CMakeLists.txt +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/CMakeLists.txt @@ -48,6 +48,7 @@ target_link_libraries( react_renderer_textlayoutmanager react_renderer_uimanager react_renderer_uimanager_consistency + rrc_keyboardavoidingview rrc_legacyviewmanagerinterop react_utils reactnativejni diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/KeyboardAvoidingViewBinding.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/KeyboardAvoidingViewBinding.cpp new file mode 100644 index 000000000000..cd2dfcac078e --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/KeyboardAvoidingViewBinding.cpp @@ -0,0 +1,47 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "KeyboardAvoidingViewBinding.h" + +#include + +namespace facebook::react { + +namespace { + +std::shared_ptr getState( + jni::alias_ref stateWrapper) { + if (!stateWrapper) { + return nullptr; + } + return std::dynamic_pointer_cast< + const KeyboardAvoidingViewShadowNode::ConcreteState>( + stateWrapper->cthis()->getState()); +} + +} // namespace + +void KeyboardAvoidingViewBinding::registerNatives() { + javaClassStatic()->registerNatives({ + makeNativeMethod("update", KeyboardAvoidingViewBinding::update), + }); +} + +void KeyboardAvoidingViewBinding::update( + jni::alias_ref /*unused*/, + jni::alias_ref stateWrapper, + jfloat keyboardInset, + jfloat keyboardRestingHeight) { + auto state = getState(stateWrapper); + if (state == nullptr) { + return; + } + + state->getData().update(keyboardInset, keyboardRestingHeight); +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/KeyboardAvoidingViewBinding.h b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/KeyboardAvoidingViewBinding.h new file mode 100644 index 000000000000..2068ea288c8c --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/KeyboardAvoidingViewBinding.h @@ -0,0 +1,35 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +#include "StateWrapperImpl.h" + +namespace facebook::react { + +/* + * Gives `ReactKeyboardAvoidingView` access to the shared animation backend + * through the C++ state of its shadow node. + */ +class KeyboardAvoidingViewBinding : public jni::JavaClass { + public: + constexpr static const char *const kJavaDescriptor = + "Lcom/facebook/react/views/keyboardavoidingview/KeyboardAvoidingViewBinding;"; + + static void registerNatives(); + + private: + static void update( + jni::alias_ref /*unused*/, + jni::alias_ref stateWrapper, + jfloat keyboardInset, + jfloat keyboardRestingHeight); +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/OnLoad.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/OnLoad.cpp index 2d411d4dc7af..47caaf3d93e6 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/OnLoad.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/OnLoad.cpp @@ -11,6 +11,7 @@ #include "EventBeatManager.h" #include "EventEmitterWrapper.h" #include "FabricUIManagerBinding.h" +#include "KeyboardAvoidingViewBinding.h" #include "StateWrapperImpl.h" #include "SurfaceHandlerBinding.h" @@ -19,6 +20,7 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* /*unused*/) { facebook::react::EventBeatManager::registerNatives(); facebook::react::EventEmitterWrapper::registerNatives(); facebook::react::FabricUIManagerBinding::registerNatives(); + facebook::react::KeyboardAvoidingViewBinding::registerNatives(); facebook::react::StateWrapperImpl::registerNatives(); facebook::react::ComponentFactory::registerNatives(); facebook::react::SurfaceHandlerBinding::registerNatives(); diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/newarchdefaults/CMakeLists.txt b/packages/react-native/ReactAndroid/src/main/jni/react/newarchdefaults/CMakeLists.txt index 62b76deff23f..fd9418769a61 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/newarchdefaults/CMakeLists.txt +++ b/packages/react-native/ReactAndroid/src/main/jni/react/newarchdefaults/CMakeLists.txt @@ -33,6 +33,7 @@ target_link_libraries(react_newarchdefaults react_renderer_componentregistry react_renderer_textlayoutmanager rrc_image + rrc_keyboardavoidingview rrc_modal rrc_progressbar rrc_safeareaview diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/newarchdefaults/CoreComponentsRegistry.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/newarchdefaults/CoreComponentsRegistry.cpp index 5eacbff0b81d..18647c8cffb7 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/newarchdefaults/CoreComponentsRegistry.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/newarchdefaults/CoreComponentsRegistry.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -58,6 +59,9 @@ void addCoreComponents( concreteComponentDescriptorProvider()); providerRegistry->add( concreteComponentDescriptorProvider()); + providerRegistry->add( + concreteComponentDescriptorProvider< + KeyboardAvoidingViewComponentDescriptor>()); providerRegistry->add( concreteComponentDescriptorProvider()); providerRegistry->add( diff --git a/packages/react-native/ReactCommon/React-FabricComponents.podspec b/packages/react-native/ReactCommon/React-FabricComponents.podspec index 95d9c1c4548f..facd6079820a 100644 --- a/packages/react-native/ReactCommon/React-FabricComponents.podspec +++ b/packages/react-native/ReactCommon/React-FabricComponents.podspec @@ -101,6 +101,12 @@ Pod::Spec.new do |s| sss.header_dir = "react/renderer/components/inputaccessory" end + ss.subspec "keyboardavoidingview" do |sss| + sss.source_files = podspec_sources("react/renderer/components/keyboardavoidingview/**/*.{m,mm,cpp,h}", "react/renderer/components/keyboardavoidingview/**/*.h") + sss.exclude_files = "react/renderer/components/keyboardavoidingview/tests" + sss.header_dir = "react/renderer/components/keyboardavoidingview" + end + ss.subspec "modal" do |sss| sss.source_files = podspec_sources("react/renderer/components/modal/*.{m,mm,cpp,h}", "react/renderer/components/modal/*.h") sss.exclude_files = "react/renderer/components/modal/tests" diff --git a/packages/react-native/ReactCommon/react/renderer/components/keyboardavoidingview/CMakeLists.txt b/packages/react-native/ReactCommon/react/renderer/components/keyboardavoidingview/CMakeLists.txt new file mode 100644 index 000000000000..057a00fa22b9 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/keyboardavoidingview/CMakeLists.txt @@ -0,0 +1,41 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +cmake_minimum_required(VERSION 3.13) +set(CMAKE_VERBOSE_MAKEFILE on) + +include(${REACT_COMMON_DIR}/cmake-utils/react-native-flags.cmake) + +file(GLOB rrc_keyboardavoidingview_SRCS CONFIGURE_DEPENDS *.cpp) + +add_library( + rrc_keyboardavoidingview + STATIC + ${rrc_keyboardavoidingview_SRCS} +) + +target_include_directories(rrc_keyboardavoidingview PUBLIC .) + +target_link_libraries( + rrc_keyboardavoidingview + glog + fbjni + folly_runtime + glog_init + react_codegen_rncore + react_cxxstableapi + react_debug + react_renderer_animationbackend + react_renderer_componentregistry + react_renderer_core + react_renderer_debug + react_renderer_graphics + react_renderer_uimanager + reactnativejni + rrc_view + yoga +) + +target_compile_reactnative_options(rrc_keyboardavoidingview PRIVATE) diff --git a/packages/react-native/ReactCommon/react/renderer/components/keyboardavoidingview/KeyboardAvoidingViewComponentDescriptor.h b/packages/react-native/ReactCommon/react/renderer/components/keyboardavoidingview/KeyboardAvoidingViewComponentDescriptor.h new file mode 100644 index 000000000000..8086c27b2361 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/keyboardavoidingview/KeyboardAvoidingViewComponentDescriptor.h @@ -0,0 +1,31 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +#include +#include + +namespace facebook::react { + +/* + * Descriptor for component. + */ +class KeyboardAvoidingViewComponentDescriptor final + : public ConcreteComponentDescriptor { + using ConcreteComponentDescriptor::ConcreteComponentDescriptor; + void adopt(ShadowNode &shadowNode) const override + { + static_cast(shadowNode).applyKeyboardInset(); + + ConcreteComponentDescriptor::adopt(shadowNode); + } +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/keyboardavoidingview/KeyboardAvoidingViewShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/components/keyboardavoidingview/KeyboardAvoidingViewShadowNode.cpp new file mode 100644 index 000000000000..f9322b8e5d66 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/keyboardavoidingview/KeyboardAvoidingViewShadowNode.cpp @@ -0,0 +1,67 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "KeyboardAvoidingViewShadowNode.h" + +#include + +namespace facebook::react { + +// NOLINTNEXTLINE(modernize-avoid-c-arrays) +const char KeyboardAvoidingViewComponentName[] = "KeyboardAvoidingView"; + +KeyboardAvoidingViewState KeyboardAvoidingViewShadowNode::initialStateData( + const Props::Shared& /*props*/, + const ShadowNodeFamily::Shared& family, + const ComponentDescriptor& componentDescriptor) { + auto animationBackend = + componentDescriptor.getContextContainer() + ->find>(AnimationBackendKey) + .value_or(std::weak_ptr{}); + return {family, animationBackend}; +} + +void KeyboardAvoidingViewShadowNode::applyKeyboardInset() const { + ensureUnsealed(); + + const auto& props = getConcreteProps(); + if (!props.enabled || props.keyboardInset <= 0) { + return; + } + + auto inset = + yoga::StyleLength::points(static_cast(props.keyboardInset)); + auto style = yogaNode_.style(); + + switch (props.behavior) { + case KeyboardAvoidingViewBehavior::None: + return; + case KeyboardAvoidingViewBehavior::Height: + style.setDimension( + yoga::Dimension::Height, + yoga::StyleSizeLength::points( + std::max( + static_cast( + props.keyboardRestingHeight - props.keyboardInset), + 0.0f))); + style.setFlex(yoga::FloatOptional(0)); + break; + case KeyboardAvoidingViewBehavior::Padding: + style.setPadding(yoga::Edge::Bottom, inset); + break; + case KeyboardAvoidingViewBehavior::Position: + style.setPosition(yoga::Edge::Bottom, inset); + break; + } + + if (style != yogaNode_.style()) { + yogaNode_.setStyle(style); + yogaNode_.setDirty(true); + } +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/keyboardavoidingview/KeyboardAvoidingViewShadowNode.h b/packages/react-native/ReactCommon/react/renderer/components/keyboardavoidingview/KeyboardAvoidingViewShadowNode.h new file mode 100644 index 000000000000..7713e5d4b1bf --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/keyboardavoidingview/KeyboardAvoidingViewShadowNode.h @@ -0,0 +1,45 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +#include +#include +#include +#include + +namespace facebook::react { + +// NOLINTNEXTLINE(modernize-avoid-c-arrays) +extern const char KeyboardAvoidingViewComponentName[]; + +/* + * `ShadowNode` for component. + */ +class KeyboardAvoidingViewShadowNode final : public ConcreteViewShadowNode< + KeyboardAvoidingViewComponentName, + KeyboardAvoidingViewProps, + ViewEventEmitter, + KeyboardAvoidingViewState> { + public: + using ConcreteViewShadowNode::ConcreteViewShadowNode; + + static KeyboardAvoidingViewState initialStateData( + const Props::Shared &props, + const ShadowNodeFamily::Shared &family, + const ComponentDescriptor &componentDescriptor); + + /* + * Applies the `keyboardInset` prop to the Yoga style, on top of the style + * set by React. Called on each new node and on each clone. + */ + void applyKeyboardInset() const; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/keyboardavoidingview/KeyboardAvoidingViewState.cpp b/packages/react-native/ReactCommon/react/renderer/components/keyboardavoidingview/KeyboardAvoidingViewState.cpp new file mode 100644 index 000000000000..c18e4f77e34b --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/keyboardavoidingview/KeyboardAvoidingViewState.cpp @@ -0,0 +1,58 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "KeyboardAvoidingViewState.h" + +#include +#include + +#include + +namespace facebook::react { + +KeyboardAvoidingViewState::KeyboardAvoidingViewState( + ShadowNodeFamily::Weak family, + std::weak_ptr animationBackend) + : family_(std::move(family)), + animationBackend_(std::move(animationBackend)) {} + +#ifdef ANDROID +folly::dynamic KeyboardAvoidingViewState::getDynamic() const { + return folly::dynamic::object(); +} +#endif + +void KeyboardAvoidingViewState::update( + Float keyboardInset, + Float keyboardRestingHeight) const { + auto family = family_.lock(); + auto animationBackend = animationBackend_.lock(); + if (!family || !animationBackend) { + return; + } + + animationBackend->pushAnimationMutations( + [&](AnimationTimestamp /*timestamp*/) { + folly::dynamic rawProps = folly::dynamic::object( + "keyboardInset", static_cast(keyboardInset))( + "keyboardRestingHeight", + static_cast(keyboardRestingHeight)); + AnimatedPropsBuilder builder; + builder.storeDynamic(rawProps); + + AnimationMutations mutations; + mutations.batch.push_back( + AnimationMutation{ + .tag = family->getTag(), + .family = family, + .props = builder.get(), + .hasLayoutUpdates = true}); + return mutations; + }); +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/keyboardavoidingview/KeyboardAvoidingViewState.h b/packages/react-native/ReactCommon/react/renderer/components/keyboardavoidingview/KeyboardAvoidingViewState.h new file mode 100644 index 000000000000..3e2ecf4d25d8 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/keyboardavoidingview/KeyboardAvoidingViewState.h @@ -0,0 +1,51 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +#include +#include +#include + +#include + +#ifdef ANDROID +#include +#endif + +namespace facebook::react { + +/* + * State for component. + */ +class KeyboardAvoidingViewState final { + public: + KeyboardAvoidingViewState() = default; + + KeyboardAvoidingViewState(ShadowNodeFamily::Weak family, std::weak_ptr animationBackend); + +#ifdef ANDROID + KeyboardAvoidingViewState(const KeyboardAvoidingViewState &previousState, const folly::dynamic & /*data*/) + : KeyboardAvoidingViewState(previousState) {}; + + folly::dynamic getDynamic() const; +#endif + + /* + * Sends the keyboard inset to the shared animation backend, which applies + * it with a commit. Must be called on the main thread. + */ + void update(Float keyboardInset, Float keyboardRestingHeight) const; + + private: + ShadowNodeFamily::Weak family_; + std::weak_ptr animationBackend_; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp index a278fb2b3400..0f4777843067 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp @@ -130,6 +130,7 @@ Scheduler::Scheduler( // Must come after `setDelegate`: the backend's constructor registers a // surface-start callback, and `UIManager` silently drops those while it has // no delegate. + contextContainer_->erase(AnimationBackendKey); if (ReactNativeFeatureFlags::useSharedAnimatedBackend()) { auto animationBackend = std::make_shared( schedulerToolbox.animationChoreographer, uiManager); @@ -138,6 +139,10 @@ Scheduler::Scheduler( animationBackend); uiManager->unstable_setAnimationBackend(animationBackend); + + contextContainer_->insert( + AnimationBackendKey, + std::weak_ptr(animationBackend)); } auto bindingsExecutor = diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerAnimationBackend.h b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerAnimationBackend.h index d57f1e772ca9..b05c6fc88417 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerAnimationBackend.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerAnimationBackend.h @@ -24,6 +24,13 @@ struct AnimationMutations; using AnimationTimestamp = std::chrono::duration; using CallbackId = uint64_t; +/* + * `ContextContainer` key of the `std::weak_ptr` + * that native components use to animate their own props. + * Only present when the backend is enabled. + */ +constexpr const char *const AnimationBackendKey = "AnimationBackend"; + class UIManagerAnimationBackend { public: using Callback = std::function; diff --git a/packages/react-native/scripts/ios-prebuild/headers-config.js b/packages/react-native/scripts/ios-prebuild/headers-config.js index 61addbc7a4c4..bd7f01e322ec 100644 --- a/packages/react-native/scripts/ios-prebuild/headers-config.js +++ b/packages/react-native/scripts/ios-prebuild/headers-config.js @@ -374,6 +374,17 @@ const PodspecExceptions /*: {[key: string]: PodSpecConfiguration} */ = { headerDir: 'react/renderer/components/inputaccessory', }, + { + name: 'keyboardavoidingview', + headerPatterns: [ + 'react/renderer/components/keyboardavoidingview/**/*.h', + ], + excludePatterns: [ + 'react/renderer/components/keyboardavoidingview/tests', + ], + headerDir: 'react/renderer/components/keyboardavoidingview', + }, + { name: 'modal', headerPatterns: ['react/renderer/components/modal/*.h'], diff --git a/packages/react-native/src/private/components/keyboardavoidingview/specs/KeyboardAvoidingViewNativeComponent.js b/packages/react-native/src/private/components/keyboardavoidingview/specs/KeyboardAvoidingViewNativeComponent.js new file mode 100644 index 000000000000..d3213c10a3c3 --- /dev/null +++ b/packages/react-native/src/private/components/keyboardavoidingview/specs/KeyboardAvoidingViewNativeComponent.js @@ -0,0 +1,40 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +import type {ViewProps} from '../../../../../Libraries/Components/View/ViewPropTypes'; +import type { + Float, + WithDefault, +} from '../../../../../Libraries/Types/CodegenTypes'; +import type {HostComponent} from '../../../types/HostComponent'; + +import codegenNativeComponent from '../../../../../Libraries/Utilities/codegenNativeComponent'; + +type KeyboardAvoidingViewNativeProps = Readonly<{ + ...ViewProps, + + behavior?: WithDefault<'none' | 'height' | 'padding' | 'position', 'none'>, + enabled?: WithDefault, + keyboardVerticalOffset?: WithDefault, + + /** + * Written by native code through the shared animation backend on each + * keyboard animation frame. Do not set from JavaScript. + */ + keyboardInset?: WithDefault, + keyboardRestingHeight?: WithDefault, +}>; + +export default codegenNativeComponent( + 'KeyboardAvoidingView', + { + interfaceOnly: true, + }, +) as HostComponent; diff --git a/packages/rn-tester/js/examples/KeyboardAvoidingView/KeyboardAvoidingViewExample.js b/packages/rn-tester/js/examples/KeyboardAvoidingView/KeyboardAvoidingViewExample.js index 750496f9a369..b77759beb092 100644 --- a/packages/rn-tester/js/examples/KeyboardAvoidingView/KeyboardAvoidingViewExample.js +++ b/packages/rn-tester/js/examples/KeyboardAvoidingView/KeyboardAvoidingViewExample.js @@ -22,6 +22,7 @@ import { Pressable, ScrollView, StyleSheet, + Switch, Text, TextInput, TouchableOpacity, @@ -72,6 +73,19 @@ const BEHAVIOR_DESCRIPTIONS: {[KeyboardAvoidingBehavior]: string} = { height: 'Shrinks the view height by the keyboard overlap.', }; +const NativeToggle = (props: {native: boolean, setNative: boolean => void}) => { + return ( + + experimental_native + + + ); +}; + const BehaviorPicker = (props: { behavior: KeyboardAvoidingBehavior, setBehavior: KeyboardAvoidingBehavior => void, @@ -114,11 +128,16 @@ const BehaviorPicker = (props: { const KeyboardAvoidingViewBehaviour = () => { const [modalOpen, setModalOpen] = useState(false); const [behavior, setBehavior] = useState('padding'); + const [native, setNative] = useState(false); return ( - + + @@ -305,6 +324,13 @@ const styles = StyleSheet.create({ fontWeight: '500', color: 'blue', }, + nativeToggle: { + alignItems: 'center', + flexDirection: 'row', + gap: 8, + justifyContent: 'center', + marginBottom: 8, + }, behaviorDescription: { textAlign: 'center', alignSelf: 'center', diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index 6d9ac1caa787..99fb7f575eb0 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -1705,6 +1705,26 @@ struct facebook::react::JavaTurboModule::InitParams { public std::string moduleName; } +class facebook::react::KeyboardAvoidingViewBinding : public facebook::jni::JavaClass { + public static constexpr char* const kJavaDescriptor; + public static void registerNatives(); +} + +class facebook::react::KeyboardAvoidingViewEventEmitter : public facebook::react::BaseViewEventEmitter { +} + +class facebook::react::KeyboardAvoidingViewProps : public facebook::react::HostPlatformViewProps { + public KeyboardAvoidingViewProps() = default; + public KeyboardAvoidingViewProps(const facebook::react::PropsParserContext& context, const facebook::react::KeyboardAvoidingViewProps& sourceProps, const facebook::react::RawProps& rawProps); + public bool enabled; + public facebook::react::Float keyboardInset; + public facebook::react::Float keyboardRestingHeight; + public facebook::react::Float keyboardVerticalOffset; + public facebook::react::KeyboardAvoidingViewBehavior behavior; + public virtual facebook::react::ComponentName getDiffPropsImplementationTarget() const override; + public virtual folly::dynamic getDiffProps(const facebook::react::Props* prevProps) const override; +} + class facebook::react::LayoutableShadowNode : public facebook::react::ShadowNode { public LayoutableShadowNode(const facebook::react::ShadowNode& sourceShadowNode, const facebook::react::ShadowNodeFragment& fragment); public LayoutableShadowNode(const facebook::react::ShadowNodeFragment& fragment, const facebook::react::ShadowNodeFamily::Shared& family, facebook::react::ShadowNodeTraits traits); @@ -2857,6 +2877,13 @@ enum facebook::react::Isolation { Isolate, } +enum facebook::react::KeyboardAvoidingViewBehavior { + Height, + None, + Padding, + Position, +} + enum facebook::react::LayoutConformance : uint8_t { Compatibility, Strict, diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api index d2b41ea569c8..70e24701d4c3 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api @@ -387,6 +387,7 @@ const char facebook::react::ViewComponentName[]; const facebook::react::EventTag facebook::react::EMPTY_EVENT_TAG; constexpr auto facebook::react::kSimpleThreadSafeCacheSizeCap; constexpr char facebook::react::LayoutConformanceShadowNodeComponentName[]; +constexpr char* const facebook::react::AnimationBackendKey; constexpr char* const facebook::react::TextLayoutManagerKey; constexpr facebook::react::ReactNativeVersionType facebook::react::ReactNativeVersion; constexpr facebook::react::Tag facebook::react::kNoTag; @@ -2538,6 +2539,26 @@ struct facebook::react::JsErrorHandler::ProcessedError::StackFrame { public std::string methodName; } +class facebook::react::KeyboardAvoidingViewBinding : public facebook::jni::JavaClass { + public static constexpr char* const kJavaDescriptor; + public static void registerNatives(); +} + +class facebook::react::KeyboardAvoidingViewEventEmitter : public facebook::react::BaseViewEventEmitter { +} + +class facebook::react::KeyboardAvoidingViewProps : public facebook::react::HostPlatformViewProps { + public KeyboardAvoidingViewProps() = default; + public KeyboardAvoidingViewProps(const facebook::react::PropsParserContext& context, const facebook::react::KeyboardAvoidingViewProps& sourceProps, const facebook::react::RawProps& rawProps); + public bool enabled; + public facebook::react::Float keyboardInset; + public facebook::react::Float keyboardRestingHeight; + public facebook::react::Float keyboardVerticalOffset; + public facebook::react::KeyboardAvoidingViewBehavior behavior; + public virtual facebook::react::ComponentName getDiffPropsImplementationTarget() const override; + public virtual folly::dynamic getDiffProps(const facebook::react::Props* prevProps) const override; +} + class facebook::react::LayoutAnimationCallbackWrapper { public LayoutAnimationCallbackWrapper(); public LayoutAnimationCallbackWrapper(facebook::jsi::Function&& callback); @@ -5115,6 +5136,13 @@ enum facebook::react::Isolation { Isolate, } +enum facebook::react::KeyboardAvoidingViewBehavior { + Height, + None, + Padding, + Position, +} + enum facebook::react::LayoutConformance : uint8_t { Compatibility, Strict, diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api index 57f7bc098311..7fb278abcb3e 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api @@ -1704,6 +1704,26 @@ struct facebook::react::JavaTurboModule::InitParams { public std::string moduleName; } +class facebook::react::KeyboardAvoidingViewBinding : public facebook::jni::JavaClass { + public static constexpr char* const kJavaDescriptor; + public static void registerNatives(); +} + +class facebook::react::KeyboardAvoidingViewEventEmitter : public facebook::react::BaseViewEventEmitter { +} + +class facebook::react::KeyboardAvoidingViewProps : public facebook::react::HostPlatformViewProps { + public KeyboardAvoidingViewProps() = default; + public KeyboardAvoidingViewProps(const facebook::react::PropsParserContext& context, const facebook::react::KeyboardAvoidingViewProps& sourceProps, const facebook::react::RawProps& rawProps); + public bool enabled; + public facebook::react::Float keyboardInset; + public facebook::react::Float keyboardRestingHeight; + public facebook::react::Float keyboardVerticalOffset; + public facebook::react::KeyboardAvoidingViewBehavior behavior; + public virtual facebook::react::ComponentName getDiffPropsImplementationTarget() const override; + public virtual folly::dynamic getDiffProps(const facebook::react::Props* prevProps) const override; +} + class facebook::react::LayoutableShadowNode : public facebook::react::ShadowNode { public LayoutableShadowNode(const facebook::react::ShadowNode& sourceShadowNode, const facebook::react::ShadowNodeFragment& fragment); public LayoutableShadowNode(const facebook::react::ShadowNodeFragment& fragment, const facebook::react::ShadowNodeFamily::Shared& family, facebook::react::ShadowNodeTraits traits); @@ -2850,6 +2870,13 @@ enum facebook::react::Isolation { Isolate, } +enum facebook::react::KeyboardAvoidingViewBehavior { + Height, + None, + Padding, + Position, +} + enum facebook::react::LayoutConformance : uint8_t { Compatibility, Strict, diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api index 57f7bc098311..7fb278abcb3e 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -1704,6 +1704,26 @@ struct facebook::react::JavaTurboModule::InitParams { public std::string moduleName; } +class facebook::react::KeyboardAvoidingViewBinding : public facebook::jni::JavaClass { + public static constexpr char* const kJavaDescriptor; + public static void registerNatives(); +} + +class facebook::react::KeyboardAvoidingViewEventEmitter : public facebook::react::BaseViewEventEmitter { +} + +class facebook::react::KeyboardAvoidingViewProps : public facebook::react::HostPlatformViewProps { + public KeyboardAvoidingViewProps() = default; + public KeyboardAvoidingViewProps(const facebook::react::PropsParserContext& context, const facebook::react::KeyboardAvoidingViewProps& sourceProps, const facebook::react::RawProps& rawProps); + public bool enabled; + public facebook::react::Float keyboardInset; + public facebook::react::Float keyboardRestingHeight; + public facebook::react::Float keyboardVerticalOffset; + public facebook::react::KeyboardAvoidingViewBehavior behavior; + public virtual facebook::react::ComponentName getDiffPropsImplementationTarget() const override; + public virtual folly::dynamic getDiffProps(const facebook::react::Props* prevProps) const override; +} + class facebook::react::LayoutableShadowNode : public facebook::react::ShadowNode { public LayoutableShadowNode(const facebook::react::ShadowNode& sourceShadowNode, const facebook::react::ShadowNodeFragment& fragment); public LayoutableShadowNode(const facebook::react::ShadowNodeFragment& fragment, const facebook::react::ShadowNodeFamily::Shared& family, facebook::react::ShadowNodeTraits traits); @@ -2850,6 +2870,13 @@ enum facebook::react::Isolation { Isolate, } +enum facebook::react::KeyboardAvoidingViewBehavior { + Height, + None, + Padding, + Position, +} + enum facebook::react::LayoutConformance : uint8_t { Compatibility, Strict, diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index 8de2c49759a6..3e4684ade23f 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -1257,6 +1257,9 @@ interface RCTKeyCommands : public NSObject { public virtual void unregisterKeyCommandWithInput:modifierFlags:(NSString* input, UIKeyModifierFlags flags); } +interface RCTKeyboardAvoidingViewComponentView : public RCTViewComponentView { +} + interface RCTKeyboardObserver : public RCTEventEmitter { } @@ -2786,6 +2789,9 @@ protocol RCTJSRuntimeConfiguratorProtocol { public virtual JSRuntimeFactoryRef createJSRuntimeFactory(); } +protocol RCTKeyboardAvoidingViewViewProtocol : public NSObject { +} + protocol RCTLocalizationProtocol : public NSObject { public virtual NSString* localizedString:withDescription:(NSString* oldString, NSString* description); } @@ -3986,6 +3992,19 @@ class facebook::react::JSBigString : public facebook::jsi::Buffer { public ~JSBigString() override = default; } +class facebook::react::KeyboardAvoidingViewEventEmitter : public facebook::react::BaseViewEventEmitter { +} + +class facebook::react::KeyboardAvoidingViewProps : public facebook::react::HostPlatformViewProps { + public KeyboardAvoidingViewProps() = default; + public KeyboardAvoidingViewProps(const facebook::react::PropsParserContext& context, const facebook::react::KeyboardAvoidingViewProps& sourceProps, const facebook::react::RawProps& rawProps); + public bool enabled; + public facebook::react::Float keyboardInset; + public facebook::react::Float keyboardRestingHeight; + public facebook::react::Float keyboardVerticalOffset; + public facebook::react::KeyboardAvoidingViewBehavior behavior; +} + class facebook::react::LayoutableShadowNode : public facebook::react::ShadowNode { public LayoutableShadowNode(const facebook::react::ShadowNode& sourceShadowNode, const facebook::react::ShadowNodeFragment& fragment); public LayoutableShadowNode(const facebook::react::ShadowNodeFragment& fragment, const facebook::react::ShadowNodeFamily::Shared& family, facebook::react::ShadowNodeTraits traits); @@ -5010,6 +5029,13 @@ enum facebook::react::Isolation { Isolate, } +enum facebook::react::KeyboardAvoidingViewBehavior { + Height, + None, + Padding, + Position, +} + enum facebook::react::LayoutConformance : uint8_t { Compatibility, Strict, diff --git a/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api index 6b5e45141f07..8415a67cb2b6 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api @@ -1300,6 +1300,9 @@ interface RCTKeyCommands : public NSObject { public virtual void unregisterKeyCommandWithInput:modifierFlags:(NSString* input, UIKeyModifierFlags flags); } +interface RCTKeyboardAvoidingViewComponentView : public RCTViewComponentView { +} + interface RCTKeyboardObserver : public RCTEventEmitter { } @@ -2867,6 +2870,9 @@ protocol RCTJSRuntimeConfiguratorProtocol { public virtual JSRuntimeFactoryRef createJSRuntimeFactory(); } +protocol RCTKeyboardAvoidingViewViewProtocol : public NSObject { +} + protocol RCTLocalizationProtocol : public NSObject { public virtual NSString* localizedString:withDescription:(NSString* oldString, NSString* description); } @@ -3250,6 +3256,7 @@ const char facebook::react::ViewComponentName[]; const facebook::react::EventTag facebook::react::EMPTY_EVENT_TAG; constexpr auto facebook::react::kSimpleThreadSafeCacheSizeCap; constexpr char facebook::react::LayoutConformanceShadowNodeComponentName[]; +constexpr char* const facebook::react::AnimationBackendKey; constexpr char* const facebook::react::TextLayoutManagerKey; constexpr facebook::react::ReactNativeVersionType facebook::react::ReactNativeVersion; constexpr facebook::react::Tag facebook::react::kNoTag; @@ -4752,6 +4759,19 @@ struct facebook::react::JsErrorHandler::ProcessedError::StackFrame { public std::string methodName; } +class facebook::react::KeyboardAvoidingViewEventEmitter : public facebook::react::BaseViewEventEmitter { +} + +class facebook::react::KeyboardAvoidingViewProps : public facebook::react::HostPlatformViewProps { + public KeyboardAvoidingViewProps() = default; + public KeyboardAvoidingViewProps(const facebook::react::PropsParserContext& context, const facebook::react::KeyboardAvoidingViewProps& sourceProps, const facebook::react::RawProps& rawProps); + public bool enabled; + public facebook::react::Float keyboardInset; + public facebook::react::Float keyboardRestingHeight; + public facebook::react::Float keyboardVerticalOffset; + public facebook::react::KeyboardAvoidingViewBehavior behavior; +} + class facebook::react::LayoutAnimationCallbackWrapper { public LayoutAnimationCallbackWrapper(); public LayoutAnimationCallbackWrapper(facebook::jsi::Function&& callback); @@ -7254,6 +7274,13 @@ enum facebook::react::KeyboardAppearance { Light, } +enum facebook::react::KeyboardAvoidingViewBehavior { + Height, + None, + Padding, + Position, +} + enum facebook::react::KeyboardType { ASCIICapable, ASCIICapableNumberPad, diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index e8201541b2e1..865585cf8a34 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -1257,6 +1257,9 @@ interface RCTKeyCommands : public NSObject { public virtual void unregisterKeyCommandWithInput:modifierFlags:(NSString* input, UIKeyModifierFlags flags); } +interface RCTKeyboardAvoidingViewComponentView : public RCTViewComponentView { +} + interface RCTKeyboardObserver : public RCTEventEmitter { } @@ -2778,6 +2781,9 @@ protocol RCTJSRuntimeConfiguratorProtocol { public virtual JSRuntimeFactoryRef createJSRuntimeFactory(); } +protocol RCTKeyboardAvoidingViewViewProtocol : public NSObject { +} + protocol RCTLocalizationProtocol : public NSObject { public virtual NSString* localizedString:withDescription:(NSString* oldString, NSString* description); } @@ -3977,6 +3983,19 @@ class facebook::react::JSBigString : public facebook::jsi::Buffer { public ~JSBigString() override = default; } +class facebook::react::KeyboardAvoidingViewEventEmitter : public facebook::react::BaseViewEventEmitter { +} + +class facebook::react::KeyboardAvoidingViewProps : public facebook::react::HostPlatformViewProps { + public KeyboardAvoidingViewProps() = default; + public KeyboardAvoidingViewProps(const facebook::react::PropsParserContext& context, const facebook::react::KeyboardAvoidingViewProps& sourceProps, const facebook::react::RawProps& rawProps); + public bool enabled; + public facebook::react::Float keyboardInset; + public facebook::react::Float keyboardRestingHeight; + public facebook::react::Float keyboardVerticalOffset; + public facebook::react::KeyboardAvoidingViewBehavior behavior; +} + class facebook::react::LayoutableShadowNode : public facebook::react::ShadowNode { public LayoutableShadowNode(const facebook::react::ShadowNode& sourceShadowNode, const facebook::react::ShadowNodeFragment& fragment); public LayoutableShadowNode(const facebook::react::ShadowNodeFragment& fragment, const facebook::react::ShadowNodeFamily::Shared& family, facebook::react::ShadowNodeTraits traits); @@ -4995,6 +5014,13 @@ enum facebook::react::Isolation { Isolate, } +enum facebook::react::KeyboardAvoidingViewBehavior { + Height, + None, + Padding, + Position, +} + enum facebook::react::LayoutConformance : uint8_t { Compatibility, Strict, diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index d3ff97cc59c6..3a4f9775e594 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -1257,6 +1257,9 @@ interface RCTKeyCommands : public NSObject { public virtual void unregisterKeyCommandWithInput:modifierFlags:(NSString* input, UIKeyModifierFlags flags); } +interface RCTKeyboardAvoidingViewComponentView : public RCTViewComponentView { +} + interface RCTKeyboardObserver : public RCTEventEmitter { } @@ -2786,6 +2789,9 @@ protocol RCTJSRuntimeConfiguratorProtocol { public virtual JSRuntimeFactoryRef createJSRuntimeFactory(); } +protocol RCTKeyboardAvoidingViewViewProtocol : public NSObject { +} + protocol RCTLocalizationProtocol : public NSObject { public virtual NSString* localizedString:withDescription:(NSString* oldString, NSString* description); } @@ -3985,6 +3991,19 @@ class facebook::react::JSBigString : public facebook::jsi::Buffer { public ~JSBigString() override = default; } +class facebook::react::KeyboardAvoidingViewEventEmitter : public facebook::react::BaseViewEventEmitter { +} + +class facebook::react::KeyboardAvoidingViewProps : public facebook::react::HostPlatformViewProps { + public KeyboardAvoidingViewProps() = default; + public KeyboardAvoidingViewProps(const facebook::react::PropsParserContext& context, const facebook::react::KeyboardAvoidingViewProps& sourceProps, const facebook::react::RawProps& rawProps); + public bool enabled; + public facebook::react::Float keyboardInset; + public facebook::react::Float keyboardRestingHeight; + public facebook::react::Float keyboardVerticalOffset; + public facebook::react::KeyboardAvoidingViewBehavior behavior; +} + class facebook::react::LayoutableShadowNode : public facebook::react::ShadowNode { public LayoutableShadowNode(const facebook::react::ShadowNode& sourceShadowNode, const facebook::react::ShadowNodeFragment& fragment); public LayoutableShadowNode(const facebook::react::ShadowNodeFragment& fragment, const facebook::react::ShadowNodeFamily::Shared& family, facebook::react::ShadowNodeTraits traits); @@ -5003,6 +5022,13 @@ enum facebook::react::Isolation { Isolate, } +enum facebook::react::KeyboardAvoidingViewBehavior { + Height, + None, + Padding, + Position, +} + enum facebook::react::LayoutConformance : uint8_t { Compatibility, Strict, diff --git a/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api index c06d3fc44d2a..fb3c464eaa64 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api @@ -84,6 +84,7 @@ const char facebook::react::ViewComponentName[]; const facebook::react::EventTag facebook::react::EMPTY_EVENT_TAG; constexpr auto facebook::react::kSimpleThreadSafeCacheSizeCap; constexpr char facebook::react::LayoutConformanceShadowNodeComponentName[]; +constexpr char* const facebook::react::AnimationBackendKey; constexpr char* const facebook::react::TextLayoutManagerKey; constexpr facebook::react::ReactNativeVersionType facebook::react::ReactNativeVersion; constexpr facebook::react::Tag facebook::react::kNoTag;