Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -176,13 +176,19 @@ 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
@Synchronized
private fun computeDummyLayout(
fontSize: Int,
isTitleEmpty: Boolean,
applyTopInset: Boolean,
): Float {
if (!isLayoutInitialized) {
val reactContext =
Expand All @@ -198,7 +204,7 @@ internal class ScreenDummyLayoutHelper(
}
}

if (cache.hasKey(CacheKey(fontSize, isTitleEmpty))) {
if (cache.hasKey(CacheKey(fontSize, isTitleEmpty, applyTopInset))) {
return cache.headerHeight
}

Expand All @@ -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.
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -359,6 +367,7 @@ internal class ScreenDummyLayoutHelper(
private data class CacheKey(
val fontSize: Int,
val isTitleEmpty: Boolean,
val applyTopInset: Boolean,
)

private class CacheEntry(
Expand All @@ -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)
}
}
56 changes: 56 additions & 0 deletions apps/src/tests/issue-tests/Test4357.tsx
Original file line number Diff line number Diff line change
@@ -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 = () => <View style={styles.contentContainer} />;

function Screen1() {
return <DummyContent />;
}

function NavigationStack() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name="Screen1"
component={Screen1}
options={{ unstable_headerInsets: { top: false } }}
/>
</Stack.Navigator>
</NavigationContainer>
);
}

export default function App() {
const [show, setShow] = useState(false);

if (show) {
return <NavigationStack />;
}

return (
<View style={styles.centerLayout}>
<Button title="Mount Navigation Stack" onPress={() => setShow(true)} />
</View>
);
}

const styles = StyleSheet.create({
centerLayout: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
screen: {
flex: 1,
},
contentContainer: {
flex: 1,
backgroundColor: Colors.NavyDark100,
},
});
1 change: 1 addition & 0 deletions apps/src/tests/issue-tests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ static constexpr const char *kScreenDummyLayoutHelperClass =

std::optional<float> findHeaderHeight(
const int fontSize,
const bool isTitleEmpty) {
const bool isTitleEmpty,
const bool applyTopInset) {
JNIEnv *env = facebook::jni::Environment::current();

if (env == nullptr) {
Expand All @@ -49,7 +50,7 @@ std::optional<float> 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";
Expand All @@ -76,7 +77,7 @@ std::optional<float> findHeaderHeight(
}

jfloat headerHeight = env->CallFloatMethod(
packageInstance, computeDummyLayoutID, fontSize, isTitleEmpty);
packageInstance, computeDummyLayoutID, fontSize, isTitleEmpty, applyTopInset);

return {headerHeight};
}
Expand All @@ -101,13 +102,23 @@ void RNSScreenShadowNode::appendChild(
*std::static_pointer_cast<const RNSScreenStackHeaderConfigProps>(
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});
Expand Down
Loading