Skip to content

fix(Android): layout shift on orientation change#3295

Merged
kmichalikk merged 8 commits into
mainfrom
@kmichalikk/layout-shift-on-orientation-change
Nov 27, 2025
Merged

fix(Android): layout shift on orientation change#3295
kmichalikk merged 8 commits into
mainfrom
@kmichalikk/layout-shift-on-orientation-change

Conversation

@kmichalikk

@kmichalikk kmichalikk commented Oct 14, 2025

Copy link
Copy Markdown
Contributor

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

@kkafar kkafar left a comment

Copy link
Copy Markdown
Member

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.

Comment thread common/cpp/react/renderer/components/rnscreens/RNSScreenComponentDescriptor.h Outdated
Comment thread common/cpp/react/renderer/components/rnscreens/RNSScreenComponentDescriptor.h Outdated
Comment thread common/cpp/react/renderer/components/rnscreens/RNSScreenShadowNodeCommitHook.cpp Outdated
Comment thread common/cpp/react/renderer/components/rnscreens/RNSScreenShadowNodeCommitHook.cpp Outdated
Comment thread common/cpp/react/renderer/components/rnscreens/RNSScreenShadowNodeCommitHook.cpp Outdated
Comment thread common/cpp/react/renderer/components/rnscreens/RNSScreenShadowNodeCommitHook.cpp Outdated
Comment thread common/cpp/react/renderer/components/rnscreens/RNSScreenShadowNodeCommitHook.cpp Outdated
Comment thread common/cpp/react/renderer/components/rnscreens/RNSScreenShadowNodeCommitHook.h Outdated
Comment thread common/cpp/react/renderer/components/rnscreens/RNSScreenShadowNodeCommitHook.h Outdated
Comment thread common/cpp/react/renderer/components/rnscreens/RNSScreenShadowNodeCommitHook.h Outdated
@kmichalikk kmichalikk requested a review from kkafar November 20, 2025 16:50
@kkafar

kkafar commented Nov 20, 2025

Copy link
Copy Markdown
Member

@kkafar

@kkafar kkafar left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Few last remarks

Comment thread common/cpp/react/renderer/components/rnscreens/RNSScreenComponentDescriptor.h Outdated
Comment thread common/cpp/react/renderer/components/rnscreens/RNSScreenShadowNodeCommitHook.cpp Outdated
Comment thread common/cpp/react/renderer/components/rnscreens/RNSScreenShadowNodeCommitHook.cpp Outdated
Comment thread common/cpp/react/renderer/components/rnscreens/RNSScreenShadowNodeCommitHook.cpp Outdated
Comment thread common/cpp/react/renderer/components/rnscreens/RNSScreenShadowNodeCommitHook.h Outdated
Comment thread common/cpp/react/renderer/components/rnscreens/RNSScreenShadowNodeCommitHook.h Outdated
@kmichalikk kmichalikk requested a review from kkafar November 25, 2025 11:01

@kkafar kkafar left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks good. I have two more nitpicks. After that we can land this PR.
Great job.

Comment thread common/cpp/react/renderer/components/rnscreens/RNSScreenShadowNodeCommitHook.h Outdated
Comment thread src/flags.ts Outdated
@kmichalikk kmichalikk requested a review from kkafar November 26, 2025 12:19
@kkafar kkafar changed the title Fix: layout shift on orientation change fix(Android): layout shift on orientation change Nov 26, 2025

@kkafar kkafar left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks good now. Great job!

@kmichalikk kmichalikk merged commit 91b17d5 into main Nov 27, 2025
9 checks passed
@kmichalikk kmichalikk deleted the @kmichalikk/layout-shift-on-orientation-change branch November 27, 2025 11:29
@t0maboro t0maboro mentioned this pull request Dec 2, 2025
8 tasks
kkafar added a commit that referenced this pull request Dec 12, 2025
…#3475)

Fixes #3476

## Description

The destructor of `RNSScreenShadowNodeCommitHook` crashes during hot
reload because it tries to access `FabricUIManagerBinding` after the
React instance has been destroyed.

## Root Cause

The crash occurs because:
1. Hot reload destroys the React instance
2. `FabricUIManagerBinding` becomes null
3. The `CommitHook` destructor tries to unregister itself
4. `getUIManagerFromSharedContext()` calls `getBinding()` which accesses
null
5. JNI crashes when accessing the null object's field

This bug was introduced in #3295 (commit 91b17d5).

## Changes

Remove the unregister call from the destructor. This is safe because the
`UIManager` automatically cleans up all registered hooks when it's
destroyed, so manual unregistration is unnecessary.

## Test Code and Steps to Reproduce

1. Create any app with `@react-navigation/native-stack`
2. Enable New Architecture on Android
3. Run the app in development mode
4. Press `r` to hot reload
5. **Before fix**: App crashes with SIGABRT
6. **After fix**: App reloads successfully

## Crash Log (Before Fix)

```
F libc: Fatal signal 6 (SIGABRT), code -1 (SI_QUEUE)
JNI DETECTED ERROR IN APPLICATION: field operation on NULL object: 0x0
  in call to GetObjectField
  from void com.facebook.jni.HybridData$Destructor.deleteNative(long)
```

## Stack Trace

```
#16 facebook::react::JFabricUIManager::getBinding()
#17 facebook::react::RNSScreenShadowNodeCommitHook::getUIManagerFromSharedContext
#18 facebook::react::RNSScreenShadowNodeCommitHook::~RNSScreenShadowNodeCommitHook
```

## Environment

- React Native: 0.83.0-rc.3 / 0.83.0-rc.5
- react-native-screens: 4.19.0-nightly-20251203
- Architecture: New Architecture (Fabric + Bridgeless)
- Platform: Android

---------

Co-authored-by: Kacper Kafara <kacperkafara@gmail.com>
kligarski added a commit that referenced this pull request Jun 24, 2026
…for `StackHost` (#4200)

## Description

Disables layout from Yoga for children of `StackHost`. Fixes a
nondeterministic bug with incorrect screen size on orientation change.

Closes
software-mansion/react-native-screens-labs#1499.

### Details

On orientation change, sometimes measure & layout from Yoga was applied
with incorrect measurements. This is very likely connected to
software-mansion/react-native-screens-labs#543,
#3295 but I
didn't investigate further why we receive the outdated measurements.

In order to mitigate this, I decided to use similar strategy to
`StackHeaderConfig`. By enabling `needsCustomLayoutForChildren` in
`StackHostViewManager`, we will still receive `measure` with incorrect
width & height from Yoga but `layout` won't be called. This way,
incorrect measurements won't be applied and next native layout will
remeasure & apply correct layout. Ultimately, we want the screen to be
laid out fully by native layout, not by Yoga, so this is the desired
behavior. If anything changes, `StackHost` receives new measurement from
Yoga and will trigger native layout pass for `StackScreen`.

## Changes

- set `needsCustomLayoutForChildren=true` in `StackHostViewManager`

## Before & after - visual documentation

| Before | After |
| --- | --- |
| <video
src="https://github.com/user-attachments/assets/737b9b88-4749-439a-a970-271edeff051a"
/> | <video
src="https://github.com/user-attachments/assets/7c14be70-4a00-42c0-aba2-aea8d793cb2f"
/> |

## Test plan

Run `test-stack-toolbar-menu-show-as-action`. Change orientation
portrait -> landscape -> portrait multiple times. The content of the
screen should be laid out correctly.

## Checklist

- [x] Included code example that can be used to test this change.
- [x] For visual changes, included screenshots / GIFs / recordings
documenting the change.
- [x] Ensured that CI passes
sgaczol pushed a commit that referenced this pull request Jun 30, 2026
…for `StackHost` (#4200)

## Description

Disables layout from Yoga for children of `StackHost`. Fixes a
nondeterministic bug with incorrect screen size on orientation change.

Closes
software-mansion/react-native-screens-labs#1499.

### Details

On orientation change, sometimes measure & layout from Yoga was applied
with incorrect measurements. This is very likely connected to
software-mansion/react-native-screens-labs#543,
#3295 but I
didn't investigate further why we receive the outdated measurements.

In order to mitigate this, I decided to use similar strategy to
`StackHeaderConfig`. By enabling `needsCustomLayoutForChildren` in
`StackHostViewManager`, we will still receive `measure` with incorrect
width & height from Yoga but `layout` won't be called. This way,
incorrect measurements won't be applied and next native layout will
remeasure & apply correct layout. Ultimately, we want the screen to be
laid out fully by native layout, not by Yoga, so this is the desired
behavior. If anything changes, `StackHost` receives new measurement from
Yoga and will trigger native layout pass for `StackScreen`.

## Changes

- set `needsCustomLayoutForChildren=true` in `StackHostViewManager`

## Before & after - visual documentation

| Before | After |
| --- | --- |
| <video
src="https://github.com/user-attachments/assets/737b9b88-4749-439a-a970-271edeff051a"
/> | <video
src="https://github.com/user-attachments/assets/7c14be70-4a00-42c0-aba2-aea8d793cb2f"
/> |

## Test plan

Run `test-stack-toolbar-menu-show-as-action`. Change orientation
portrait -> landscape -> portrait multiple times. The content of the
screen should be laid out correctly.

## Checklist

- [x] Included code example that can be used to test this change.
- [x] For visual changes, included screenshots / GIFs / recordings
documenting the change.
- [x] Ensured that CI passes
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