Force scroll sync for inactive tabs

This commit is contained in:
Dan Abramov
2023-11-10 03:29:29 +00:00
parent 03789cd77c
commit a3c14b8108
+47 -18
View File
@@ -6,6 +6,7 @@ import Animated, {
useAnimatedStyle, useAnimatedStyle,
useSharedValue, useSharedValue,
runOnJS, runOnJS,
scrollTo,
useAnimatedRef, useAnimatedRef,
} from 'react-native-reanimated' } from 'react-native-reanimated'
import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager' import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager'
@@ -13,8 +14,6 @@ import {TabBar} from './TabBar'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {OnScrollCb} from 'lib/hooks/useOnMainScroll' import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
const SCROLLED_DOWN_LIMIT = 200
interface PagerWithHeaderChildParams { interface PagerWithHeaderChildParams {
headerHeight: number headerHeight: number
onScroll: OnScrollCb onScroll: OnScrollCb
@@ -50,21 +49,13 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
) { ) {
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const [currentPage, setCurrentPage] = React.useState(0) const [currentPage, setCurrentPage] = React.useState(0)
const scrollY = useSharedValue(0) const clampedScrollY = useSharedValue(0)
const [tabBarHeight, setTabBarHeight] = React.useState(0) const [tabBarHeight, setTabBarHeight] = React.useState(0)
const [headerOnlyHeight, setHeaderOnlyHeight] = React.useState(0) const [headerOnlyHeight, setHeaderOnlyHeight] = React.useState(0)
const [isScrolledDown, setIsScrolledDown] = React.useState(false) const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const headerHeight = headerOnlyHeight + tabBarHeight 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 // capture the header bar sizing
const onTabBarLayout = React.useCallback( const onTabBarLayout = React.useCallback(
(evt: LayoutChangeEvent) => { (evt: LayoutChangeEvent) => {
@@ -84,14 +75,11 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
() => ({ () => ({
transform: [ transform: [
{ {
translateY: Math.min( translateY: Math.min(-clampedScrollY.value, 0),
Math.min(scrollY.value, headerOnlyHeight) * -1,
0,
),
}, },
], ],
}), }),
[scrollY, headerHeight, tabBarHeight], [clampedScrollY, headerHeight, tabBarHeight],
) )
const renderTabBar = React.useCallback( const renderTabBar = React.useCallback(
(props: RenderTabBarFnProps) => { (props: RenderTabBarFnProps) => {
@@ -136,9 +124,17 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
// props to pass into children render functions // props to pass into children render functions
const onScroll = useAnimatedScrollHandler({ const onScroll = useAnimatedScrollHandler({
onScroll(e) { 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( const onPageSelectedInner = React.useCallback(
(index: number) => { (index: number) => {
@@ -170,6 +166,7 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
headerHeight={headerHeight} headerHeight={headerHeight}
isScrolledDown={isScrolledDown} isScrolledDown={isScrolledDown}
onScroll={i === currentPage ? onScroll : noop} onScroll={i === currentPage ? onScroll : noop}
forcedScrollY={i === currentPage ? null : clampedScrollY}
renderTab={ renderTab={
isHeaderReady && headerOnlyHeight > 0 && tabBarHeight > 0 isHeaderReady && headerOnlyHeight > 0 && tabBarHeight > 0
? child ? child
@@ -185,9 +182,41 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
) )
function PagerItem( function PagerItem(
{headerHeight, isScrolledDown, onScroll, renderTab}: any /* TODO */, {
headerHeight,
isScrolledDown,
onScroll,
renderTab,
forcedScrollY,
}: any /* TODO */,
) { ) {
const scrollElRef = useAnimatedRef() 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) { if (renderTab == null) {
return null return null
} }