Bluesky app fork with some witchin' additions 💫

[APP-1917] fix header measurement for android content disappearing on switch (#9964)

Co-authored-by: Samuel Newman <mozzius@protonmail.com>

authored by

Spence Pope
Samuel Newman
and committed by
GitHub
a95b877e f614bf65

+27 -9
+27 -9
src/view/com/pager/PagerWithHeader.tsx
··· 16 useSharedValue, 17 } from 'react-native-reanimated' 18 19 - import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 20 import {ScrollProvider} from '#/lib/ScrollContext' 21 import { 22 Pager, ··· 72 const headerHeight = headerOnlyHeight + tabBarHeight 73 74 // capture the header bar sizing 75 - const onTabBarLayout = useNonReactiveCallback((evt: LayoutChangeEvent) => { 76 const height = evt.nativeEvent.layout.height 77 if (height > 0) { 78 // The rounding is necessary to prevent jumps on iOS 79 setTabBarHeight(Math.round(height * 2) / 2) 80 } 81 - }) 82 - const onHeaderOnlyLayout = useNonReactiveCallback((height: number) => { 83 if (height > 0) { 84 // The rounding is necessary to prevent jumps on iOS 85 setHeaderOnlyHeight(Math.round(height * 2) / 2) 86 } 87 - }) 88 89 const renderTabBar = useCallback( 90 (props: RenderTabBarFnProps) => { ··· 270 ], 271 } 272 }) 273 - const headerRef = useRef(null) 274 return ( 275 <Animated.View 276 pointerEvents={IS_IOS ? 'auto' : 'box-none'} ··· 278 <View 279 ref={headerRef} 280 pointerEvents={IS_IOS ? 'auto' : 'box-none'} 281 - collapsable={false}> 282 {renderHeader?.({setMinimumHeight: setMinimumHeaderHeight})} 283 { 284 // It wouldn't be enough to place `onLayout` on the parent node because ··· 286 // Instead, we'll render a brand node conditionally and get fresh layout. 287 isHeaderReady && ( 288 <View 289 // It wouldn't be enough to do this in a `ref` of an effect because, 290 // even if `isHeaderReady` might have turned `true`, the associated 291 // layout might not have been performed yet on the native side. 292 onLayout={() => { 293 - // @ts-ignore 294 headerRef.current?.measure( 295 (_x: number, _y: number, _width: number, height: number) => { 296 - onHeaderOnlyLayout(height) 297 }, 298 ) 299 }}
··· 16 useSharedValue, 17 } from 'react-native-reanimated' 18 19 import {ScrollProvider} from '#/lib/ScrollContext' 20 import { 21 Pager, ··· 71 const headerHeight = headerOnlyHeight + tabBarHeight 72 73 // capture the header bar sizing 74 + const onTabBarLayout = useCallback((evt: LayoutChangeEvent) => { 75 const height = evt.nativeEvent.layout.height 76 if (height > 0) { 77 // The rounding is necessary to prevent jumps on iOS 78 setTabBarHeight(Math.round(height * 2) / 2) 79 } 80 + }, []) 81 + const onHeaderOnlyLayout = useCallback((height: number) => { 82 if (height > 0) { 83 // The rounding is necessary to prevent jumps on iOS 84 setHeaderOnlyHeight(Math.round(height * 2) / 2) 85 } 86 + }, []) 87 88 const renderTabBar = useCallback( 89 (props: RenderTabBarFnProps) => { ··· 269 ], 270 } 271 }) 272 + const headerRef = useRef<View>(null) 273 + const fallbackHeaderOnlyHeight = useRef(0) 274 return ( 275 <Animated.View 276 pointerEvents={IS_IOS ? 'auto' : 'box-none'} ··· 278 <View 279 ref={headerRef} 280 pointerEvents={IS_IOS ? 'auto' : 'box-none'} 281 + collapsable={false} 282 + onLayout={(e: LayoutChangeEvent) => { 283 + // Fallback measurement using onLayout directly on the header wrapper. 284 + // This is more reliable than .measure() on Android after certain 285 + // navigation transitions (e.g. returning from the logged-out view) 286 + // where .measure() can fail to return a height. in general though, 287 + // we should prefer using .measure() when possible as this can 288 + // fire too early and cause layout thrashing. 289 + // ref: https://github.com/bluesky-social/social-app/pull/9964 -sfp 290 + if (isHeaderReady) { 291 + fallbackHeaderOnlyHeight.current = e.nativeEvent.layout.height 292 + } 293 + }}> 294 {renderHeader?.({setMinimumHeight: setMinimumHeaderHeight})} 295 { 296 // It wouldn't be enough to place `onLayout` on the parent node because ··· 298 // Instead, we'll render a brand node conditionally and get fresh layout. 299 isHeaderReady && ( 300 <View 301 + collapsable={false} 302 // It wouldn't be enough to do this in a `ref` of an effect because, 303 // even if `isHeaderReady` might have turned `true`, the associated 304 // layout might not have been performed yet on the native side. 305 onLayout={() => { 306 headerRef.current?.measure( 307 (_x: number, _y: number, _width: number, height: number) => { 308 + // sometimes height is `undefined` on Android, see above 309 + if (height !== undefined) { 310 + onHeaderOnlyLayout(height) 311 + } else { 312 + // if measure fails, use the value we got from `onLayout` 313 + onHeaderOnlyLayout(fallbackHeaderOnlyHeight.current) 314 + } 315 }, 316 ) 317 }}