diff --git a/FabricExample/ios/Podfile.lock b/FabricExample/ios/Podfile.lock index ac1dccc996..3c6e916e5a 100644 --- a/FabricExample/ios/Podfile.lock +++ b/FabricExample/ios/Podfile.lock @@ -3184,7 +3184,7 @@ SPEC CHECKSUMS: RNCMaskedView: 63268de1986a098b5f4d1fb5b1bc1e97fade0aee RNGestureHandler: 4f7cc97a71d4fe0fcba38c94acdd969f5f17c91c RNReactNativeHapticFeedback: 63aa39dbd6ef56e9de688210c5761d4007927a7e - RNReanimated: f2cdef8c5ec70e440498b949f9af3ea39f70fcec + RNReanimated: aabfea4a99566babeedbd3e33eb234b9756932bd RNScreens: 74985ca8e102294a60cec7513fa84c936fa0b20b RNWorklets: ab7740c2a152f77ff76a40d52be860d8f128fab1 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 diff --git a/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardControllerModule.kt b/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardControllerModule.kt index 5728c21833..811fdb9fd1 100644 --- a/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardControllerModule.kt +++ b/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardControllerModule.kt @@ -1,5 +1,6 @@ package com.reactnativekeyboardcontroller +import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.reactnativekeyboardcontroller.modules.KeyboardControllerModuleImpl @@ -33,6 +34,13 @@ class KeyboardControllerModule( module.setFocusTo(direction) } + override fun viewPositionInWindow( + viewTag: Double, + promise: Promise, + ) { + module.viewPositionInWindow(viewTag, promise) + } + override fun addListener(eventName: String?) { // Required for RN built-in Event Emitter Calls. } diff --git a/android/src/main/java/com/reactnativekeyboardcontroller/extensions/ReactContext.kt b/android/src/main/java/com/reactnativekeyboardcontroller/extensions/ReactContext.kt index a940750b20..52c966458b 100644 --- a/android/src/main/java/com/reactnativekeyboardcontroller/extensions/ReactContext.kt +++ b/android/src/main/java/com/reactnativekeyboardcontroller/extensions/ReactContext.kt @@ -3,6 +3,18 @@ package com.reactnativekeyboardcontroller.extensions import android.view.View import android.view.ViewGroup import com.facebook.react.bridge.ReactContext +import com.facebook.react.uimanager.UIManagerHelper +import com.facebook.react.uimanager.common.UIManagerType +import com.facebook.react.uimanager.events.EventDispatcher +import com.reactnativekeyboardcontroller.BuildConfig + +private val archType = if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) UIManagerType.FABRIC else UIManagerType.DEFAULT + +val ReactContext.uiManager + get() = UIManagerHelper.getUIManager(this, archType) + +val ReactContext.eventDispatcher: EventDispatcher? + get() = UIManagerHelper.getEventDispatcher(this, archType) val ReactContext.rootView: View? get() = diff --git a/android/src/main/java/com/reactnativekeyboardcontroller/modal/ModalAttachedWatcher.kt b/android/src/main/java/com/reactnativekeyboardcontroller/modal/ModalAttachedWatcher.kt index 32cbf45260..a00689bbf2 100644 --- a/android/src/main/java/com/reactnativekeyboardcontroller/modal/ModalAttachedWatcher.kt +++ b/android/src/main/java/com/reactnativekeyboardcontroller/modal/ModalAttachedWatcher.kt @@ -4,15 +4,14 @@ import android.view.ViewGroup import android.view.WindowManager import androidx.core.view.ViewCompat import com.facebook.react.uimanager.ThemedReactContext -import com.facebook.react.uimanager.UIManagerHelper -import com.facebook.react.uimanager.common.UIManagerType import com.facebook.react.uimanager.events.Event import com.facebook.react.uimanager.events.EventDispatcherListener import com.facebook.react.views.modal.ReactModalHostView import com.facebook.react.views.view.ReactViewGroup -import com.reactnativekeyboardcontroller.BuildConfig import com.reactnativekeyboardcontroller.constants.Keyboard +import com.reactnativekeyboardcontroller.extensions.eventDispatcher import com.reactnativekeyboardcontroller.extensions.removeSelf +import com.reactnativekeyboardcontroller.extensions.uiManager import com.reactnativekeyboardcontroller.listeners.KeyboardAnimationCallback import com.reactnativekeyboardcontroller.listeners.KeyboardAnimationCallbackConfig import com.reactnativekeyboardcontroller.log.Logger @@ -26,9 +25,8 @@ class ModalAttachedWatcher( private val config: KeyboardAnimationCallbackConfig, private var callback: () -> KeyboardAnimationCallback?, ) : EventDispatcherListener { - private val archType = if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) UIManagerType.FABRIC else UIManagerType.DEFAULT - private val uiManager = UIManagerHelper.getUIManager(reactContext.reactApplicationContext, archType) - private val eventDispatcher = UIManagerHelper.getEventDispatcher(reactContext.reactApplicationContext, archType) + private val uiManager = reactContext.uiManager + private val eventDispatcher = reactContext.eventDispatcher override fun onEventDispatch(event: Event<*>) { if (event.eventName != MODAL_SHOW_EVENT) { diff --git a/android/src/main/java/com/reactnativekeyboardcontroller/modules/KeyboardControllerModuleImpl.kt b/android/src/main/java/com/reactnativekeyboardcontroller/modules/KeyboardControllerModuleImpl.kt index e2ec849d68..0a5b7262e2 100644 --- a/android/src/main/java/com/reactnativekeyboardcontroller/modules/KeyboardControllerModuleImpl.kt +++ b/android/src/main/java/com/reactnativekeyboardcontroller/modules/KeyboardControllerModuleImpl.kt @@ -5,8 +5,13 @@ import android.os.Build import android.view.View import android.view.WindowManager import android.view.inputmethod.InputMethodManager +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.UiThreadUtil +import com.reactnativekeyboardcontroller.extensions.dp +import com.reactnativekeyboardcontroller.extensions.screenLocation +import com.reactnativekeyboardcontroller.extensions.uiManager import com.reactnativekeyboardcontroller.interactive.KeyboardAnimationController import com.reactnativekeyboardcontroller.traversal.FocusedInputHolder import com.reactnativekeyboardcontroller.traversal.ViewHierarchyNavigator @@ -14,6 +19,7 @@ import com.reactnativekeyboardcontroller.traversal.ViewHierarchyNavigator class KeyboardControllerModuleImpl( private val mReactContext: ReactApplicationContext, ) { + private val uiManager = mReactContext.uiManager private val controller = KeyboardAnimationController() private val mDefaultMode: Int = getCurrentMode() @@ -77,6 +83,26 @@ class KeyboardControllerModuleImpl( } } + fun viewPositionInWindow( + viewTag: Double, + promise: Promise, + ) { + UiThreadUtil.runOnUiThread { + val view = uiManager?.resolveView(viewTag.toInt()) + if (view == null) { + promise.reject("E_VIEW_NOT_FOUND", "Could not find view for tag") + return@runOnUiThread + } + val location = view.screenLocation + val map = Arguments.createMap() + map.putDouble("x", location[0].toFloat().dp) + map.putDouble("y", location[1].toFloat().dp) + map.putDouble("width", view.width.toFloat().dp) + map.putDouble("height", view.height.toFloat().dp) + promise.resolve(map) + } + } + private fun setSoftInputMode(mode: Int) { UiThreadUtil.runOnUiThread { if (getCurrentMode() != mode) { diff --git a/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardControllerModule.kt b/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardControllerModule.kt index 6c59c9cf04..f54f59566e 100644 --- a/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardControllerModule.kt +++ b/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardControllerModule.kt @@ -1,5 +1,6 @@ package com.reactnativekeyboardcontroller +import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactMethod @@ -40,6 +41,14 @@ class KeyboardControllerModule( module.setFocusTo(direction) } + @ReactMethod + fun viewPositionInWindow( + viewTag: Double, + promise: Promise, + ) { + module.viewPositionInWindow(viewTag, promise) + } + @Suppress("detekt:UnusedParameter") @ReactMethod fun addListener(eventName: String?) { diff --git a/ios/KeyboardControllerModule.mm b/ios/KeyboardControllerModule.mm index 60ca396b27..41d0a81ca1 100644 --- a/ios/KeyboardControllerModule.mm +++ b/ios/KeyboardControllerModule.mm @@ -23,6 +23,9 @@ #endif #import +#ifndef RCT_NEW_ARCH_ENABLED +#import +#endif #ifdef RCT_NEW_ARCH_ENABLED @interface KeyboardController () @@ -95,6 +98,39 @@ - (void)setFocusTo:(NSString *)direction [ViewHierarchyNavigator setFocusToDirection:direction]; } +#ifdef RCT_NEW_ARCH_ENABLED +- (void)viewPositionInWindow:(double)viewTag + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject +#else +RCT_EXPORT_METHOD(viewPositionInWindow + : (nonnull NSNumber *)viewTag resolve + : (RCTPromiseResolveBlock)resolve reject + : (RCTPromiseRejectBlock)reject) +#endif +{ + dispatch_async(dispatch_get_main_queue(), ^{ + UIView *view = nil; +#ifdef RCT_NEW_ARCH_ENABLED + NSInteger tag = (NSInteger)viewTag; + view = [UIApplication.sharedApplication.activeWindow viewWithTag:tag]; +#else + view = [self.bridge.uiManager viewForReactTag:viewTag]; +#endif + if (!view || !view.superview) { + reject(@"E_VIEW_NOT_FOUND", @"Could not find view for tag", nil); + return; + } + CGRect frame = [view.superview convertRect:view.frame toView:nil]; + resolve(@{ + @"x" : @(frame.origin.x), + @"y" : @(frame.origin.y), + @"width" : @(frame.size.width), + @"height" : @(frame.size.height), + }); + }); +} + + (KeyboardController *)shared { return shared; diff --git a/ios/extensions/UIApplication.swift b/ios/extensions/UIApplication.swift index c226dff53e..4a188c1009 100644 --- a/ios/extensions/UIApplication.swift +++ b/ios/extensions/UIApplication.swift @@ -9,7 +9,7 @@ import Foundation import UIKit public extension UIApplication { - var activeWindow: UIWindow? { + @objc var activeWindow: UIWindow? { if #available(iOS 13.0, *) { for scene in connectedScenes { if scene.activationState == .foregroundActive, diff --git a/jest/index.js b/jest/index.js index f1f79d5228..7ca5a79610 100644 --- a/jest/index.js +++ b/jest/index.js @@ -87,6 +87,9 @@ const mock = { setFocusTo: jest.fn(), isVisible: jest.fn().mockReturnValue(false), state: jest.fn().mockReturnValue(lastKeyboardEvent), + viewPositionInWindow: jest + .fn() + .mockReturnValue(Promise.resolve({ x: 0, y: 0, width: 0, height: 0 })), }, AndroidSoftInputModes: { SOFT_INPUT_ADJUST_NOTHING: 48, diff --git a/src/bindings.ts b/src/bindings.ts index 535ab53d9c..65f8c56f2d 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -22,6 +22,8 @@ export const KeyboardControllerNative: KeyboardControllerNativeModule = { preload: NOOP, dismiss: NOOP, setFocusTo: NOOP, + viewPositionInWindow: () => + Promise.resolve({ x: 0, y: 0, width: 0, height: 0 }), addListener: NOOP, removeListeners: NOOP, }; diff --git a/src/components/KeyboardAvoidingView/index.tsx b/src/components/KeyboardAvoidingView/index.tsx index ae0c78b962..ec2ee2961c 100644 --- a/src/components/KeyboardAvoidingView/index.tsx +++ b/src/components/KeyboardAvoidingView/index.tsx @@ -8,7 +8,9 @@ import Reanimated, { useSharedValue, } from "react-native-reanimated"; +import { KeyboardControllerNative } from "../../bindings"; import { useWindowDimensions } from "../../hooks"; +import { findNodeHandle } from "../../utils/findNodeHandle"; import useCombinedRef from "../hooks/useCombinedRef"; import { useKeyboardAnimation, useTranslateAnimation } from "./hooks"; @@ -131,18 +133,33 @@ const KeyboardAvoidingView = forwardRef< ); const onLayout = useCallback>( (e) => { + onLayoutProps?.(e); + const layout = e.nativeEvent.layout; if (automaticOffset) { - // ref is always set here — onLayout only fires after mount - internalRef.current?.measureInWindow((x, y) => { - runOnUI(onLayoutWorklet)({ ...layout, x, y }); - }); - } else { - runOnUI(onLayoutWorklet)(layout); + const tag = findNodeHandle(internalRef.current); + + if (tag !== null) { + // Use native `viewPositionInWindow` to get true screen-absolute coordinates. + // This fixes current RN bugs: + // - https://github.com/facebook/react-native/pull/56062 + // - https://github.com/facebook/react-native/pull/56056 + return KeyboardControllerNative.viewPositionInWindow(tag) + .then((position) => { + runOnUI(onLayoutWorklet)({ + ...layout, + x: position.x, + y: position.y, + }); + }) + .catch(() => { + runOnUI(onLayoutWorklet)(layout); + }); + } } - onLayoutProps?.(e); + return runOnUI(onLayoutWorklet)(layout); }, [onLayoutProps, automaticOffset], ); diff --git a/src/specs/NativeKeyboardController.ts b/src/specs/NativeKeyboardController.ts index aa3a977db8..b0acdade2e 100644 --- a/src/specs/NativeKeyboardController.ts +++ b/src/specs/NativeKeyboardController.ts @@ -11,6 +11,7 @@ export interface Spec extends TurboModule { preload(): void; dismiss(keepFocus: boolean, animated: boolean): void; setFocusTo(direction: string): void; + viewPositionInWindow(viewTag: number): Promise; // event emitter addListener: (eventName: string) => void; diff --git a/src/types/module.ts b/src/types/module.ts index 45fe2d1c85..86117d6b9a 100644 --- a/src/types/module.ts +++ b/src/types/module.ts @@ -109,6 +109,12 @@ export type KeyboardControllerModule = { */ state: () => KeyboardEventData; }; +export type ViewPositionInWindowResult = { + x: number; + y: number; + width: number; + height: number; +}; export type KeyboardControllerNativeModule = { // android only setDefaultMode: () => void; @@ -118,6 +124,9 @@ export type KeyboardControllerNativeModule = { // all platforms dismiss: (keepFocus: boolean, animated: boolean) => void; setFocusTo: (direction: Direction) => void; + viewPositionInWindow: ( + viewTag: number, + ) => Promise; // native event module stuff addListener: (eventName: string) => void; removeListeners: (count: number) => void;