diff options
author | dan <dan.abramov@gmail.com> | 2024-12-03 01:29:45 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-12-03 01:29:45 +0000 |
commit | cd811114ef0fc1164b8909e3debda792cd2a659c (patch) | |
tree | c866398bb1fec935c3d61305a0530d220fcbad1d /src/view/com/pager/Pager.tsx | |
parent | 5a313c2d10b112458830b3bfc708031f6f8726a0 (diff) | |
download | voidsky-cd811114ef0fc1164b8909e3debda792cd2a659c.tar.zst |
[Nicer Tabs] New native pager (#6868)
* Remove tab bar autoscroll This will be replaced by a different mechanism. * Track pager drag gesture in a worklet * Track pager state change in a worklet * Track offset relative to current page * Sync scroll to swipe * Extract TabBarItem * Sync scroll to swipe properly * Implement all interactions * Clarify more hacks * Simplify the implementation I was trying to be too smart and this was causing the current page event to lag behind if you continuously drag. Better to let the library do its job. * Interpolate the indicator * Fix an infinite swipe loop * Add TODO * Animate header color * Respect initial page * Keep layouts in a shared value * Fix profile and types * Fast path for initial styles * Scroll to initial * Factor out a helper * Fix positioning * Scroll into view on tap if needed * Divide free space proportionally * Scroll into view more aggressively * Fix corner case * Ignore spurious event on iOS * Simplify the condition Due to RN onLayout event ordering, we know that by now we'll have container and content sizes already. * Change boolean state to enum * Better syncing heuristic * Rm extra return
Diffstat (limited to 'src/view/com/pager/Pager.tsx')
-rw-r--r-- | src/view/com/pager/Pager.tsx | 115 |
1 files changed, 95 insertions, 20 deletions
diff --git a/src/view/com/pager/Pager.tsx b/src/view/com/pager/Pager.tsx index f0e686b6a..da7fd1e93 100644 --- a/src/view/com/pager/Pager.tsx +++ b/src/view/com/pager/Pager.tsx @@ -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' @@ -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 @@ -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, () => ({ @@ -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( @@ -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, + ) +} |