Scroll sync in the pager without jumps (#1863)
parent
65def37165
commit
91f8a23fbc
|
@ -1,6 +1,7 @@
|
|||
import React, {MutableRefObject} from 'react'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Dimensions,
|
||||
RefreshControl,
|
||||
StyleProp,
|
||||
View,
|
||||
|
@ -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'
|
||||
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -2,6 +2,7 @@ import React, {MutableRefObject} from 'react'
|
|||
import {observer} from 'mobx-react-lite'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Dimensions,
|
||||
RefreshControl,
|
||||
StyleProp,
|
||||
StyleSheet,
|
||||
|
@ -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'
|
||||
|
@ -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}
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
import React, {useMemo, useCallback} from 'react'
|
||||
import {FlatList, StyleSheet, View, ActivityIndicator} from 'react-native'
|
||||
import {
|
||||
Dimensions,
|
||||
StyleSheet,
|
||||
View,
|
||||
ActivityIndicator,
|
||||
FlatList,
|
||||
} from 'react-native'
|
||||
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
|
@ -343,16 +349,19 @@ export const ProfileFeedScreenInner = observer(
|
|||
isHeaderReady={feedInfo?.hasLoaded ?? false}
|
||||
renderHeader={renderHeader}
|
||||
onCurrentPageSelected={onCurrentPageSelected}>
|
||||
{({onScroll, headerHeight, isScrolledDown}) => (
|
||||
{({onScroll, headerHeight, isScrolledDown, scrollElRef}) => (
|
||||
<FeedSection
|
||||
ref={feedSectionRef}
|
||||
feed={feed}
|
||||
onScroll={onScroll}
|
||||
headerHeight={headerHeight}
|
||||
isScrolledDown={isScrolledDown}
|
||||
scrollElRef={
|
||||
scrollElRef as React.MutableRefObject<FlatList<any> | null>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{({onScroll, headerHeight}) => (
|
||||
{({onScroll, headerHeight, scrollElRef}) => (
|
||||
<AboutSection
|
||||
feedOwnerDid={feedOwnerDid}
|
||||
feedRkey={rkey}
|
||||
|
@ -360,6 +369,9 @@ export const ProfileFeedScreenInner = observer(
|
|||
headerHeight={headerHeight}
|
||||
onToggleLiked={onToggleLiked}
|
||||
onScroll={onScroll}
|
||||
scrollElRef={
|
||||
scrollElRef as React.MutableRefObject<ScrollView | null>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</PagerWithHeader>
|
||||
|
@ -387,14 +399,14 @@ interface FeedSectionProps {
|
|||
onScroll: OnScrollHandler
|
||||
headerHeight: number
|
||||
isScrolledDown: boolean
|
||||
scrollElRef: React.MutableRefObject<FlatList<any> | null>
|
||||
}
|
||||
const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
|
||||
function FeedSectionImpl(
|
||||
{feed, onScroll, headerHeight, isScrolledDown},
|
||||
{feed, onScroll, headerHeight, isScrolledDown, scrollElRef},
|
||||
ref,
|
||||
) {
|
||||
const hasNew = feed.hasNewLatest && !feed.isRefreshing
|
||||
const scrollElRef = React.useRef<FlatList>(null)
|
||||
|
||||
const onScrollToTop = useCallback(() => {
|
||||
scrollElRef.current?.scrollToOffset({offset: -headerHeight})
|
||||
|
@ -438,6 +450,7 @@ const AboutSection = observer(function AboutPageImpl({
|
|||
headerHeight,
|
||||
onToggleLiked,
|
||||
onScroll,
|
||||
scrollElRef,
|
||||
}: {
|
||||
feedOwnerDid: string
|
||||
feedRkey: string
|
||||
|
@ -445,6 +458,7 @@ const AboutSection = observer(function AboutPageImpl({
|
|||
headerHeight: number
|
||||
onToggleLiked: () => void
|
||||
onScroll: OnScrollHandler
|
||||
scrollElRef: React.MutableRefObject<ScrollView | null>
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
|
@ -456,8 +470,12 @@ const AboutSection = observer(function AboutPageImpl({
|
|||
|
||||
return (
|
||||
<ScrollView
|
||||
ref={scrollElRef}
|
||||
scrollEventThrottle={1}
|
||||
contentContainerStyle={{paddingTop: headerHeight}}
|
||||
contentContainerStyle={{
|
||||
paddingTop: headerHeight,
|
||||
paddingBottom: Dimensions.get('window').height - headerHeight,
|
||||
}}
|
||||
onScroll={scrollHandler}>
|
||||
<View
|
||||
style={[
|
||||
|
|
|
@ -175,18 +175,24 @@ export const ProfileListScreenInner = observer(
|
|||
isHeaderReady={list.hasLoaded}
|
||||
renderHeader={renderHeader}
|
||||
onCurrentPageSelected={onCurrentPageSelected}>
|
||||
{({onScroll, headerHeight, isScrolledDown}) => (
|
||||
{({onScroll, headerHeight, isScrolledDown, scrollElRef}) => (
|
||||
<FeedSection
|
||||
ref={feedSectionRef}
|
||||
scrollElRef={
|
||||
scrollElRef as React.MutableRefObject<FlatList<any> | null>
|
||||
}
|
||||
feed={feed}
|
||||
onScroll={onScroll}
|
||||
headerHeight={headerHeight}
|
||||
isScrolledDown={isScrolledDown}
|
||||
/>
|
||||
)}
|
||||
{({onScroll, headerHeight, isScrolledDown}) => (
|
||||
{({onScroll, headerHeight, isScrolledDown, scrollElRef}) => (
|
||||
<AboutSection
|
||||
ref={aboutSectionRef}
|
||||
scrollElRef={
|
||||
scrollElRef as React.MutableRefObject<FlatList<any> | null>
|
||||
}
|
||||
list={list}
|
||||
descriptionRT={list.descriptionRT}
|
||||
creator={list.data ? list.data.creator : undefined}
|
||||
|
@ -223,9 +229,12 @@ export const ProfileListScreenInner = observer(
|
|||
items={SECTION_TITLES_MOD}
|
||||
isHeaderReady={list.hasLoaded}
|
||||
renderHeader={renderHeader}>
|
||||
{({onScroll, headerHeight, isScrolledDown}) => (
|
||||
{({onScroll, headerHeight, isScrolledDown, scrollElRef}) => (
|
||||
<AboutSection
|
||||
list={list}
|
||||
scrollElRef={
|
||||
scrollElRef as React.MutableRefObject<FlatList<any> | null>
|
||||
}
|
||||
descriptionRT={list.descriptionRT}
|
||||
creator={list.data ? list.data.creator : undefined}
|
||||
isCurateList={list.isCuratelist}
|
||||
|
@ -557,14 +566,14 @@ interface FeedSectionProps {
|
|||
onScroll: OnScrollHandler
|
||||
headerHeight: number
|
||||
isScrolledDown: boolean
|
||||
scrollElRef: React.MutableRefObject<FlatList<any> | null>
|
||||
}
|
||||
const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
|
||||
function FeedSectionImpl(
|
||||
{feed, onScroll, headerHeight, isScrolledDown},
|
||||
{feed, scrollElRef, onScroll, headerHeight, isScrolledDown},
|
||||
ref,
|
||||
) {
|
||||
const hasNew = feed.hasNewLatest && !feed.isRefreshing
|
||||
const scrollElRef = React.useRef<FlatList>(null)
|
||||
|
||||
const onScrollToTop = useCallback(() => {
|
||||
scrollElRef.current?.scrollToOffset({offset: -headerHeight})
|
||||
|
@ -611,6 +620,7 @@ interface AboutSectionProps {
|
|||
onScroll: OnScrollHandler
|
||||
headerHeight: number
|
||||
isScrolledDown: boolean
|
||||
scrollElRef: React.MutableRefObject<FlatList<any> | null>
|
||||
}
|
||||
const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
|
||||
function AboutSectionImpl(
|
||||
|
@ -624,13 +634,13 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
|
|||
onScroll,
|
||||
headerHeight,
|
||||
isScrolledDown,
|
||||
scrollElRef,
|
||||
},
|
||||
ref,
|
||||
) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const scrollElRef = React.useRef<FlatList>(null)
|
||||
|
||||
const onScrollToTop = useCallback(() => {
|
||||
scrollElRef.current?.scrollToOffset({offset: -headerHeight})
|
||||
|
|
Loading…
Reference in New Issue