Skip to content

Conversation

@kmichalikk
Copy link
Collaborator

@kmichalikk kmichalikk commented Oct 14, 2025

Fixes #2933

Description

This PR fixes the bug on android that results in Screen orientation being calculated incorrectly for some time after the physical device orientation has changed. This bug seems to happen due to asynchronous nature of Screen state update emitted to react from native layer. The state contains a size that is set on the Screen frame.

The bug is more visible on slower devices, because with longer processing of consecutive commits, the delay is longer.

It can be noticed on a trivial application (some text + different background color for screen and content view) with some heavy computations artificially added to JS.

More detailed description:

In RNSScreenComponentDescriptor, on Android specifically, we call setSize() on the Screen view to hardcode its dimensions. This is needed to account for screen header size, of which React has no idea. It is however the core reason for the bug. The size which we set is outdated. Only after the asynchronous state update from native side is processed, it is corrected. This can take some time. If we comment the code for handling the header size, this problem disappears. But we cannot.

Since the code is staying here, we need to work with it. We agreed on the special value of frameSize = { 0, 0 }, for which we set YogaNode size to Undefined. This forces Yoga to recalculate the layout. It can do so because the parent view is correctly sized at this point (no calls to setSize() with outdated values). We cannot do this manually, because the descriptor doesn't have access to parent nodes. Using the commit hook, we detect the orientation change (it can compare old and new RootShadowNode, which have the correct dimensions) and set the frameSize for the ShadowNode which is passed to the descriptor afterwards.

Another problem is that the hook detects the change only once, for one instance of the shadow node. There could be more instances processed before the state is corrected. Luckily, React uses the same state instance for each shadow node instance between the updates. We take advantage of that, once reset to 0, the frameSize stays that way until some other updates changes the instance. Since we only store the screen offset and frameSize in the state, this works good enough. But it's still pretty hacky.

Changes

Description above. A new feature flag is added. When set, we register a new commit hook that checks if screen orientation changed & sets frameSize to 0. It is detected in Screen descriptor and Yoga is forced to recalculate the layout.

Before / After

before after
android-layout-bad.mov
android-layout-better.mov

Testing

Use Test2933. You can take a look at the "checkerboard" test from react-native-reanimated.

@kmichalikk kmichalikk marked this pull request as draft October 14, 2025 11:39
@kmichalikk kmichalikk force-pushed the @kmichalikk/layout-shift-on-orientation-change branch from 3209320 to 44345e5 Compare October 16, 2025 13:55
@kmichalikk kmichalikk force-pushed the @kmichalikk/layout-shift-on-orientation-change branch from 8eb4111 to 60ed155 Compare November 18, 2025 11:41
@kmichalikk kmichalikk marked this pull request as ready for review November 18, 2025 13:01
@kmichalikk kmichalikk self-assigned this Nov 19, 2025
Copy link
Member

@kkafar kkafar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good job! I've left few remarks & questions. Please answer them.

void adopt(ShadowNode &shadowNode) const override {
react_native_assert(dynamic_cast<RNSScreenShadowNode *>(&shadowNode));
auto &screenShadowNode = static_cast<RNSScreenShadowNode &>(shadowNode);
auto &screenShadowNode = dynamic_cast<RNSScreenShadowNode &>(shadowNode);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why change here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad, changed it back to static_cast. After consulting, I also changed the cast style in RNSScreenProps

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +30 to +39
if (contextContainer_) {
auto fabricUIManager =
contextContainer_
->at<jni::alias_ref<facebook::react::JFabricUIManager::javaobject>>(
"FabricUIManager");
fabricUIManager->getBinding()
->getScheduler()
->getUIManager()
->unregisterCommitHook(*this);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We never cleanup reference to the contextContainer. Shall we do it here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we? doesn't the smart pointer handle this on its own?

const ShadowTreeCommitOptions & /*commitOptions*/) noexcept override;

private:
std::shared_ptr<const ContextContainer> contextContainer_;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the decision of retaining the context container here intentional?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the commit hook needs to unregister in the destructor, I need the ref to the container

@kmichalikk kmichalikk requested a review from kkafar November 20, 2025 16:50
@kkafar
Copy link
Member

kkafar commented Nov 20, 2025

@kkafar

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[android] layout shift bug on new architecture

4 participants