diff options
author | dan <dan.abramov@gmail.com> | 2023-11-10 19:54:33 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-11-10 19:54:33 +0000 |
commit | 91f8a23fbca5585490bb0f2064cdec8dd4b47cc9 (patch) | |
tree | bd369627641ecef8733ae877a93b048fabe59b09 /src/view/com/pager/PagerWithHeader.tsx | |
parent | 65def371659c3b64481199b2585a40a1affd9ec2 (diff) | |
download | voidsky-91f8a23fbca5585490bb0f2064cdec8dd4b47cc9.tar.zst |
Scroll sync in the pager without jumps (#1863)
Diffstat (limited to 'src/view/com/pager/PagerWithHeader.tsx')
-rw-r--r-- | src/view/com/pager/PagerWithHeader.tsx | 170 |
1 files changed, 100 insertions, 70 deletions
diff --git a/src/view/com/pager/PagerWithHeader.tsx b/src/view/com/pager/PagerWithHeader.tsx index 8b9e0c85a..e93d91fed 100644 --- a/src/view/com/pager/PagerWithHeader.tsx +++ b/src/view/com/pager/PagerWithHeader.tsx @@ -1,17 +1,19 @@ import * as React from 'react' import { LayoutChangeEvent, - NativeScrollEvent, + FlatList, + ScrollView, StyleSheet, View, + NativeScrollEvent, } from 'react-native' import Animated, { - Easing, - useAnimatedReaction, useAnimatedStyle, useSharedValue, - withTiming, runOnJS, + scrollTo, + useAnimatedRef, + AnimatedRef, } from 'react-native-reanimated' import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager' import {TabBar} from './TabBar' @@ -24,6 +26,7 @@ interface PagerWithHeaderChildParams { headerHeight: number onScroll: OnScrollHandler isScrolledDown: boolean + scrollElRef: React.MutableRefObject<FlatList<any> | ScrollView | null> } export interface PagerWithHeaderProps { @@ -54,28 +57,12 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>( ) { const {isMobile} = useWebMediaQueries() const [currentPage, setCurrentPage] = React.useState(0) - const scrollYs = React.useRef<Record<number, number>>({}) - const scrollY = useSharedValue(scrollYs.current[currentPage] || 0) const [tabBarHeight, setTabBarHeight] = React.useState(0) const [headerOnlyHeight, setHeaderOnlyHeight] = React.useState(0) - const [isScrolledDown, setIsScrolledDown] = React.useState( - scrollYs.current[currentPage] > SCROLLED_DOWN_LIMIT, - ) - + const [isScrolledDown, setIsScrolledDown] = React.useState(false) + const scrollY = useSharedValue(0) const headerHeight = headerOnlyHeight + tabBarHeight - // react to scroll updates - function onScrollUpdate(v: number) { - // track each page's current scroll position - scrollYs.current[currentPage] = Math.min(v, headerOnlyHeight) - // update the 'is scrolled down' value - setIsScrolledDown(v > SCROLLED_DOWN_LIMIT) - } - useAnimatedReaction( - () => scrollY.value, - v => runOnJS(onScrollUpdate)(v), - ) - // capture the header bar sizing const onTabBarLayout = React.useCallback( (evt: LayoutChangeEvent) => { @@ -91,19 +78,17 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>( ) // render the the header and tab bar - const headerTransform = useAnimatedStyle( - () => ({ - transform: [ - { - translateY: Math.min( - Math.min(scrollY.value, headerOnlyHeight) * -1, - 0, - ), - }, - ], - }), - [scrollY, headerHeight, tabBarHeight], - ) + const headerTransform = useAnimatedStyle(() => ({ + transform: [ + { + translateY: Math.min( + Math.min(scrollY.value, headerOnlyHeight) * -1, + 0, + ), + }, + ], + })) + const renderTabBar = React.useCallback( (props: RenderTabBarFnProps) => { return ( @@ -144,12 +129,38 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>( ], ) - // props to pass into children render functions - function onScrollWorklet(e: NativeScrollEvent) { - 'worklet' - scrollY.value = e.contentOffset.y + const scrollRefs = useSharedValue<AnimatedRef<any>[]>([]) + const registerRef = (scrollRef: AnimatedRef<any>, index: number) => { + scrollRefs.modify(refs => { + 'worklet' + refs[index] = scrollRef + return refs + }) } + const onScrollWorklet = React.useCallback( + (e: NativeScrollEvent) => { + 'worklet' + const nextScrollY = e.contentOffset.y + scrollY.value = nextScrollY + + if (nextScrollY < headerOnlyHeight) { + const refs = scrollRefs.value + for (let i = 0; i < refs.length; i++) { + if (i !== currentPage) { + scrollTo(refs[i], 0, nextScrollY, false) + } + } + } + + const nextIsScrolledDown = nextScrollY > SCROLLED_DOWN_LIMIT + if (isScrolledDown !== nextIsScrolledDown) { + runOnJS(setIsScrolledDown)(nextIsScrolledDown) + } + }, + [currentPage, headerOnlyHeight, isScrolledDown, scrollRefs, scrollY], + ) + const onPageSelectedInner = React.useCallback( (index: number) => { setCurrentPage(index) @@ -158,19 +169,9 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>( [onPageSelected, setCurrentPage], ) - const onPageSelecting = React.useCallback( - (index: number) => { - setCurrentPage(index) - if (scrollY.value > headerHeight) { - scrollY.value = headerHeight - } - scrollY.value = withTiming(scrollYs.current[index] || 0, { - duration: 170, - easing: Easing.inOut(Easing.quad), - }) - }, - [scrollY, setCurrentPage, scrollYs, headerHeight], - ) + const onPageSelecting = React.useCallback((index: number) => { + setCurrentPage(index) + }, []) return ( <Pager @@ -184,26 +185,18 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>( {toArray(children) .filter(Boolean) .map((child, i) => { - let output = null - if ( - child != null && - // Defer showing content until we know it won't jump. - isHeaderReady && - headerOnlyHeight > 0 && - tabBarHeight > 0 - ) { - output = child({ - headerHeight, - isScrolledDown, - onScroll: { - onScroll: i === currentPage ? onScrollWorklet : noop, - }, - }) - } - // Pager children must be noncollapsible plain <View>s. + const isReady = + isHeaderReady && headerOnlyHeight > 0 && tabBarHeight > 0 return ( <View key={i} collapsable={false}> - {output} + <PagerItem + headerHeight={headerHeight} + isReady={isReady} + isScrolledDown={isScrolledDown} + onScrollWorklet={i === currentPage ? onScrollWorklet : noop} + registerRef={(r: AnimatedRef<any>) => registerRef(r, i)} + renderTab={child} + /> </View> ) })} @@ -212,6 +205,43 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>( }, ) +function PagerItem({ + headerHeight, + isReady, + isScrolledDown, + onScrollWorklet, + renderTab, + registerRef, +}: { + headerHeight: number + isReady: boolean + isScrolledDown: boolean + registerRef: (scrollRef: AnimatedRef<any>) => void + onScrollWorklet: (e: NativeScrollEvent) => void + renderTab: ((props: PagerWithHeaderChildParams) => JSX.Element) | null +}) { + const scrollElRef = useAnimatedRef() + registerRef(scrollElRef) + + const scrollHandler = React.useMemo( + () => ({onScroll: onScrollWorklet}), + [onScrollWorklet], + ) + + if (!isReady || renderTab == null) { + return null + } + + return renderTab({ + headerHeight, + isScrolledDown, + onScroll: scrollHandler, + scrollElRef: scrollElRef as React.MutableRefObject< + FlatList<any> | ScrollView | null + >, + }) +} + const styles = StyleSheet.create({ tabBarMobile: { position: 'absolute', |