diff options
Diffstat (limited to 'src/view/com/pager/PagerWithHeader.tsx')
-rw-r--r-- | src/view/com/pager/PagerWithHeader.tsx | 315 |
1 files changed, 155 insertions, 160 deletions
diff --git a/src/view/com/pager/PagerWithHeader.tsx b/src/view/com/pager/PagerWithHeader.tsx index 1746d2ca1..57aaac074 100644 --- a/src/view/com/pager/PagerWithHeader.tsx +++ b/src/view/com/pager/PagerWithHeader.tsx @@ -1,17 +1,16 @@ -import * as React from 'react' +import {memo, useCallback, useEffect, useRef, useState} from 'react' import { - LayoutChangeEvent, - NativeScrollEvent, - ScrollView, + type LayoutChangeEvent, + type NativeScrollEvent, + type ScrollView, StyleSheet, View, } from 'react-native' import Animated, { - AnimatedRef, - runOnJS, + type AnimatedRef, runOnUI, scrollTo, - SharedValue, + type SharedValue, useAnimatedRef, useAnimatedStyle, useSharedValue, @@ -20,9 +19,13 @@ import Animated, { import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {ScrollProvider} from '#/lib/ScrollContext' import {isIOS} from '#/platform/detection' -import {Pager, PagerRef, RenderTabBarFnProps} from '#/view/com/pager/Pager' +import { + Pager, + type PagerRef, + type RenderTabBarFnProps, +} from '#/view/com/pager/Pager' import {useTheme} from '#/alf' -import {ListMethods} from '../util/List' +import {type ListMethods} from '../util/List' import {PagerHeaderProvider} from './PagerHeaderContext' import {TabBar} from './TabBar' @@ -33,6 +36,7 @@ export interface PagerWithHeaderChildParams { } export interface PagerWithHeaderProps { + ref?: React.Ref<PagerRef> testID?: string children: | (((props: PagerWithHeaderChildParams) => JSX.Element) | null)[] @@ -49,97 +53,94 @@ export interface PagerWithHeaderProps { onCurrentPageSelected?: (index: number) => void allowHeaderOverScroll?: boolean } -export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>( - function PageWithHeaderImpl( - { - children, - testID, +export function PagerWithHeader({ + ref, + children, + testID, + items, + isHeaderReady, + renderHeader, + initialPage, + onPageSelected, + onCurrentPageSelected, + allowHeaderOverScroll, +}: PagerWithHeaderProps) { + const [currentPage, setCurrentPage] = useState(0) + const [tabBarHeight, setTabBarHeight] = useState(0) + const [headerOnlyHeight, setHeaderOnlyHeight] = useState(0) + const scrollY = useSharedValue(0) + const headerHeight = headerOnlyHeight + tabBarHeight + + // capture the header bar sizing + const onTabBarLayout = useNonReactiveCallback((evt: LayoutChangeEvent) => { + const height = evt.nativeEvent.layout.height + if (height > 0) { + // The rounding is necessary to prevent jumps on iOS + setTabBarHeight(Math.round(height * 2) / 2) + } + }) + const onHeaderOnlyLayout = useNonReactiveCallback((height: number) => { + if (height > 0) { + // The rounding is necessary to prevent jumps on iOS + setHeaderOnlyHeight(Math.round(height * 2) / 2) + } + }) + + const renderTabBar = useCallback( + (props: RenderTabBarFnProps) => { + return ( + <PagerHeaderProvider scrollY={scrollY} headerHeight={headerOnlyHeight}> + <PagerTabBar + headerOnlyHeight={headerOnlyHeight} + items={items} + isHeaderReady={isHeaderReady} + renderHeader={renderHeader} + currentPage={currentPage} + onCurrentPageSelected={onCurrentPageSelected} + onTabBarLayout={onTabBarLayout} + onHeaderOnlyLayout={onHeaderOnlyLayout} + onSelect={props.onSelect} + scrollY={scrollY} + testID={testID} + allowHeaderOverScroll={allowHeaderOverScroll} + dragProgress={props.dragProgress} + dragState={props.dragState} + /> + </PagerHeaderProvider> + ) + }, + [ + headerOnlyHeight, items, isHeaderReady, renderHeader, - initialPage, - onPageSelected, + currentPage, onCurrentPageSelected, + onTabBarLayout, + onHeaderOnlyLayout, + scrollY, + testID, allowHeaderOverScroll, - }: PagerWithHeaderProps, - ref, - ) { - const [currentPage, setCurrentPage] = React.useState(0) - const [tabBarHeight, setTabBarHeight] = React.useState(0) - const [headerOnlyHeight, setHeaderOnlyHeight] = React.useState(0) - const scrollY = useSharedValue(0) - const headerHeight = headerOnlyHeight + tabBarHeight - - // capture the header bar sizing - const onTabBarLayout = useNonReactiveCallback((evt: LayoutChangeEvent) => { - const height = evt.nativeEvent.layout.height - if (height > 0) { - // The rounding is necessary to prevent jumps on iOS - setTabBarHeight(Math.round(height * 2) / 2) - } - }) - const onHeaderOnlyLayout = useNonReactiveCallback((height: number) => { - if (height > 0) { - // The rounding is necessary to prevent jumps on iOS - setHeaderOnlyHeight(Math.round(height * 2) / 2) - } - }) - - const renderTabBar = React.useCallback( - (props: RenderTabBarFnProps) => { - return ( - <PagerHeaderProvider - scrollY={scrollY} - headerHeight={headerOnlyHeight}> - <PagerTabBar - headerOnlyHeight={headerOnlyHeight} - items={items} - isHeaderReady={isHeaderReady} - renderHeader={renderHeader} - currentPage={currentPage} - onCurrentPageSelected={onCurrentPageSelected} - onTabBarLayout={onTabBarLayout} - onHeaderOnlyLayout={onHeaderOnlyLayout} - onSelect={props.onSelect} - scrollY={scrollY} - testID={testID} - allowHeaderOverScroll={allowHeaderOverScroll} - dragProgress={props.dragProgress} - dragState={props.dragState} - /> - </PagerHeaderProvider> - ) - }, - [ - headerOnlyHeight, - items, - isHeaderReady, - renderHeader, - currentPage, - onCurrentPageSelected, - onTabBarLayout, - onHeaderOnlyLayout, - scrollY, - testID, - allowHeaderOverScroll, - ], - ) + ], + ) - const scrollRefs = useSharedValue<Array<AnimatedRef<any> | null>>([]) - const registerRef = React.useCallback( - (scrollRef: AnimatedRef<any> | null, atIndex: number) => { - scrollRefs.modify(refs => { - 'worklet' - refs[atIndex] = scrollRef - return refs - }) - }, - [scrollRefs], - ) + const scrollRefs = useSharedValue<Array<AnimatedRef<any> | null>>([]) + const registerRef = useCallback( + (scrollRef: AnimatedRef<any> | null, atIndex: number) => { + scrollRefs.modify(refs => { + 'worklet' + refs[atIndex] = scrollRef + return refs + }) + }, + [scrollRefs], + ) - const lastForcedScrollY = useSharedValue(0) - const adjustScrollForOtherPages = () => { + const lastForcedScrollY = useSharedValue(0) + const adjustScrollForOtherPages = useCallback( + (scrollState: 'idle' | 'dragging' | 'settling') => { 'worklet' + if (scrollState !== 'dragging') return const currentScrollY = scrollY.get() const forcedScrollY = Math.min(currentScrollY, headerOnlyHeight) if (lastForcedScrollY.get() !== forcedScrollY) { @@ -152,75 +153,69 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>( } } } - } + }, + [currentPage, headerOnlyHeight, lastForcedScrollY, scrollRefs, scrollY], + ) - const throttleTimeout = React.useRef<ReturnType<typeof setTimeout> | null>( - null, - ) - const queueThrottledOnScroll = useNonReactiveCallback(() => { - if (!throttleTimeout.current) { - throttleTimeout.current = setTimeout(() => { - throttleTimeout.current = null - runOnUI(adjustScrollForOtherPages)() - }, 80 /* Sync often enough you're unlikely to catch it unsynced */) + const onScrollWorklet = useCallback( + (e: NativeScrollEvent) => { + 'worklet' + const nextScrollY = e.contentOffset.y + // HACK: onScroll is reporting some strange values on load (negative header height). + // Highly improbable that you'd be overscrolled by over 400px - + // in fact, I actually can't do it, so let's just ignore those. -sfn + const isPossiblyInvalid = + headerHeight > 0 && Math.round(nextScrollY * 2) / 2 === -headerHeight + if (!isPossiblyInvalid) { + scrollY.set(nextScrollY) } - }) + }, + [scrollY, headerHeight], + ) - const onScrollWorklet = React.useCallback( - (e: NativeScrollEvent) => { - 'worklet' - const nextScrollY = e.contentOffset.y - // HACK: onScroll is reporting some strange values on load (negative header height). - // Highly improbable that you'd be overscrolled by over 400px - - // in fact, I actually can't do it, so let's just ignore those. -sfn - const isPossiblyInvalid = - headerHeight > 0 && Math.round(nextScrollY * 2) / 2 === -headerHeight - if (!isPossiblyInvalid) { - scrollY.set(nextScrollY) - runOnJS(queueThrottledOnScroll)() - } - }, - [scrollY, queueThrottledOnScroll, headerHeight], - ) + const onPageSelectedInner = useCallback( + (index: number) => { + setCurrentPage(index) + onPageSelected?.(index) + }, + [onPageSelected, setCurrentPage], + ) - const onPageSelectedInner = React.useCallback( - (index: number) => { - setCurrentPage(index) - onPageSelected?.(index) - }, - [onPageSelected, setCurrentPage], - ) + const onTabPressed = useCallback(() => { + runOnUI(adjustScrollForOtherPages)('dragging') + }, [adjustScrollForOtherPages]) - return ( - <Pager - ref={ref} - testID={testID} - initialPage={initialPage} - onPageSelected={onPageSelectedInner} - renderTabBar={renderTabBar}> - {toArray(children) - .filter(Boolean) - .map((child, i) => { - const isReady = - isHeaderReady && headerOnlyHeight > 0 && tabBarHeight > 0 - return ( - <View key={i} collapsable={false}> - <PagerItem - headerHeight={headerHeight} - index={i} - isReady={isReady} - isFocused={i === currentPage} - onScrollWorklet={i === currentPage ? onScrollWorklet : noop} - registerRef={registerRef} - renderTab={child} - /> - </View> - ) - })} - </Pager> - ) - }, -) + return ( + <Pager + ref={ref} + testID={testID} + initialPage={initialPage} + onTabPressed={onTabPressed} + onPageSelected={onPageSelectedInner} + renderTabBar={renderTabBar} + onPageScrollStateChanged={adjustScrollForOtherPages}> + {toArray(children) + .filter(Boolean) + .map((child, i) => { + const isReady = + isHeaderReady && headerOnlyHeight > 0 && tabBarHeight > 0 + return ( + <View key={i} collapsable={false}> + <PagerItem + headerHeight={headerHeight} + index={i} + isReady={isReady} + isFocused={i === currentPage} + onScrollWorklet={i === currentPage ? onScrollWorklet : noop} + registerRef={registerRef} + renderTab={child} + /> + </View> + ) + })} + </Pager> + ) +} let PagerTabBar = ({ currentPage, @@ -258,7 +253,7 @@ let PagerTabBar = ({ dragState: SharedValue<'idle' | 'dragging' | 'settling'> }): React.ReactNode => { const t = useTheme() - const [minimumHeaderHeight, setMinimumHeaderHeight] = React.useState(0) + const [minimumHeaderHeight, setMinimumHeaderHeight] = useState(0) const headerTransform = useAnimatedStyle(() => { const translateY = Math.min( @@ -275,7 +270,7 @@ let PagerTabBar = ({ ], } }) - const headerRef = React.useRef(null) + const headerRef = useRef(null) return ( <Animated.View pointerEvents={isIOS ? 'auto' : 'box-none'} @@ -327,7 +322,7 @@ let PagerTabBar = ({ </Animated.View> ) } -PagerTabBar = React.memo(PagerTabBar) +PagerTabBar = memo(PagerTabBar) function PagerItem({ headerHeight, @@ -348,7 +343,7 @@ function PagerItem({ }) { const scrollElRef = useAnimatedRef() - React.useEffect(() => { + useEffect(() => { registerRef(scrollElRef, index) return () => { registerRef(null, index) |