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

[Nicer Tabs] New native pager #6868

Merged
merged 30 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
7429be2
Remove tab bar autoscroll
gaearon Nov 26, 2024
d8a5515
Track pager drag gesture in a worklet
gaearon Nov 26, 2024
32120ce
Track pager state change in a worklet
gaearon Nov 27, 2024
35c451f
Track offset relative to current page
gaearon Nov 29, 2024
7cad249
Sync scroll to swipe
gaearon Nov 29, 2024
63388e8
Extract TabBarItem
gaearon Nov 29, 2024
ca1ee56
Sync scroll to swipe properly
gaearon Nov 29, 2024
21ff405
Implement all interactions
gaearon Nov 30, 2024
23f900c
Clarify more hacks
gaearon Nov 30, 2024
5d9291d
Simplify the implementation
gaearon Nov 30, 2024
6f9bc56
Interpolate the indicator
gaearon Nov 30, 2024
2811862
Fix an infinite swipe loop
gaearon Nov 30, 2024
65842a7
Add TODO
gaearon Nov 30, 2024
01ad1d8
Animate header color
gaearon Nov 30, 2024
9b94333
Respect initial page
gaearon Nov 30, 2024
4f33328
Keep layouts in a shared value
gaearon Nov 30, 2024
f3e9a87
Fix profile and types
gaearon Nov 30, 2024
fadb2d0
Fast path for initial styles
gaearon Nov 30, 2024
dd45704
Scroll to initial
gaearon Nov 30, 2024
80bd82e
Factor out a helper
gaearon Nov 30, 2024
287d5b3
Fix positioning
gaearon Nov 30, 2024
a0deb8f
Scroll into view on tap if needed
gaearon Nov 30, 2024
f129a92
Divide free space proportionally
gaearon Dec 1, 2024
3325752
Scroll into view more aggressively
gaearon Dec 1, 2024
cf586df
Fix corner case
gaearon Dec 1, 2024
a93e2b9
Ignore spurious event on iOS
gaearon Dec 2, 2024
44cea4e
Simplify the condition
gaearon Dec 2, 2024
05da3ad
Change boolean state to enum
gaearon Dec 2, 2024
50f50fb
Better syncing heuristic
gaearon Dec 2, 2024
129070f
Rm extra return
gaearon Dec 2, 2024
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
5 changes: 2 additions & 3 deletions src/view/com/home/HomeHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React from 'react'
import {useNavigation} from '@react-navigation/native'

import {usePalette} from '#/lib/hooks/usePalette'
import {NavigationProp} from '#/lib/routes/types'
import {FeedSourceInfo} from '#/state/queries/feed'
import {useSession} from '#/state/session'
Expand All @@ -19,7 +18,6 @@ export function HomeHeader(
const {feeds} = props
const {hasSession} = useSession()
const navigation = useNavigation<NavigationProp>()
const pal = usePalette('default')

const hasPinnedCustom = React.useMemo<boolean>(() => {
if (!hasSession) return false
Expand Down Expand Up @@ -61,7 +59,8 @@ export function HomeHeader(
onSelect={onSelect}
testID={props.testID}
items={items}
indicatorColor={pal.colors.link}
dragProgress={props.dragProgress}
dragState={props.dragState}
/>
</HomeHeaderLayout>
)
Expand Down
115 changes: 95 additions & 20 deletions src/view/com/pager/Pager.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import React, {forwardRef} from 'react'
import {View} from 'react-native'
import PagerView, {
PagerViewOnPageScrollEventData,
PagerViewOnPageSelectedEvent,
PageScrollStateChangedNativeEvent,
PagerViewOnPageSelectedEventData,
PageScrollStateChangedNativeEventData,
} from 'react-native-pager-view'
import Animated, {
runOnJS,
SharedValue,
useEvent,
useHandler,
useSharedValue,
} from 'react-native-reanimated'

import {atoms as a, native} from '#/alf'

Expand All @@ -17,6 +26,8 @@ export interface RenderTabBarFnProps {
selectedPage: number
onSelect?: (index: number) => void
tabBarAnchor?: JSX.Element | null | undefined // Ignored on native.
dragProgress: SharedValue<number> // Ignored on web.
dragState: SharedValue<'idle' | 'dragging' | 'settling'> // Ignored on web.
}
export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element

Expand All @@ -29,19 +40,22 @@ interface Props {
) => void
testID?: string
}

const AnimatedPagerView = Animated.createAnimatedComponent(PagerView)

export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
function PagerImpl(
{
children,
initialPage = 0,
renderTabBar,
onPageScrollStateChanged,
onPageSelected,
onPageScrollStateChanged: parentOnPageScrollStateChanged,
onPageSelected: parentOnPageSelected,
testID,
}: React.PropsWithChildren<Props>,
ref,
) {
const [selectedPage, setSelectedPage] = React.useState(0)
const [selectedPage, setSelectedPage] = React.useState(initialPage)
const pagerView = React.useRef<PagerView>(null)

React.useImperativeHandle(ref, () => ({
Expand All @@ -50,19 +64,12 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
},
}))

const onPageSelectedInner = React.useCallback(
(e: PageSelectedEvent) => {
setSelectedPage(e.nativeEvent.position)
onPageSelected?.(e.nativeEvent.position)
},
[setSelectedPage, onPageSelected],
)

const handlePageScrollStateChanged = React.useCallback(
(e: PageScrollStateChangedNativeEvent) => {
onPageScrollStateChanged?.(e.nativeEvent.pageScrollState)
const onPageSelectedJSThread = React.useCallback(
(nextPosition: number) => {
setSelectedPage(nextPosition)
parentOnPageSelected?.(nextPosition)
},
[onPageScrollStateChanged],
[setSelectedPage, parentOnPageSelected],
)

const onTabBarSelect = React.useCallback(
Expand All @@ -72,21 +79,89 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
[pagerView],
)

const dragState = useSharedValue<'idle' | 'settling' | 'dragging'>('idle')
const dragProgress = useSharedValue(selectedPage)
const didInit = useSharedValue(false)
const handlePageScroll = usePagerHandlers(
{
onPageScroll(e: PagerViewOnPageScrollEventData) {
'worklet'
if (didInit.get() === false) {
// On iOS, there's a spurious scroll event with 0 position
// even if a different page was supplied as the initial page.
// Ignore it and wait for the first confirmed selection instead.
return
}
dragProgress.set(e.offset + e.position)
},
onPageScrollStateChanged(e: PageScrollStateChangedNativeEventData) {
'worklet'
if (dragState.get() === 'idle' && e.pageScrollState === 'settling') {
// This is a programmatic scroll on Android.
// Stay "idle" to match iOS and avoid confusing downstream code.
return
}
dragState.set(e.pageScrollState)
parentOnPageScrollStateChanged?.(e.pageScrollState)
},
onPageSelected(e: PagerViewOnPageSelectedEventData) {
'worklet'
didInit.set(true)
runOnJS(onPageSelectedJSThread)(e.position)
},
},
[parentOnPageScrollStateChanged],
)

return (
<View testID={testID} style={[a.flex_1, native(a.overflow_hidden)]}>
{renderTabBar({
selectedPage,
onSelect: onTabBarSelect,
dragProgress,
dragState,
})}
<PagerView
<AnimatedPagerView
ref={pagerView}
style={[a.flex_1]}
initialPage={initialPage}
onPageScrollStateChanged={handlePageScrollStateChanged}
onPageSelected={onPageSelectedInner}>
onPageScroll={handlePageScroll}>
{children}
</PagerView>
</AnimatedPagerView>
</View>
)
},
)

function usePagerHandlers(
handlers: {
onPageScroll: (e: PagerViewOnPageScrollEventData) => void
onPageScrollStateChanged: (e: PageScrollStateChangedNativeEventData) => void
onPageSelected: (e: PagerViewOnPageSelectedEventData) => void
},
dependencies: unknown[],
) {
const {doDependenciesDiffer} = useHandler(handlers as any, dependencies)
const subscribeForEvents = [
'onPageScroll',
'onPageScrollStateChanged',
'onPageSelected',
]
return useEvent(
event => {
'worklet'
const {onPageScroll, onPageScrollStateChanged, onPageSelected} = handlers
if (event.eventName.endsWith('onPageScroll')) {
onPageScroll(event as any as PagerViewOnPageScrollEventData)
} else if (event.eventName.endsWith('onPageScrollStateChanged')) {
onPageScrollStateChanged(
event as any as PageScrollStateChangedNativeEventData,
)
} else if (event.eventName.endsWith('onPageSelected')) {
onPageSelected(event as any as PagerViewOnPageSelectedEventData)
}
},
subscribeForEvents,
doDependenciesDiffer,
)
}
8 changes: 8 additions & 0 deletions src/view/com/pager/PagerWithHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
scrollY={scrollY}
testID={testID}
allowHeaderOverScroll={allowHeaderOverScroll}
dragProgress={props.dragProgress}
dragState={props.dragState}
/>
</PagerHeaderProvider>
)
Expand Down Expand Up @@ -226,6 +228,8 @@ let PagerTabBar = ({
onCurrentPageSelected,
onSelect,
allowHeaderOverScroll,
dragProgress,
dragState,
}: {
currentPage: number
headerOnlyHeight: number
Expand All @@ -239,6 +243,8 @@ let PagerTabBar = ({
onCurrentPageSelected?: (index: number) => void
onSelect?: (index: number) => void
allowHeaderOverScroll?: boolean
dragProgress: SharedValue<number>
dragState: SharedValue<'idle' | 'dragging' | 'settling'>
}): React.ReactNode => {
const headerTransform = useAnimatedStyle(() => {
const translateY = Math.min(scrollY.get(), headerOnlyHeight) * -1
Expand Down Expand Up @@ -297,6 +303,8 @@ let PagerTabBar = ({
selectedPage={currentPage}
onSelect={onSelect}
onPressSelected={onCurrentPageSelected}
dragProgress={dragProgress}
dragState={dragState}
/>
</View>
</Animated.View>
Expand Down
2 changes: 2 additions & 0 deletions src/view/com/pager/PagerWithHeader.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ let PagerTabBar = ({
selectedPage={currentPage}
onSelect={onSelect}
onPressSelected={onCurrentPageSelected}
dragProgress={undefined as any /* native-only */}
dragState={undefined as any /* native-only */}
Comment on lines +154 to +155
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wish there was a way to express platforms in the type system :/

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could probably hack something together for this in the future

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, some sort of monad perhaps

/>
</View>
</>
Expand Down
Loading
Loading