From a3c14b8108e5823dba413f7c134161fbd7d98c8d Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 10 Nov 2023 03:29:29 +0000 Subject: [PATCH] Force scroll sync for inactive tabs --- src/view/com/pager/PagerWithHeader.tsx | 65 +++++++++++++++++++------- 1 file changed, 47 insertions(+), 18 deletions(-) diff --git a/src/view/com/pager/PagerWithHeader.tsx b/src/view/com/pager/PagerWithHeader.tsx index b1a18ed9ef..b52ce2da0d 100644 --- a/src/view/com/pager/PagerWithHeader.tsx +++ b/src/view/com/pager/PagerWithHeader.tsx @@ -6,6 +6,7 @@ import Animated, { useAnimatedStyle, useSharedValue, runOnJS, + scrollTo, useAnimatedRef, } from 'react-native-reanimated' import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager' @@ -13,8 +14,6 @@ import {TabBar} from './TabBar' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {OnScrollCb} from 'lib/hooks/useOnMainScroll' -const SCROLLED_DOWN_LIMIT = 200 - interface PagerWithHeaderChildParams { headerHeight: number onScroll: OnScrollCb @@ -50,21 +49,13 @@ export const PagerWithHeader = React.forwardRef( ) { const {isMobile} = useWebMediaQueries() const [currentPage, setCurrentPage] = React.useState(0) - const scrollY = useSharedValue(0) + const clampedScrollY = useSharedValue(0) const [tabBarHeight, setTabBarHeight] = React.useState(0) const [headerOnlyHeight, setHeaderOnlyHeight] = React.useState(0) const [isScrolledDown, setIsScrolledDown] = React.useState(false) const headerHeight = headerOnlyHeight + tabBarHeight - function onScrollUpdate(v: number) { - setIsScrolledDown(v > SCROLLED_DOWN_LIMIT) - } - useAnimatedReaction( - () => scrollY.value, - v => runOnJS(onScrollUpdate)(v), - ) - // capture the header bar sizing const onTabBarLayout = React.useCallback( (evt: LayoutChangeEvent) => { @@ -84,14 +75,11 @@ export const PagerWithHeader = React.forwardRef( () => ({ transform: [ { - translateY: Math.min( - Math.min(scrollY.value, headerOnlyHeight) * -1, - 0, - ), + translateY: Math.min(-clampedScrollY.value, 0), }, ], }), - [scrollY, headerHeight, tabBarHeight], + [clampedScrollY, headerHeight, tabBarHeight], ) const renderTabBar = React.useCallback( (props: RenderTabBarFnProps) => { @@ -136,9 +124,17 @@ export const PagerWithHeader = React.forwardRef( // props to pass into children render functions const onScroll = useAnimatedScrollHandler({ onScroll(e) { - scrollY.value = e.contentOffset.y + clampedScrollY.value = Math.min(e.contentOffset.y, headerOnlyHeight) }, }) + useAnimatedReaction( + () => clampedScrollY.value === headerOnlyHeight, + (nextIsScrolledDown, prevIsScrolledDown) => { + if (nextIsScrolledDown !== prevIsScrolledDown) { + runOnJS(setIsScrolledDown)(nextIsScrolledDown) + } + }, + ) const onPageSelectedInner = React.useCallback( (index: number) => { @@ -170,6 +166,7 @@ export const PagerWithHeader = React.forwardRef( headerHeight={headerHeight} isScrolledDown={isScrolledDown} onScroll={i === currentPage ? onScroll : noop} + forcedScrollY={i === currentPage ? null : clampedScrollY} renderTab={ isHeaderReady && headerOnlyHeight > 0 && tabBarHeight > 0 ? child @@ -185,9 +182,41 @@ export const PagerWithHeader = React.forwardRef( ) function PagerItem( - {headerHeight, isScrolledDown, onScroll, renderTab}: any /* TODO */, + { + headerHeight, + isScrolledDown, + onScroll, + renderTab, + forcedScrollY, + }: any /* TODO */, ) { const scrollElRef = useAnimatedRef() + useAnimatedReaction( + () => { + if (forcedScrollY) { + return forcedScrollY.value + } else { + // Active tab doesn't get synced. + return null + } + }, + (nextForcedY, prevForcedY) => { + if (nextForcedY === prevForcedY) { + return + } + if (nextForcedY == null) { + // This tab just became active. It was already being synced before. + // So we don't need to do anything. + return + } + if (prevForcedY == null) { + // This tab just became inactive, so it used to be the source of truth. + // There is no need to sync it until we get more scroll events. + return + } + scrollTo(scrollElRef, 0, nextForcedY, false) + }, + ) if (renderTab == null) { return null }