Optimize pager rendering (#2055)
* Pull out and memoize PagerTabBar * Avoid invalidating onScroll and add throttling * Make isScrolledDown update non-blocking * Fix typeszio/stable
parent
23ad3ad98b
commit
9fa90bb8d9
|
@ -11,14 +11,17 @@ import Animated, {
|
|||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
runOnJS,
|
||||
runOnUI,
|
||||
scrollTo,
|
||||
useAnimatedRef,
|
||||
AnimatedRef,
|
||||
SharedValue,
|
||||
} from 'react-native-reanimated'
|
||||
import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager'
|
||||
import {TabBar} from './TabBar'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {OnScrollHandler} from 'lib/hooks/useOnMainScroll'
|
||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||
|
||||
const SCROLLED_DOWN_LIMIT = 200
|
||||
|
||||
|
@ -56,7 +59,6 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
|
|||
}: PagerWithHeaderProps,
|
||||
ref,
|
||||
) {
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const [currentPage, setCurrentPage] = React.useState(0)
|
||||
const [tabBarHeight, setTabBarHeight] = React.useState(0)
|
||||
const [headerOnlyHeight, setHeaderOnlyHeight] = React.useState(0)
|
||||
|
@ -78,56 +80,34 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
|
|||
[setHeaderOnlyHeight],
|
||||
)
|
||||
|
||||
// render the the header and tab bar
|
||||
const headerTransform = useAnimatedStyle(() => ({
|
||||
transform: [
|
||||
{
|
||||
translateY: Math.min(
|
||||
Math.min(scrollY.value, headerOnlyHeight) * -1,
|
||||
0,
|
||||
),
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
const renderTabBar = React.useCallback(
|
||||
(props: RenderTabBarFnProps) => {
|
||||
return (
|
||||
<Animated.View
|
||||
style={[
|
||||
isMobile ? styles.tabBarMobile : styles.tabBarDesktop,
|
||||
headerTransform,
|
||||
]}>
|
||||
<View onLayout={onHeaderOnlyLayout}>{renderHeader?.()}</View>
|
||||
<View
|
||||
onLayout={onTabBarLayout}
|
||||
style={{
|
||||
// Render it immediately to measure it early since its size doesn't depend on the content.
|
||||
// However, keep it invisible until the header above stabilizes in order to prevent jumps.
|
||||
opacity: isHeaderReady ? 1 : 0,
|
||||
pointerEvents: isHeaderReady ? 'auto' : 'none',
|
||||
}}>
|
||||
<TabBar
|
||||
testID={testID}
|
||||
<PagerTabBar
|
||||
headerOnlyHeight={headerOnlyHeight}
|
||||
items={items}
|
||||
selectedPage={currentPage}
|
||||
isHeaderReady={isHeaderReady}
|
||||
renderHeader={renderHeader}
|
||||
currentPage={currentPage}
|
||||
onCurrentPageSelected={onCurrentPageSelected}
|
||||
onTabBarLayout={onTabBarLayout}
|
||||
onHeaderOnlyLayout={onHeaderOnlyLayout}
|
||||
onSelect={props.onSelect}
|
||||
onPressSelected={onCurrentPageSelected}
|
||||
scrollY={scrollY}
|
||||
testID={testID}
|
||||
/>
|
||||
</View>
|
||||
</Animated.View>
|
||||
)
|
||||
},
|
||||
[
|
||||
headerOnlyHeight,
|
||||
items,
|
||||
isHeaderReady,
|
||||
renderHeader,
|
||||
headerTransform,
|
||||
currentPage,
|
||||
onCurrentPageSelected,
|
||||
isMobile,
|
||||
onTabBarLayout,
|
||||
onHeaderOnlyLayout,
|
||||
scrollY,
|
||||
testID,
|
||||
],
|
||||
)
|
||||
|
@ -142,36 +122,50 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
|
|||
}
|
||||
|
||||
const lastForcedScrollY = useSharedValue(0)
|
||||
const onScrollWorklet = React.useCallback(
|
||||
(e: NativeScrollEvent) => {
|
||||
const adjustScrollForOtherPages = () => {
|
||||
'worklet'
|
||||
const nextScrollY = e.contentOffset.y
|
||||
scrollY.value = nextScrollY
|
||||
|
||||
const forcedScrollY = Math.min(nextScrollY, headerOnlyHeight)
|
||||
const currentScrollY = scrollY.value
|
||||
const forcedScrollY = Math.min(currentScrollY, headerOnlyHeight)
|
||||
if (lastForcedScrollY.value !== forcedScrollY) {
|
||||
lastForcedScrollY.value = forcedScrollY
|
||||
const refs = scrollRefs.value
|
||||
for (let i = 0; i < refs.length; i++) {
|
||||
if (i !== currentPage) {
|
||||
// This needs to run on the UI thread.
|
||||
scrollTo(refs[i], 0, forcedScrollY, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const nextIsScrolledDown = nextScrollY > SCROLLED_DOWN_LIMIT
|
||||
if (isScrolledDown !== nextIsScrolledDown) {
|
||||
runOnJS(setIsScrolledDown)(nextIsScrolledDown)
|
||||
}
|
||||
|
||||
const throttleTimeout = React.useRef<ReturnType<typeof setTimeout> | null>(
|
||||
null,
|
||||
)
|
||||
const queueThrottledOnScroll = useNonReactiveCallback(() => {
|
||||
if (!throttleTimeout.current) {
|
||||
throttleTimeout.current = setTimeout(() => {
|
||||
throttleTimeout.current = null
|
||||
|
||||
runOnUI(adjustScrollForOtherPages)()
|
||||
|
||||
const nextIsScrolledDown = scrollY.value > SCROLLED_DOWN_LIMIT
|
||||
if (isScrolledDown !== nextIsScrolledDown) {
|
||||
React.startTransition(() => {
|
||||
setIsScrolledDown(nextIsScrolledDown)
|
||||
})
|
||||
}
|
||||
}, 80 /* Sync often enough you're unlikely to catch it unsynced */)
|
||||
}
|
||||
})
|
||||
|
||||
const onScrollWorklet = React.useCallback(
|
||||
(e: NativeScrollEvent) => {
|
||||
'worklet'
|
||||
const nextScrollY = e.contentOffset.y
|
||||
scrollY.value = nextScrollY
|
||||
runOnJS(queueThrottledOnScroll)()
|
||||
},
|
||||
[
|
||||
currentPage,
|
||||
headerOnlyHeight,
|
||||
isScrolledDown,
|
||||
scrollRefs,
|
||||
scrollY,
|
||||
lastForcedScrollY,
|
||||
],
|
||||
[scrollY, queueThrottledOnScroll],
|
||||
)
|
||||
|
||||
const onPageSelectedInner = React.useCallback(
|
||||
|
@ -219,6 +213,67 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
|
|||
},
|
||||
)
|
||||
|
||||
let PagerTabBar = ({
|
||||
currentPage,
|
||||
headerOnlyHeight,
|
||||
isHeaderReady,
|
||||
items,
|
||||
scrollY,
|
||||
testID,
|
||||
renderHeader,
|
||||
onHeaderOnlyLayout,
|
||||
onTabBarLayout,
|
||||
onCurrentPageSelected,
|
||||
onSelect,
|
||||
}: {
|
||||
currentPage: number
|
||||
headerOnlyHeight: number
|
||||
isHeaderReady: boolean
|
||||
items: string[]
|
||||
testID?: string
|
||||
scrollY: SharedValue<number>
|
||||
renderHeader?: () => JSX.Element
|
||||
onHeaderOnlyLayout: (e: LayoutChangeEvent) => void
|
||||
onTabBarLayout: (e: LayoutChangeEvent) => void
|
||||
onCurrentPageSelected?: (index: number) => void
|
||||
onSelect?: (index: number) => void
|
||||
}): React.ReactNode => {
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const headerTransform = useAnimatedStyle(() => ({
|
||||
transform: [
|
||||
{
|
||||
translateY: Math.min(Math.min(scrollY.value, headerOnlyHeight) * -1, 0),
|
||||
},
|
||||
],
|
||||
}))
|
||||
return (
|
||||
<Animated.View
|
||||
style={[
|
||||
isMobile ? styles.tabBarMobile : styles.tabBarDesktop,
|
||||
headerTransform,
|
||||
]}>
|
||||
<View onLayout={onHeaderOnlyLayout}>{renderHeader?.()}</View>
|
||||
<View
|
||||
onLayout={onTabBarLayout}
|
||||
style={{
|
||||
// Render it immediately to measure it early since its size doesn't depend on the content.
|
||||
// However, keep it invisible until the header above stabilizes in order to prevent jumps.
|
||||
opacity: isHeaderReady ? 1 : 0,
|
||||
pointerEvents: isHeaderReady ? 'auto' : 'none',
|
||||
}}>
|
||||
<TabBar
|
||||
testID={testID}
|
||||
items={items}
|
||||
selectedPage={currentPage}
|
||||
onSelect={onSelect}
|
||||
onPressSelected={onCurrentPageSelected}
|
||||
/>
|
||||
</View>
|
||||
</Animated.View>
|
||||
)
|
||||
}
|
||||
PagerTabBar = React.memo(PagerTabBar)
|
||||
|
||||
function PagerItem({
|
||||
headerHeight,
|
||||
isReady,
|
||||
|
|
Loading…
Reference in New Issue