diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/view/com/pager/PagerWithHeader.tsx | 179 |
1 files changed, 117 insertions, 62 deletions
diff --git a/src/view/com/pager/PagerWithHeader.tsx b/src/view/com/pager/PagerWithHeader.tsx index 2c7640c43..487c589e3 100644 --- a/src/view/com/pager/PagerWithHeader.tsx +++ b/src/view/com/pager/PagerWithHeader.tsx @@ -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} - items={items} - selectedPage={currentPage} - onSelect={props.onSelect} - onPressSelected={onCurrentPageSelected} - /> - </View> - </Animated.View> + <PagerTabBar + headerOnlyHeight={headerOnlyHeight} + items={items} + isHeaderReady={isHeaderReady} + renderHeader={renderHeader} + currentPage={currentPage} + onCurrentPageSelected={onCurrentPageSelected} + onTabBarLayout={onTabBarLayout} + onHeaderOnlyLayout={onHeaderOnlyLayout} + onSelect={props.onSelect} + scrollY={scrollY} + testID={testID} + /> ) }, [ + 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 adjustScrollForOtherPages = () => { + 'worklet' + 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 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 - - const forcedScrollY = Math.min(nextScrollY, headerOnlyHeight) - if (lastForcedScrollY.value !== forcedScrollY) { - lastForcedScrollY.value = forcedScrollY - const refs = scrollRefs.value - for (let i = 0; i < refs.length; i++) { - if (i !== currentPage) { - scrollTo(refs[i], 0, forcedScrollY, false) - } - } - } - - const nextIsScrolledDown = nextScrollY > SCROLLED_DOWN_LIMIT - if (isScrolledDown !== nextIsScrolledDown) { - runOnJS(setIsScrolledDown)(nextIsScrolledDown) - } + 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, |