diff --git a/android/src/main/java/com/swmansion/rnscreens/legacy/utils/ScreenDummyLayoutHelper.kt b/android/src/main/java/com/swmansion/rnscreens/legacy/utils/ScreenDummyLayoutHelper.kt
index b24f5c535b..e1343afd14 100644
--- a/android/src/main/java/com/swmansion/rnscreens/legacy/utils/ScreenDummyLayoutHelper.kt
+++ b/android/src/main/java/com/swmansion/rnscreens/legacy/utils/ScreenDummyLayoutHelper.kt
@@ -176,6 +176,11 @@ internal class ScreenDummyLayoutHelper(
* the cache while a measurement is in progress.
*
* @param fontSize font size value as passed from JS
+ * @param isTitleEmpty whether the header title is empty
+ * @param applyTopInset whether the native header applies the top inset (mirrors
+ * `legacyTopInsetBehavior || consumeTopInset` from [CustomToolbar]). When `false`
+ * (e.g. `disableTopInsetApplication`), the top inset must be excluded so the reported
+ * header height matches what actually renders.
* @return header height in dp as consumed by Yoga
*/
@DoNotStrip
@@ -183,6 +188,7 @@ internal class ScreenDummyLayoutHelper(
private fun computeDummyLayout(
fontSize: Int,
isTitleEmpty: Boolean,
+ applyTopInset: Boolean,
): Float {
if (!isLayoutInitialized) {
val reactContext =
@@ -198,7 +204,7 @@ internal class ScreenDummyLayoutHelper(
}
}
- if (cache.hasKey(CacheKey(fontSize, isTitleEmpty))) {
+ if (cache.hasKey(CacheKey(fontSize, isTitleEmpty, applyTopInset))) {
return cache.headerHeight
}
@@ -212,7 +218,7 @@ internal class ScreenDummyLayoutHelper(
}
val topLevelDecorView = currentActivity.window.decorView
- val topInset = getDecorViewTopInset(topLevelDecorView)
+ val topInset = if (applyTopInset) getDecorViewTopInset(topLevelDecorView) else 0
// These dimensions are not accurate, as they do include navigation bar, however
// it is ok for our purposes.
@@ -242,11 +248,13 @@ internal class ScreenDummyLayoutHelper(
// scenarios when layout violates measured dimensions.
currentCoordinatorLayout.layout(0, 0, decorViewWidth, decorViewHeight)
- // Include the top inset to account for the extra padding manually applied to the CustomToolbar.
+ // Include the top inset to account for the extra padding manually applied to the
+ // CustomToolbar. When the header opts out of the top inset (`applyTopInset == false`),
+ // `topInset` is 0 here so the measured height matches the rendered header.
val totalAppBarLayoutHeight = currentAppBarLayout.height.toFloat() + topInset
val headerHeight = PixelUtil.toDIPFromPixel(totalAppBarLayoutHeight)
- cache = CacheEntry(CacheKey(fontSize, isTitleEmpty), headerHeight)
+ cache = CacheEntry(CacheKey(fontSize, isTitleEmpty, applyTopInset), headerHeight)
return headerHeight
}
@@ -359,6 +367,7 @@ internal class ScreenDummyLayoutHelper(
private data class CacheKey(
val fontSize: Int,
val isTitleEmpty: Boolean,
+ val applyTopInset: Boolean,
)
private class CacheEntry(
@@ -368,6 +377,6 @@ private class CacheEntry(
fun hasKey(key: CacheKey) = cacheKey.fontSize != Int.MIN_VALUE && cacheKey == key
companion object {
- val EMPTY = CacheEntry(CacheKey(Int.MIN_VALUE, false), 0f)
+ val EMPTY = CacheEntry(CacheKey(Int.MIN_VALUE, false, true), 0f)
}
}
diff --git a/apps/src/tests/issue-tests/Test4357.tsx b/apps/src/tests/issue-tests/Test4357.tsx
new file mode 100644
index 0000000000..8741f980fe
--- /dev/null
+++ b/apps/src/tests/issue-tests/Test4357.tsx
@@ -0,0 +1,56 @@
+import React, { useState } from 'react';
+import { Button, StyleSheet, View } from 'react-native';
+import { NavigationContainer } from '@react-navigation/native';
+import { createNativeStackNavigator } from '@react-navigation/native-stack';
+import { Colors } from '@apps/shared/styling';
+
+const Stack = createNativeStackNavigator();
+
+const DummyContent = () => ;
+
+function Screen1() {
+ return ;
+}
+
+function NavigationStack() {
+ return (
+
+
+
+
+
+ );
+}
+
+export default function App() {
+ const [show, setShow] = useState(false);
+
+ if (show) {
+ return ;
+ }
+
+ return (
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ centerLayout: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ screen: {
+ flex: 1,
+ },
+ contentContainer: {
+ flex: 1,
+ backgroundColor: Colors.NavyDark100,
+ },
+});
diff --git a/apps/src/tests/issue-tests/index.ts b/apps/src/tests/issue-tests/index.ts
index 74fa2cce45..8e8054bcea 100644
--- a/apps/src/tests/issue-tests/index.ts
+++ b/apps/src/tests/issue-tests/index.ts
@@ -204,6 +204,7 @@ export { default as Test4258 } from './Test4258';
export { default as Test4264 } from './Test4264';
export { default as Test4265 } from './Test4265';
export { default as Test4276 } from './Test4276';
+export { default as Test4357 } from './Test4357';
export { default as TestScreenAnimation } from './TestScreenAnimation';
// The following test was meant to demo the "go back" gesture using Reanimated
// but the associated PR in react-navigation is currently put on hold
diff --git a/common/cpp/react/renderer/components/rnscreens/legacy/RNSScreenShadowNode.cpp b/common/cpp/react/renderer/components/rnscreens/legacy/RNSScreenShadowNode.cpp
index fb0fcb2724..07ddd4fe17 100644
--- a/common/cpp/react/renderer/components/rnscreens/legacy/RNSScreenShadowNode.cpp
+++ b/common/cpp/react/renderer/components/rnscreens/legacy/RNSScreenShadowNode.cpp
@@ -32,7 +32,8 @@ static constexpr const char *kScreenDummyLayoutHelperClass =
std::optional findHeaderHeight(
const int fontSize,
- const bool isTitleEmpty) {
+ const bool isTitleEmpty,
+ const bool applyTopInset) {
JNIEnv *env = facebook::jni::Environment::current();
if (env == nullptr) {
@@ -49,7 +50,7 @@ std::optional findHeaderHeight(
}
jmethodID computeDummyLayoutID =
- env->GetMethodID(layoutHelperClass, "computeDummyLayout", "(IZ)F");
+ env->GetMethodID(layoutHelperClass, "computeDummyLayout", "(IZZ)F");
if (computeDummyLayoutID == nullptr) {
LOG(ERROR) << "[RNScreens] Failed to retrieve computeDummyLayout method ID";
@@ -76,7 +77,7 @@ std::optional findHeaderHeight(
}
jfloat headerHeight = env->CallFloatMethod(
- packageInstance, computeDummyLayoutID, fontSize, isTitleEmpty);
+ packageInstance, computeDummyLayoutID, fontSize, isTitleEmpty, applyTopInset);
return {headerHeight};
}
@@ -101,13 +102,23 @@ void RNSScreenShadowNode::appendChild(
*std::static_pointer_cast(
headerConfigChild->getProps());
+ // The native CustomToolbar only pads itself with the top inset when
+ // `legacyTopInsetBehavior || consumeTopInset` holds (see CustomToolbar.kt).
+ // The dummy measurement must mirror that condition, otherwise the header
+ // height is over-reported by the top inset whenever a header opts out via
+ // `disableTopInsetApplication` (consumeTopInset == false).
+ const bool applyTopInset =
+ headerProps.legacyTopInsetBehavior || headerProps.consumeTopInset;
+
// Translucent headers do not reserve vertical space, so applying the
// estimated header correction can expose the previous screen at the
// bottom during the first Fabric layout.
const auto headerHeight = (headerProps.hidden || headerProps.translucent)
? 0.f
: findHeaderHeight(
- headerProps.titleFontSize, headerProps.title.empty())
+ headerProps.titleFontSize,
+ headerProps.title.empty(),
+ applyTopInset)
.value_or(0.f);
screenShadowNode.setPadding({0, 0, 0, headerHeight});