Header jump when animation is set for bottom tab navigator
Description
Moving this issue from https://github.com/react-navigation/react-navigation/issues/12273.
I can't repro this on JS stack, and the headerTopInsetEnabled prop for screens is always set to true. So it seems to be an issue with react-native-screens.
The header will have an initial jump when animation is set for tab navigator. It doesn't matter if it's set to fade or shift.
https://github.com/user-attachments/assets/8435fe72-3fad-4e48-a27e-ce92c11d1ce9
Expected behavior
The header should not flicker or have jumps.
https://github.com/user-attachments/assets/b13aeb15-2d52-4955-aeba-87ae63f6ac84
Steps to reproduce
Set bottom tab animation to fade or shift.
Snack or a link to a repository
https://github.com/matinzd/ReactNavigationHeaderFlicker
Screens version
4.1.0
React Native version
0.76.2
Platforms
iOS
JavaScript runtime
Hermes
Workflow
React Native (without Expo)
Architecture
Fabric (New Architecture)
Build type
Debug mode
Device
iOS simulator
Device model
No response
Acknowledgements
Yes
@matinzd ~After almost 4 hours on this bug, this hack fixed the issue for me: https://github.com/software-mansion/react-native-screens/issues/1779#issuecomment-1580291463~
This doesn't fix the issue.
That is the same issue but it's not caused by the same thing. Now the new tab animation is causing this issue which is not the same as that one.
The workaround may work but it's not a correct solution. I also usually use scroll view with automatic inset adjustment so that I don't need to take care of the header height.
I agree with you. And that hack actually doesn't fix the issue.
Alright, I could pretty much narrow it down. It is indeed related to the new animation I guess, and in my case it was triggered because we enabled fullScreenGestureEnabled, which under the hood switches to simple_push.
It's now also reproducible in the Expo Snack using stock react navigation v7. https://snack.expo.dev/@hirbod/native-stack-navigator-header-to-non-header
I suspect this PR caused it: https://github.com/software-mansion/react-native-screens/pull/2477 @kkafar
https://github.com/user-attachments/assets/2f2fbc2e-cf48-438f-b3a9-ebaf94e0060d
EDIT: 100% isolated reproducible with react-native-screens alone. Here is the issue for it: https://github.com/software-mansion/react-native-screens/issues/2550
Any attempts to fix this?
I also found out that useHeaderHeight changes to some weird values when screen is mounted within tab navigation.
Here is how useHeaderHeight behaves when component re-renders when it's inside tab navigator:
LOG headerHeight 100.66666666666666
LOG headerHeight 44
LOG headerHeight 100.33333333333334
Here is how it behvaes when we navigate to another stack:
LOG headerHeight 100.66666666666666
LOG headerHeight 100.33333333333334
You can see it here as well:
https://github.com/matinzd/ReactNavigationHeaderFlicker/commit/31a433848f2d5a587fb4f42c4795fb199f60921d
UPDATE
useHeaderHeight still behaves the same even if animation is not set to fade in tab navigator. It starts with a correct values then it jumps back to much lower value then back to the same value causing jumps if someone is using this hook.
I updated the reproducible. Please take a look at it. This is so annoying.
I digged more into the react navigation code and I found that those incorrect values are being emitted from onHeaderHeightChange which comes from ScreenStackItem:
https://github.com/react-navigation/react-navigation/blob/main/packages/native-stack/src/views/NativeStackView.native.tsx#L343C11-L343C31
I tried to debug it a bit with xcode.
This is what happens in the first calculation for the header height:
Then the value is now corrected in the second run:
navbarInset is in the first run.
Sorry for mentioning you here but It's been a long time I opened this issue and I am still waiting for your response. I know you are very busy with all the new arch changes but it would be so helpful if we can also get this issue fixed. I am also available to hop on a call with you on discord to dig into this. Thank you @kkafar for all the work you are putting in for this repo!
I haven't had opportunity to work on this yet, but I should have capacity to look into it tomorrow / at Monday.
Any updates @kkafar ? This issue is so annoying and causes a unnecessary re-renders inside the pages we use as well as visual jumps.
Sorry, still haven't looked into it yet. Will do it soon
Hey @matinzd, ~this problem is not reproducible using the provided example.~ (see below 👇🏻)
https://github.com/user-attachments/assets/082cb67f-9c31-44ae-80e1-097bd15d3733
~Do I need to do some extra steps, or I can assume this works now?~
Oh, I see the commented line now. I confirm the issue is reproducible
Important note: if you swap scrollview for regular view it works correctly.
I have a grasp on what's going on here. Making very long story short:
- When animation is not set on tab navigator, the "two state container" is being used - it uses
RNSNavigationController&RNSScreenNavigationContainerViewunder the hood for managing transitions - works as expected. - In opposite case - plain
RNSScreenContainerView&RNSViewControllerare being used leading to situation where the UIKit callbacks_UIScrollViewAdjustForOverlayInsetsChangeIfNecessaryare coming after the transaction completes, and not before it is scheduled - leading to corrupted UI state.
Leaving this as a note for myself to continue investigation tomorrow on how to fix this.
@matinzd Do you happen to have context on when was this bug introduced? Is this regression os has it always happened? I doubt #2477 is responsible because the RNSScreenStackAnimator modified there is literally not created even once.
I confirmed this is also an issue on RN 0.76 & react-native-screens 3.35.0. Therefore I recon that this is a long standing issue not introduced by v4.
For future me:
I've spent some more time today debugging and do not have any big breakthrough.
Only thing I learned is that the _UIScrollViewAdjustForOverlayInsetsChangeIfNecessary is called on time in both cases, but in case where there is no UINavigationController present the computed offset value is 0.f until the transition finishes -> therefore this method fails to set the correct value. I've determined this by looking through assembler of _UIScrollViewAdjustForOverlayInsetsChangeIfNecessary, however I could not find out why it is the case as the current offset value is passed as argument to this method (through d0 register), but I've failed to navigate up the stack frames of private UIKit methods with lldb to determine how it is computed. 🤷♂
My working hypothesis is that it works fine in presence of navigation controller, because setViewControllers attaches the views to the model layer, allowing correct layout to execute and the transition starts in yet another iteration of main run loop. While attaching the views via regular view controller causes the transition to start first. No idea how to fix this sensibly yet.
Thanks for the investigation @kkafar ! Let me know when this is fixed.
I can experience this issue with RN 0.76.9 and react-native-screens 4.4.0. Does anyone know if this is reproducible on RN 0.77+?
Most likely it is. I haven't had opportunity to return to the issue yet. I'll try to look into it next week.
Hi @kkafar Is there any update for this ? I am still experiencing this issue. Using "react-native": "0.78.0"
We are experiencing a similar header jump. We've also tracked down that onHeaderHeightChange is sometimes returning 44 and then immediately after the correct number.