Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

onLayout stops firing after rotation on iOS when Animated.View is used #6684

Open
mhoran opened this issue Nov 10, 2024 · 8 comments · May be fixed by #6739
Open

onLayout stops firing after rotation on iOS when Animated.View is used #6684

mhoran opened this issue Nov 10, 2024 · 8 comments · May be fixed by #6739
Labels
Platform: iOS This issue is specific to iOS Repro provided A reproduction with a snippet of code, snack or repo is provided

Comments

@mhoran
Copy link

mhoran commented Nov 10, 2024

Description

Since upgrading to React Native 0.76 (via Expo 52) I've been trying to track down an issue with KeyboardAvoidingView on iPadOS. Despite the resolution of a related bug in React Native, the issue persisted in a project using react-native-reanimated and the New Architecture.

After rotation, the KeyboardAvoidingView eventually stops updating properly. The root cause is that the onLayout handler stops being called. This causes _relativeKeyboardHeight to calculate the wrong height due to a stale frame height. As such, the height is set to 0 and no padding is applied.

The onLayout bug is not limited to KeyboardAvoidingView. It seems that any React Native project using react-native-reanimated will eventually stop firing onLayout after rotation on iPadOS iOS. This issue is limited to the New Architecture. If I switch to the old architecture, the issue does not reproduce.

The provided repro shows this behavior. When running in a simulator or device, rotating the screen will show the keyboard either pushing up the main content with too much padding (simulator) or hiding the input text box completely (real device). The console.log also shows an extra onLayout being called when an Animated.View is present in the DOM, right before onLayout stops firing. Removing the Animated.View prevents this issue from occurring, and there is only a single onLayout call during rotation.

I thought this issue may have been Expo specific. However, after further testing and isolation of the reproducer, I found that it can be reproduced using React Native without Expo as well. I also isolated the issue to react-native-reanimated, initially believing it was due to an interaction with react-native-gesture-handler.

Steps to reproduce

  1. Launch the reproducer on an iPad simulator with npm run ios
  2. Focus the text input at the bottom
  3. Open the on screen keyboard
  4. Rotate screen
  5. Note that onLayout has been printed twice in the log
  6. Rotate the screen again
  7. Note that the KeyboardAvoidingView has applied incorrect padding and that onLayout was not printed in the log

Snack or a link to a repository

https://github.com/mhoran/keyboard-avoiding-view-repro/

Reanimated version

3.16.1

React Native version

0.76.1

Platforms

iOS

JavaScript runtime

Hermes

Workflow

React Native

Architecture

Fabric (New Architecture)

Build type

Debug app & dev bundle

Device

iOS simulator

Device model

iPad Air 11-inch (M2)

Acknowledgements

Yes

Screenshots

Prior to rotation After rotation
Simulator Screenshot - iPad Air 11-inch (M2) - 2024-11-10 at 11 50 56 Simulator Screenshot - iPad Air 11-inch (M2) - 2024-11-10 at 11 51 03
@quentez
Copy link

quentez commented Nov 15, 2024

This does not seem limited to iPadOS as I'm seeing similar issues on iOS using KeyboardAvoidingView. Reproduction here: https://github.com/quentez/keyboard-bug-repro

@mhoran
Copy link
Author

mhoran commented Nov 19, 2024

I've tracked the issue down to the following line of code:

When the layout event is dispatched from React Native, it is intercepted by react-native-reanimated. I traced the event from the ShadowTree:
https://github.com/facebook/react-native/blob/v0.76.2/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp#L532
to the BaseViewEventEmitter:
https://github.com/facebook/react-native/blob/v0.76.2/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewEventEmitter.cpp#L82-L112
and see that it is dispatched by the EventDispatcher:
https://github.com/facebook/react-native/blob/v0.76.2/packages/react-native/ReactCommon/react/renderer/core/EventDispatcher.cpp#L43.

However, the onLayout event never fires on the JavaScript side.

I traced the event through react-native-reanimated is picked up by the event listener:

auto eventListener =
std::make_shared<facebook::react::EventListener>([nativeReanimatedModule](const RawEvent &rawEvent) {
if (!RCTIsMainQueue()) {
// event listener called on the JS thread, let's ignore this event
// as we cannot safely access worklet runtime here
// and also we don't care about topLayout events
return false;
}
return nativeReanimatedModule->handleRawEvent(rawEvent, CACurrentMediaTime() * 1000);
});

I see that the event listener receives the onLayout event and passes it to handleRawEvent. However, when line 617 executes, the onLayout event is swallowed and does not execute on the JavaScript side. Returning before line 617 causes the onLayout event to fire as expected.

Interestingly, other events such as onContentSizeChange do make it through. It just seems to be something about the onLayout event. Also, as mentioned, I see a spurious onLayout event fire when the above code is executed.

@mhoran mhoran changed the title onLayout stops firing after rotation on iPadOS when Animated.View is used onLayout stops firing after rotation on iOS when Animated.View is used Nov 19, 2024
@mhoran
Copy link
Author

mhoran commented Nov 19, 2024

I've confirmed that the onLayout issue is not limited to iPad and have updated this issue accordingly. Oddly, I cannot reproduce the KeyboardAvoidingView issue myself on an iPad simulator, but I do see that onLayout does not fire.

I've further simplified the repro on the react-native branch. The view consists only of an outer view with onLayout handler and an inner Animated.View. Only an initial onLayout log message will be printed when run in a simulator. Subsequent device rotation will not print anything to the console log.

In the broken state, I see two attempts to dispatch layout events by BaseViewEventEmitter::onLayout. One is a duplicate and as such bails out at https://github.com/facebook/react-native/blob/v0.76.2/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewEventEmitter.cpp#L101.

Returning prior to line 617 in NativeReanimatedModule I see only a single onLayout event emitted and the console log prints as expected. Returning on line 618 I see the broken behavior, and two attempts to dispatch the onLayout event.

@mhoran
Copy link
Author

mhoran commented Nov 19, 2024

Okay, I figured it out. First, why does this only happen with onLayout? Because onLayout has a guard that prevents the event from being dispatched multiple times. See https://github.com/facebook/react-native/blob/v0.76.2/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewEventEmitter.cpp#L93-L95.

I noticed that, while react-native-reanimated was receiving the onLayout event as expected, a second event was being dispatched from React Native, but then dropped before reaching JavaScript. This is because of the above guard, ultimately setting the dispatched value to jsi::Value::null(), which bails out in React Native here: https://github.com/facebook/react-native/blob/v0.76.2/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp#L140-L142.

When asJSIValue is called in


the lambda defined in the BaseViewEventEmitter is evaluated (see https://github.com/facebook/react-native/blob/v0.76.2/packages/react-native/ReactCommon/react/renderer/core/ValueFactoryEventPayload.cpp#L16). react-native-reanimated receives the layout value, but since the layoutEventState->wasDispatched flips to true, when React Native receives the event it is ignored.

It seems that the wasDispatched guard prevents the onLayout event from ever being dispatched by the React Native event dispatcher, since reading the value evaluates the lambda and sets this flag.

I'm not sure how best to address this issue. It looks like this change was introduced in fe587cf by @piaskowyk, @tomekzaw and @michalmaka; perhaps they can weigh in.

mhoran added a commit to mhoran/react-native-reanimated that referenced this issue Nov 20, 2024
Previously, NativeReanimatedModule::handleRawEvent would intercept all
events received by the event listener. This resulted in an issue where
onLayout would not fire in JS on the New Architecture.

Instead, only intercept events with waiting handlers. This prevents
asJSIValue from being called on the Reanimated event loop and allows
onLayout to bubble up in JS.

See
https://github.com/facebook/react-native/blob/v0.76.2/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewEventEmitter.cpp#L82-L112,
which prevents onLayout from being dispatched more than once.

asJSIValue evaluates the lambda above in
https://github.com/facebook/react-native/blob/v0.76.2/packages/react-native/ReactCommon/react/renderer/core/ValueFactoryEventPayload.cpp#L16.

Fixes software-mansion#6684
@mhoran mhoran linked a pull request Nov 20, 2024 that will close this issue
@mhoran
Copy link
Author

mhoran commented Nov 20, 2024

I've opened a PR that resolves this issue and would appreciate feedback.

@tomekzaw
Copy link
Member

@mhoran Thanks for opening the PR, we'll review it shortly

mhoran added a commit to mhoran/react-native-reanimated that referenced this issue Nov 21, 2024
Previously, NativeReanimatedModule::handleRawEvent would intercept all
events received by the event listener. This resulted in an issue where
onLayout would not fire in JS on the New Architecture.

Instead, only intercept events with waiting handlers. This prevents
asJSIValue from being called on the Reanimated event loop and allows
onLayout to bubble up in JS.

See
https://github.com/facebook/react-native/blob/v0.76.2/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewEventEmitter.cpp#L82-L112,
which prevents onLayout from being dispatched more than once.

asJSIValue evaluates the lambda above in
https://github.com/facebook/react-native/blob/v0.76.2/packages/react-native/ReactCommon/react/renderer/core/ValueFactoryEventPayload.cpp#L16.

Fixes software-mansion#6684
mhoran added a commit to mhoran/react-native-reanimated that referenced this issue Nov 21, 2024
Previously, NativeReanimatedModule::handleRawEvent would intercept all
events received by the event listener. This resulted in an issue where
onLayout would not fire in JS on the New Architecture.

Instead, only intercept events with waiting handlers. This prevents
asJSIValue from being called on the Reanimated event loop and allows
onLayout to bubble up in JS.

See
https://github.com/facebook/react-native/blob/v0.76.2/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewEventEmitter.cpp#L82-L112,
which prevents onLayout from being dispatched more than once.

asJSIValue evaluates the lambda above in
https://github.com/facebook/react-native/blob/v0.76.2/packages/react-native/ReactCommon/react/renderer/core/ValueFactoryEventPayload.cpp#L16.

Fixes software-mansion#6684
mhoran added a commit to mhoran/react-native-reanimated that referenced this issue Nov 22, 2024
Previously, NativeReanimatedModule::handleRawEvent would intercept all
events received by the event listener. This resulted in an issue where
onLayout would not fire in JS on the New Architecture.

Instead, only intercept events with waiting handlers. This prevents
asJSIValue from being called on the Reanimated event loop and allows
onLayout to bubble up in JS.

See
https://github.com/facebook/react-native/blob/v0.76.2/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewEventEmitter.cpp#L82-L112,
which prevents onLayout from being dispatched more than once.

asJSIValue evaluates the lambda above in
https://github.com/facebook/react-native/blob/v0.76.2/packages/react-native/ReactCommon/react/renderer/core/ValueFactoryEventPayload.cpp#L16.

Fixes software-mansion#6684
@moritzlang
Copy link

Any update on this? This issue causes KeyboardAvoidingView not animating for Expo SDK 52 projects

@mhoran
Copy link
Author

mhoran commented Dec 17, 2024

The lack of animation is likely due to #6751. This issue causes the keyboard avoiding view to stop avoiding the keyboard when the device is rotated, or some other layout event changes the size of the frame (e.g. window resized using Stage Manager, etc.)

mhoran added a commit to mhoran/react-native-reanimated that referenced this issue Dec 18, 2024
Previously, NativeReanimatedModule::handleRawEvent would intercept all
events received by the event listener. This resulted in an issue where
onLayout would not fire in JS on the New Architecture.

Instead, only intercept events with waiting handlers. This prevents
asJSIValue from being called on the Reanimated event loop and allows
onLayout to bubble up in JS.

See
https://github.com/facebook/react-native/blob/v0.76.2/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewEventEmitter.cpp#L82-L112,
which prevents onLayout from being dispatched more than once.

asJSIValue evaluates the lambda above in
https://github.com/facebook/react-native/blob/v0.76.2/packages/react-native/ReactCommon/react/renderer/core/ValueFactoryEventPayload.cpp#L16.

Fixes software-mansion#6684
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Platform: iOS This issue is specific to iOS Repro provided A reproduction with a snippet of code, snack or repo is provided
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants