Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix sticky pager jumps #1825

Merged
merged 6 commits into from
Nov 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 44 additions & 20 deletions src/view/com/pager/PagerWithHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react'
import {LayoutChangeEvent, StyleSheet} from 'react-native'
import {LayoutChangeEvent, StyleSheet, View} from 'react-native'
import Animated, {
Easing,
useAnimatedReaction,
Expand Down Expand Up @@ -28,6 +28,7 @@ export interface PagerWithHeaderProps {
| (((props: PagerWithHeaderChildParams) => JSX.Element) | null)[]
| ((props: PagerWithHeaderChildParams) => JSX.Element)
items: string[]
isHeaderReady: boolean
renderHeader?: () => JSX.Element
initialPage?: number
onPageSelected?: (index: number) => void
Expand All @@ -39,6 +40,7 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
children,
testID,
items,
isHeaderReady,
renderHeader,
initialPage,
onPageSelected,
Expand All @@ -51,15 +53,17 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
const scrollYs = React.useRef<Record<number, number>>({})
const scrollY = useSharedValue(scrollYs.current[currentPage] || 0)
const [tabBarHeight, setTabBarHeight] = React.useState(0)
const [headerHeight, setHeaderHeight] = React.useState(0)
const [headerOnlyHeight, setHeaderOnlyHeight] = React.useState(0)
const [isScrolledDown, setIsScrolledDown] = React.useState(
scrollYs.current[currentPage] > SCROLLED_DOWN_LIMIT,
)

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, headerHeight - tabBarHeight)
scrollYs.current[currentPage] = Math.min(v, headerOnlyHeight)
// update the 'is scrolled down' value
setIsScrolledDown(v > SCROLLED_DOWN_LIMIT)
}
Expand All @@ -75,11 +79,11 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
},
[setTabBarHeight],
)
const onHeaderLayout = React.useCallback(
const onHeaderOnlyLayout = React.useCallback(
(evt: LayoutChangeEvent) => {
setHeaderHeight(evt.nativeEvent.layout.height)
setHeaderOnlyHeight(evt.nativeEvent.layout.height)
},
[setHeaderHeight],
[setHeaderOnlyHeight],
)

// render the the header and tab bar
Expand All @@ -88,7 +92,7 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
transform: [
{
translateY: Math.min(
Math.min(scrollY.value, headerHeight - tabBarHeight) * -1,
Math.min(scrollY.value, headerOnlyHeight) * -1,
0,
),
},
Expand All @@ -100,31 +104,39 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
(props: RenderTabBarFnProps) => {
return (
<Animated.View
onLayout={onHeaderLayout}
style={[
isMobile ? styles.tabBarMobile : styles.tabBarDesktop,
headerTransform,
]}>
{renderHeader?.()}
<TabBar
items={items}
selectedPage={currentPage}
onSelect={props.onSelect}
onPressSelected={onCurrentPageSelected}
<View onLayout={onHeaderOnlyLayout}>{renderHeader?.()}</View>
<View
onLayout={onTabBarLayout}
/>
style={{
// Render it immediately to measure it early since its size doesn't depend on the content.
// However, keep it invisible until the header above stabilizes in order to prevent jumps.
opacity: isHeaderReady ? 1 : 0,
pointerEvents: isHeaderReady ? 'auto' : 'none',
}}>
<TabBar
items={items}
selectedPage={currentPage}
onSelect={props.onSelect}
onPressSelected={onCurrentPageSelected}
/>
</View>
</Animated.View>
)
},
[
items,
isHeaderReady,
renderHeader,
headerTransform,
currentPage,
onCurrentPageSelected,
isMobile,
onTabBarLayout,
onHeaderLayout,
onHeaderOnlyLayout,
],
)

Expand Down Expand Up @@ -175,11 +187,23 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
tabBarPosition="top">
{toArray(children)
.filter(Boolean)
.map(child => {
if (child) {
return child(childProps)
.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(childProps)
}
return null
// Pager children must be noncollapsible plain <View>s.
return (
<View key={i} collapsable={false}>
{output}
</View>
)
})}
</Pager>
)
Expand Down
4 changes: 1 addition & 3 deletions src/view/com/pager/TabBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ export interface TabBarProps {
indicatorColor?: string
onSelect?: (index: number) => void
onPressSelected?: (index: number) => void
onLayout?: (evt: LayoutChangeEvent) => void
}

export function TabBar({
Expand All @@ -24,7 +23,6 @@ export function TabBar({
indicatorColor,
onSelect,
onPressSelected,
onLayout,
}: TabBarProps) {
const pal = usePalette('default')
const scrollElRef = useRef<ScrollView>(null)
Expand Down Expand Up @@ -68,7 +66,7 @@ export function TabBar({
const styles = isDesktop || isTablet ? desktopStyles : mobileStyles

return (
<View testID={testID} style={[pal.view, styles.outer]} onLayout={onLayout}>
<View testID={testID} style={[pal.view, styles.outer]}>
<DraggableScrollView
horizontal={true}
showsHorizontalScrollIndicator={false}
Expand Down
3 changes: 1 addition & 2 deletions src/view/screens/ProfileFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -332,11 +332,11 @@ export const ProfileFeedScreenInner = observer(
<View style={s.hContentRegion}>
<PagerWithHeader
items={SECTION_TITLES}
isHeaderReady={feedInfo?.hasLoaded ?? false}
renderHeader={renderHeader}
onCurrentPageSelected={onCurrentPageSelected}>
{({onScroll, headerHeight, isScrolledDown}) => (
<FeedSection
key="1"
ref={feedSectionRef}
feed={feed}
onScroll={onScroll}
Expand All @@ -346,7 +346,6 @@ export const ProfileFeedScreenInner = observer(
)}
{({onScroll, headerHeight}) => (
<ScrollView
key="2"
onScroll={onScroll}
scrollEventThrottle={1}
contentContainerStyle={{paddingTop: headerHeight}}>
Expand Down
5 changes: 2 additions & 3 deletions src/view/screens/ProfileList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,11 @@ export const ProfileListScreenInner = observer(
<View style={s.hContentRegion}>
<PagerWithHeader
items={SECTION_TITLES_CURATE}
isHeaderReady={list.hasLoaded}
renderHeader={renderHeader}
onCurrentPageSelected={onCurrentPageSelected}>
{({onScroll, headerHeight, isScrolledDown}) => (
<FeedSection
key="1"
ref={feedSectionRef}
feed={feed}
onScroll={onScroll}
Expand All @@ -179,7 +179,6 @@ export const ProfileListScreenInner = observer(
)}
{({onScroll, headerHeight, isScrolledDown}) => (
<AboutSection
key="2"
ref={aboutSectionRef}
list={list}
descriptionRT={list.descriptionRT}
Expand Down Expand Up @@ -215,10 +214,10 @@ export const ProfileListScreenInner = observer(
<View style={s.hContentRegion}>
<PagerWithHeader
items={SECTION_TITLES_MOD}
isHeaderReady={list.hasLoaded}
renderHeader={renderHeader}>
{({onScroll, headerHeight, isScrolledDown}) => (
<AboutSection
key="2"
list={list}
descriptionRT={list.descriptionRT}
creator={list.data ? list.data.creator : undefined}
Expand Down