-
-
Notifications
You must be signed in to change notification settings - Fork 545
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(Android,Fabric): prevent another infinite state update loop for h…
…eader subviews with zero size (#2696) ## Description Closes #2675 ### Error mechanism This one was tricky to figure out. Few concurrent facts first: 1. When `statusBarTranslucent` is `true` we apply `paddingTop` to native `Toolbar` to heighten the header. 2. Since 4.6.0 we update frames of header subviews in ShadowTree to the values from HostTree, everytime the frame changes in HostTree. 3. We had a check in header subview shadow node that would prevent node size update if the frame was (0, 0). Header subview that represents the search bar has no content. Therefore native layout sets it frame to exactly (0, 0), but with origin `(x, toolbar.paddingTop)`. This frame - `((x, toolbar.paddingTop), (0, 0))` is send to shadow node, **where it is ignored**, but the layout is triggered by subsequent *header config* update. Therefore Yoga resolves height of the subview to full height of the parent (usually `154 px = 40 dip`). New layout metrics are sent to HostTree, where next native toolbar layout determines toolbar size to be its content height + padding == subview height + paddingTop. This gets send to ShadowTree, then back to HostTree, native layout is triggered and another `paddingTop` value is added to the overall header height, and so on. This is a regression introduced in 4.6.0 and limited to Fabric. The fix is simple - accept the (0, 0) size for the subview in ShadowTree. We just need to use more reasonable value to denote the uninitialized frame - `{-1, -1}` seems like a better choice as it is an invalid frame. ## Changes - **Use different initial value in state so that we can unambiguously distinguish between un- and initialized state** ## Test code and steps to reproduce `Test2675` Tested on all combinations: * Android SDK 34 (w/o edge-to-edge) and 35 (w/ edge-to-edge enabled), * `statusBarTranslucent: true` and `false`, * search bar present, not present * other header elements present / not present. Also tested iOS on the same example, because the code changes affect shared C++ code. ## Checklist - [x] Included code example that can be used to test this change - [x] Ensured that CI passes
- Loading branch information
Showing
7 changed files
with
142 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
import { NavigationContainer } from '@react-navigation/native'; | ||
import { NativeStackNavigationProp, createNativeStackNavigator } from '@react-navigation/native-stack'; | ||
import React from 'react'; | ||
import { Button, StyleSheet, Text, View } from 'react-native'; | ||
|
||
type RouteParams = { | ||
Home: undefined; | ||
DynamicHeader: undefined; | ||
} | ||
|
||
type NavigationProps = { | ||
navigation: NativeStackNavigationProp<RouteParams>; | ||
} | ||
|
||
const Stack = createNativeStackNavigator(); | ||
|
||
function HomeScreen({ navigation }: NavigationProps) { | ||
return ( | ||
<View style={styles.container}> | ||
<Text>Home Screen</Text> | ||
<Button title="Navigate DynamicHeaderScreen" onPress={() => navigation.navigate('DynamicHeader')} /> | ||
</View> | ||
); | ||
} | ||
|
||
function DynamicHeaderScreen({ navigation }: NavigationProps) { | ||
React.useLayoutEffect(() => { | ||
navigation.setOptions({ | ||
headerRight: HeaderRight, | ||
}); | ||
}, [navigation]); | ||
|
||
React.useEffect(() => { | ||
const timerId = setTimeout(() => { | ||
navigation.setOptions({ | ||
headerLeft: HeaderLeft, | ||
headerRight: HeaderLeft, | ||
}); | ||
}, 1300); | ||
|
||
return () => { | ||
clearTimeout(timerId); | ||
}; | ||
}, [navigation]); | ||
|
||
return ( | ||
<View style={styles.container}> | ||
<Text>DynamicHeaderScreen</Text> | ||
<Button title="Go back" onPress={() => navigation.popTo('Home')} /> | ||
</View> | ||
); | ||
} | ||
|
||
function HeaderLeft() { | ||
return ( | ||
<View> | ||
<Text>Left</Text> | ||
</View> | ||
); | ||
} | ||
|
||
function HeaderTitle() { | ||
return ( | ||
<View> | ||
<Text>Title</Text> | ||
</View> | ||
); | ||
} | ||
|
||
function HeaderRight() { | ||
return ( | ||
<View> | ||
<Text>Right</Text> | ||
</View> | ||
); | ||
} | ||
|
||
export default function App() { | ||
return ( | ||
<NavigationContainer> | ||
<Stack.Navigator> | ||
<Stack.Screen | ||
name="Home" | ||
component={HomeScreen} | ||
options={{ | ||
statusBarTranslucent: true, | ||
statusBarStyle: 'dark', | ||
//headerTitle: HeaderTitle, | ||
//headerRight: HeaderRight, | ||
headerSearchBarOptions: { | ||
placeholder: 'Search...', | ||
onChangeText: (event) => { | ||
console.log('Search text:', event.nativeEvent.text); | ||
}, | ||
}, | ||
}} | ||
/> | ||
<Stack.Screen name="DynamicHeader" component={DynamicHeaderScreen} options={{ | ||
statusBarTranslucent: true, | ||
statusBarStyle: 'dark', | ||
headerTitle: HeaderTitle, | ||
headerSearchBarOptions: { | ||
placeholder: 'Search...', | ||
onChangeText: (event) => { | ||
console.log('Search text:', event.nativeEvent.text); | ||
}, | ||
}, | ||
}} /> | ||
</Stack.Navigator> | ||
</NavigationContainer> | ||
); | ||
} | ||
|
||
const styles = StyleSheet.create({ | ||
container: { | ||
flex: 1, | ||
justifyContent: 'center', | ||
alignItems: 'center', | ||
}, | ||
}); | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters