Skip to content

Commit ab2a303

Browse files
authored
fix(Android,Fabric): prevent header subview disappearance when using setOptions (#2812)
## Description * [x] Should be merged after #2811 & rebased. When setting subviews via `setOptions` from `useEffect` hook in a component, the first frame received might be computed by native layout & completely invalid (zero height). RN layout is the source of a subview **size** (not origin). When we send such update with zero height Yoga might (or might not, depending on exact update timing in relation to other ongoing commits / layouts) set the subview height to 0! This causes the subview to become invisible & we want to avoid that. This had not been a problem before #2696, because we would filter out this kind of frame in the `setSize` guard in the `ComponentDescriptor.adopt` method of the `HeaderSubview`. #2696 allowed for zero-sized frames for other, unrelated reason & we must allow these as long as they come from React Native layout & not native one. ## Changes We now filter these invalid frames on the side of HostTree, by detecting whether React has measured the subview or not. ## Test code and steps to reproduce I've tested the problem on slightly modified `Test2466`: <details> <summary>Code snippet</summary> ```tsx import { NavigationContainer } from '@react-navigation/native'; import { createNativeStackNavigator, NativeStackNavigationProp } from '@react-navigation/native-stack'; import React from 'react'; import { findNodeHandle, Text, View } from 'react-native'; import PressableWithFeedback from '../shared/PressableWithFeedback'; type StackParamList = { Home: undefined, } type RouteProps = { navigation: NativeStackNavigationProp<StackParamList>; } const Stack = createNativeStackNavigator<StackParamList>(); function HeaderTitle(): React.JSX.Element { return ( <PressableWithFeedback onLayout={event => { const { x, y, width, height } = event.nativeEvent.layout; console.log('Title onLayout', { x, y, width, height }); }} onPressIn={() => { console.log('Pressable onPressIn'); }} onPress={() => console.log('Pressable onPress')} onPressOut={() => console.log('Pressable onPressOut')} onResponderMove={() => console.log('Pressable onResponderMove')} ref={node => { console.log(findNodeHandle(node)); node?.measure((x, y, width, height, pageX, pageY) => { console.log('header component measure', { x, y, width, height, pageX, pageY }); }); }} > <View style={{ height: 40, justifyContent: 'center', alignItems: 'center' }}> <Text style={{ alignItems: 'center' }}>Regular Pressable</Text> </View> </PressableWithFeedback> ); } function HeaderLeft(): React.JSX.Element { return ( <HeaderTitle /> ); } function Home({ navigation }: RouteProps): React.JSX.Element { React.useEffect(() => { console.log('calling setOptions in useEffect'); navigation.setOptions({ //headerTitle: HeaderTitle, headerLeft: HeaderLeft, //headerRight: HeaderLeft, }); }, [navigation]); return ( <View style={{ flex: 1, backgroundColor: 'rgba(0, 0, 0, .8)' }} > <View style={{ flex: 1, alignItems: 'center', marginTop: 48 }}> <PressableWithFeedback onPressIn={() => console.log('Pressable onPressIn')} onPress={() => console.log('Pressable onPress')} onPressOut={() => console.log('Pressable onPressOut')} > <View style={{ height: 40, width: 200, justifyContent: 'center', alignItems: 'center' }}> <Text style={{ alignItems: 'center' }}>Regular Pressable</Text> </View> </PressableWithFeedback> </View> </View> ); } function App(): React.JSX.Element { return ( <NavigationContainer> <Stack.Navigator> <Stack.Screen name="Home" component={Home} options={{ //headerTitle: HeaderTitle, //headerLeft: HeaderLeft, //headerRight: HeaderLeft, }} /> </Stack.Navigator> </NavigationContainer> ); } export default App; ``` </details> ## Checklist - [x] Included code example that can be used to test this change - [x] Ensured that CI passes (7d3205e)
1 parent b1e9c71 commit ab2a303

1 file changed

Lines changed: 19 additions & 2 deletions

File tree

android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderSubview.kt

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ class ScreenStackHeaderSubview(
1010
) : FabricEnabledHeaderSubviewViewGroup(context) {
1111
private var reactWidth = 0
1212
private var reactHeight = 0
13+
14+
/**
15+
* Semantics: true iff we **believe** that SurfaceMountingManager has measured this view during mount item
16+
* execution. We recognize this case by checking measure mode in `onMeasure`. If Androidx
17+
* happens to use `EXACTLY` for both dimensions this property might convey invalid information.
18+
*/
19+
private var isReactSizeSet = false
20+
1321
var type = Type.RIGHT
1422

1523
val config: ScreenStackHeaderConfig?
@@ -22,9 +30,10 @@ class ScreenStackHeaderSubview(
2230
if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY &&
2331
MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY
2432
) {
25-
// dimensions provided by react
33+
// dimensions provided by react (with high probability)
2634
reactWidth = MeasureSpec.getSize(widthMeasureSpec)
2735
reactHeight = MeasureSpec.getSize(heightMeasureSpec)
36+
isReactSizeSet = true
2837
val parent = parent
2938
if (parent != null) {
3039
forceLayout()
@@ -44,7 +53,15 @@ class ScreenStackHeaderSubview(
4453
if (changed && BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
4554
val width = r - l
4655
val height = b - t
47-
updateSubviewFrameState(width, height, l, t)
56+
57+
// When setting subviews via `setOptions` from `useEffect` hook in a component, the first
58+
// frame received might be computed by native layout & completely invalid (zero height).
59+
// RN layout is the source of subview **size** (not origin) & we need to avoid sending
60+
// this native size to ST. Doing otherwise might lead to problems.
61+
// See: https://github.com/software-mansion/react-native-screens/pull/2812
62+
if (isReactSizeSet) {
63+
updateSubviewFrameState(width, height, l, t)
64+
}
4865
}
4966
}
5067

0 commit comments

Comments
 (0)