Skip to content

Commit

Permalink
Scroll sync in the pager without jumps (#1863)
Browse files Browse the repository at this point in the history
  • Loading branch information
gaearon authored Nov 10, 2023
1 parent 65def37 commit 91f8a23
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 87 deletions.
6 changes: 4 additions & 2 deletions src/view/com/lists/ListItems.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, {MutableRefObject} from 'react'
import {
ActivityIndicator,
Dimensions,
RefreshControl,
StyleProp,
View,
Expand All @@ -18,7 +19,6 @@ import {ListModel} from 'state/models/content/list'
import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {s} from 'lib/styles'
import {OnScrollHandler} from 'lib/hooks/useOnMainScroll'
import {logger} from '#/logger'
import {useModalControls} from '#/state/modals'
Expand Down Expand Up @@ -226,7 +226,9 @@ export const ListItems = observer(function ListItemsImpl({
progressViewOffset={headerOffset}
/>
}
contentContainerStyle={s.contentContainer}
contentContainerStyle={{
paddingBottom: Dimensions.get('window').height - headerOffset,
}}
style={{paddingTop: headerOffset}}
onScroll={scrollHandler}
onEndReached={onEndReached}
Expand Down
13 changes: 12 additions & 1 deletion src/view/com/pager/Pager.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,18 @@ export const Pager = React.forwardRef(function PagerImpl(
onSelect: onTabBarSelect,
})}
{React.Children.map(children, (child, i) => (
<View style={selectedPage === i ? s.flex1 : s.hidden} key={`page-${i}`}>
<View
style={
selectedPage === i
? s.flex1
: {
position: 'absolute',
pointerEvents: 'none',
// @ts-ignore web-only
visibility: 'hidden',
}
}
key={`page-${i}`}>
{child}
</View>
))}
Expand Down
170 changes: 100 additions & 70 deletions src/view/com/pager/PagerWithHeader.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -24,6 +26,7 @@ interface PagerWithHeaderChildParams {
headerHeight: number
onScroll: OnScrollHandler
isScrolledDown: boolean
scrollElRef: React.MutableRefObject<FlatList<any> | ScrollView | null>
}

export interface PagerWithHeaderProps {
Expand Down Expand Up @@ -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) => {
Expand All @@ -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 (
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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>
)
})}
Expand All @@ -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',
Expand Down
6 changes: 4 additions & 2 deletions src/view/com/posts/Feed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, {MutableRefObject} from 'react'
import {observer} from 'mobx-react-lite'
import {
ActivityIndicator,
Dimensions,
RefreshControl,
StyleProp,
StyleSheet,
Expand All @@ -15,7 +16,6 @@ import {PostsFeedModel} from 'state/models/feeds/posts'
import {FeedSlice} from './FeedSlice'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
import {OnScrollHandler} from 'lib/hooks/useOnMainScroll'
import {s} from 'lib/styles'
import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
Expand Down Expand Up @@ -178,7 +178,9 @@ export const Feed = observer(function Feed({
progressViewOffset={headerOffset}
/>
}
contentContainerStyle={s.contentContainer}
contentContainerStyle={{
paddingBottom: Dimensions.get('window').height - headerOffset,
}}
style={{paddingTop: headerOffset}}
onScroll={onScroll != null ? scrollHandler : undefined}
scrollEventThrottle={scrollEventThrottle}
Expand Down
Loading

0 comments on commit 91f8a23

Please sign in to comment.