[new arch] Fix profile perf (#8320)
* change approach for keeping scrollviews synced * rm forwardRef from web too --------- Co-authored-by: Hailey <me@haileyok.com>
This commit is contained in:
+115
-107
@@ -1,16 +1,22 @@
|
|||||||
import React, {forwardRef, useCallback, useContext} from 'react'
|
import {
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useImperativeHandle,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {DrawerGestureContext} from 'react-native-drawer-layout'
|
import {DrawerGestureContext} from 'react-native-drawer-layout'
|
||||||
import {Gesture, GestureDetector} from 'react-native-gesture-handler'
|
import {Gesture, GestureDetector} from 'react-native-gesture-handler'
|
||||||
import PagerView, {
|
import PagerView, {
|
||||||
PagerViewOnPageScrollEventData,
|
type PagerViewOnPageScrollEventData,
|
||||||
PagerViewOnPageSelectedEvent,
|
type PagerViewOnPageSelectedEvent,
|
||||||
PagerViewOnPageSelectedEventData,
|
type PagerViewOnPageSelectedEventData,
|
||||||
PageScrollStateChangedNativeEventData,
|
type PageScrollStateChangedNativeEventData,
|
||||||
} from 'react-native-pager-view'
|
} from 'react-native-pager-view'
|
||||||
import Animated, {
|
import Animated, {
|
||||||
runOnJS,
|
runOnJS,
|
||||||
SharedValue,
|
type SharedValue,
|
||||||
useEvent,
|
useEvent,
|
||||||
useHandler,
|
useHandler,
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
@@ -36,8 +42,12 @@ export interface RenderTabBarFnProps {
|
|||||||
export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element
|
export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
ref?: React.Ref<PagerRef>
|
||||||
initialPage?: number
|
initialPage?: number
|
||||||
renderTabBar: RenderTabBarFn
|
renderTabBar: RenderTabBarFn
|
||||||
|
// tab pressed, yet to scroll to page
|
||||||
|
onTabPressed?: (index: number) => void
|
||||||
|
// scroll settled
|
||||||
onPageSelected?: (index: number) => void
|
onPageSelected?: (index: number) => void
|
||||||
onPageScrollStateChanged?: (
|
onPageScrollStateChanged?: (
|
||||||
scrollState: 'idle' | 'dragging' | 'settling',
|
scrollState: 'idle' | 'dragging' | 'settling',
|
||||||
@@ -47,114 +57,112 @@ interface Props {
|
|||||||
|
|
||||||
const AnimatedPagerView = Animated.createAnimatedComponent(PagerView)
|
const AnimatedPagerView = Animated.createAnimatedComponent(PagerView)
|
||||||
|
|
||||||
export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
|
export function Pager({
|
||||||
function PagerImpl(
|
ref,
|
||||||
|
children,
|
||||||
|
initialPage = 0,
|
||||||
|
renderTabBar,
|
||||||
|
onPageSelected: parentOnPageSelected,
|
||||||
|
onTabPressed: parentOnTabPressed,
|
||||||
|
onPageScrollStateChanged: parentOnPageScrollStateChanged,
|
||||||
|
testID,
|
||||||
|
}: React.PropsWithChildren<Props>) {
|
||||||
|
const [selectedPage, setSelectedPage] = useState(initialPage)
|
||||||
|
const pagerView = useRef<PagerView>(null)
|
||||||
|
|
||||||
|
const [isIdle, setIsIdle] = useState(true)
|
||||||
|
const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
const canSwipeDrawer = selectedPage === 0 && isIdle
|
||||||
|
setDrawerSwipeDisabled(!canSwipeDrawer)
|
||||||
|
return () => {
|
||||||
|
setDrawerSwipeDisabled(false)
|
||||||
|
}
|
||||||
|
}, [setDrawerSwipeDisabled, selectedPage, isIdle]),
|
||||||
|
)
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
setPage: (index: number) => {
|
||||||
|
pagerView.current?.setPage(index)
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
const onPageSelectedJSThread = useCallback(
|
||||||
|
(nextPosition: number) => {
|
||||||
|
setSelectedPage(nextPosition)
|
||||||
|
parentOnPageSelected?.(nextPosition)
|
||||||
|
},
|
||||||
|
[setSelectedPage, parentOnPageSelected],
|
||||||
|
)
|
||||||
|
|
||||||
|
const onTabBarSelect = useCallback(
|
||||||
|
(index: number) => {
|
||||||
|
parentOnTabPressed?.(index)
|
||||||
|
pagerView.current?.setPage(index)
|
||||||
|
},
|
||||||
|
[pagerView, parentOnTabPressed],
|
||||||
|
)
|
||||||
|
|
||||||
|
const dragState = useSharedValue<'idle' | 'settling' | 'dragging'>('idle')
|
||||||
|
const dragProgress = useSharedValue(selectedPage)
|
||||||
|
const didInit = useSharedValue(false)
|
||||||
|
const handlePageScroll = usePagerHandlers(
|
||||||
{
|
{
|
||||||
children,
|
onPageScroll(e: PagerViewOnPageScrollEventData) {
|
||||||
initialPage = 0,
|
'worklet'
|
||||||
renderTabBar,
|
if (didInit.get() === false) {
|
||||||
onPageScrollStateChanged: parentOnPageScrollStateChanged,
|
// On iOS, there's a spurious scroll event with 0 position
|
||||||
onPageSelected: parentOnPageSelected,
|
// even if a different page was supplied as the initial page.
|
||||||
testID,
|
// Ignore it and wait for the first confirmed selection instead.
|
||||||
}: React.PropsWithChildren<Props>,
|
return
|
||||||
ref,
|
|
||||||
) {
|
|
||||||
const [selectedPage, setSelectedPage] = React.useState(initialPage)
|
|
||||||
const pagerView = React.useRef<PagerView>(null)
|
|
||||||
|
|
||||||
const [isIdle, setIsIdle] = React.useState(true)
|
|
||||||
const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
const canSwipeDrawer = selectedPage === 0 && isIdle
|
|
||||||
setDrawerSwipeDisabled(!canSwipeDrawer)
|
|
||||||
return () => {
|
|
||||||
setDrawerSwipeDisabled(false)
|
|
||||||
}
|
}
|
||||||
}, [setDrawerSwipeDisabled, selectedPage, isIdle]),
|
dragProgress.set(e.offset + e.position)
|
||||||
)
|
|
||||||
|
|
||||||
React.useImperativeHandle(ref, () => ({
|
|
||||||
setPage: (index: number) => {
|
|
||||||
pagerView.current?.setPage(index)
|
|
||||||
},
|
},
|
||||||
}))
|
onPageScrollStateChanged(e: PageScrollStateChangedNativeEventData) {
|
||||||
|
'worklet'
|
||||||
const onPageSelectedJSThread = React.useCallback(
|
runOnJS(setIsIdle)(e.pageScrollState === 'idle')
|
||||||
(nextPosition: number) => {
|
if (dragState.get() === 'idle' && e.pageScrollState === 'settling') {
|
||||||
setSelectedPage(nextPosition)
|
// This is a programmatic scroll on Android.
|
||||||
parentOnPageSelected?.(nextPosition)
|
// Stay "idle" to match iOS and avoid confusing downstream code.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dragState.set(e.pageScrollState)
|
||||||
|
parentOnPageScrollStateChanged?.(e.pageScrollState)
|
||||||
},
|
},
|
||||||
[setSelectedPage, parentOnPageSelected],
|
onPageSelected(e: PagerViewOnPageSelectedEventData) {
|
||||||
)
|
'worklet'
|
||||||
|
didInit.set(true)
|
||||||
const onTabBarSelect = React.useCallback(
|
runOnJS(onPageSelectedJSThread)(e.position)
|
||||||
(index: number) => {
|
|
||||||
pagerView.current?.setPage(index)
|
|
||||||
},
|
},
|
||||||
[pagerView],
|
},
|
||||||
)
|
[parentOnPageScrollStateChanged],
|
||||||
|
)
|
||||||
|
|
||||||
const dragState = useSharedValue<'idle' | 'settling' | 'dragging'>('idle')
|
const drawerGesture = useContext(DrawerGestureContext) ?? Gesture.Native() // noop for web
|
||||||
const dragProgress = useSharedValue(selectedPage)
|
const nativeGesture =
|
||||||
const didInit = useSharedValue(false)
|
Gesture.Native().requireExternalGestureToFail(drawerGesture)
|
||||||
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'
|
|
||||||
runOnJS(setIsIdle)(e.pageScrollState === 'idle')
|
|
||||||
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],
|
|
||||||
)
|
|
||||||
|
|
||||||
const drawerGesture = useContext(DrawerGestureContext) ?? Gesture.Native() // noop for web
|
return (
|
||||||
const nativeGesture =
|
<View testID={testID} style={[a.flex_1, native(a.overflow_hidden)]}>
|
||||||
Gesture.Native().requireExternalGestureToFail(drawerGesture)
|
{renderTabBar({
|
||||||
|
selectedPage,
|
||||||
return (
|
onSelect: onTabBarSelect,
|
||||||
<View testID={testID} style={[a.flex_1, native(a.overflow_hidden)]}>
|
dragProgress,
|
||||||
{renderTabBar({
|
dragState,
|
||||||
selectedPage,
|
})}
|
||||||
onSelect: onTabBarSelect,
|
<GestureDetector gesture={nativeGesture}>
|
||||||
dragProgress,
|
<AnimatedPagerView
|
||||||
dragState,
|
ref={pagerView}
|
||||||
})}
|
style={[a.flex_1]}
|
||||||
<GestureDetector gesture={nativeGesture}>
|
initialPage={initialPage}
|
||||||
<AnimatedPagerView
|
onPageScroll={handlePageScroll}>
|
||||||
ref={pagerView}
|
{children}
|
||||||
style={[a.flex_1]}
|
</AnimatedPagerView>
|
||||||
initialPage={initialPage}
|
</GestureDetector>
|
||||||
onPageScroll={handlePageScroll}>
|
</View>
|
||||||
{children}
|
)
|
||||||
</AnimatedPagerView>
|
}
|
||||||
</GestureDetector>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
function usePagerHandlers(
|
function usePagerHandlers(
|
||||||
handlers: {
|
handlers: {
|
||||||
|
|||||||
@@ -1,8 +1,19 @@
|
|||||||
import React from 'react'
|
import {
|
||||||
|
Children,
|
||||||
|
useCallback,
|
||||||
|
useImperativeHandle,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {flushSync} from 'react-dom'
|
import {flushSync} from 'react-dom'
|
||||||
|
|
||||||
import {s} from '#/lib/styles'
|
import {s} from '#/lib/styles'
|
||||||
|
import {atoms as a} from '#/alf'
|
||||||
|
|
||||||
|
export interface PagerRef {
|
||||||
|
setPage: (index: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
export interface RenderTabBarFnProps {
|
export interface RenderTabBarFnProps {
|
||||||
selectedPage: number
|
selectedPage: number
|
||||||
@@ -12,30 +23,30 @@ export interface RenderTabBarFnProps {
|
|||||||
export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element
|
export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
ref?: React.Ref<PagerRef>
|
||||||
initialPage?: number
|
initialPage?: number
|
||||||
renderTabBar: RenderTabBarFn
|
renderTabBar: RenderTabBarFn
|
||||||
onPageSelected?: (index: number) => void
|
onPageSelected?: (index: number) => void
|
||||||
}
|
}
|
||||||
export const Pager = React.forwardRef(function PagerImpl(
|
|
||||||
{
|
|
||||||
children,
|
|
||||||
initialPage = 0,
|
|
||||||
renderTabBar,
|
|
||||||
onPageSelected,
|
|
||||||
}: React.PropsWithChildren<Props>,
|
|
||||||
ref,
|
|
||||||
) {
|
|
||||||
const [selectedPage, setSelectedPage] = React.useState(initialPage)
|
|
||||||
const scrollYs = React.useRef<Array<number | null>>([])
|
|
||||||
const anchorRef = React.useRef(null)
|
|
||||||
|
|
||||||
React.useImperativeHandle(ref, () => ({
|
export function Pager({
|
||||||
|
ref,
|
||||||
|
children,
|
||||||
|
initialPage = 0,
|
||||||
|
renderTabBar,
|
||||||
|
onPageSelected,
|
||||||
|
}: React.PropsWithChildren<Props>) {
|
||||||
|
const [selectedPage, setSelectedPage] = useState(initialPage)
|
||||||
|
const scrollYs = useRef<Array<number | null>>([])
|
||||||
|
const anchorRef = useRef(null)
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
setPage: (index: number) => {
|
setPage: (index: number) => {
|
||||||
onTabBarSelect(index)
|
onTabBarSelect(index)
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const onTabBarSelect = React.useCallback(
|
const onTabBarSelect = useCallback(
|
||||||
(index: number) => {
|
(index: number) => {
|
||||||
const scrollY = window.scrollY
|
const scrollY = window.scrollY
|
||||||
// We want to determine if the tabbar is already "sticking" at the top (in which
|
// We want to determine if the tabbar is already "sticking" at the top (in which
|
||||||
@@ -75,11 +86,13 @@ export const Pager = React.forwardRef(function PagerImpl(
|
|||||||
tabBarAnchor: <View ref={anchorRef} />,
|
tabBarAnchor: <View ref={anchorRef} />,
|
||||||
onSelect: e => onTabBarSelect(e),
|
onSelect: e => onTabBarSelect(e),
|
||||||
})}
|
})}
|
||||||
{React.Children.map(children, (child, i) => (
|
{Children.map(children, (child, i) => (
|
||||||
<View style={selectedPage === i ? s.flex1 : s.hidden} key={`page-${i}`}>
|
<View
|
||||||
|
style={selectedPage === i ? a.flex_1 : a.hidden}
|
||||||
|
key={`page-${i}`}>
|
||||||
{child}
|
{child}
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
import * as React from 'react'
|
import {memo, useCallback, useEffect, useRef, useState} from 'react'
|
||||||
import {
|
import {
|
||||||
LayoutChangeEvent,
|
type LayoutChangeEvent,
|
||||||
NativeScrollEvent,
|
type NativeScrollEvent,
|
||||||
ScrollView,
|
type ScrollView,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
View,
|
View,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import Animated, {
|
import Animated, {
|
||||||
AnimatedRef,
|
type AnimatedRef,
|
||||||
runOnJS,
|
|
||||||
runOnUI,
|
runOnUI,
|
||||||
scrollTo,
|
scrollTo,
|
||||||
SharedValue,
|
type SharedValue,
|
||||||
useAnimatedRef,
|
useAnimatedRef,
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
@@ -20,9 +19,13 @@ import Animated, {
|
|||||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||||
import {ScrollProvider} from '#/lib/ScrollContext'
|
import {ScrollProvider} from '#/lib/ScrollContext'
|
||||||
import {isIOS} from '#/platform/detection'
|
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 {useTheme} from '#/alf'
|
||||||
import {ListMethods} from '../util/List'
|
import {type ListMethods} from '../util/List'
|
||||||
import {PagerHeaderProvider} from './PagerHeaderContext'
|
import {PagerHeaderProvider} from './PagerHeaderContext'
|
||||||
import {TabBar} from './TabBar'
|
import {TabBar} from './TabBar'
|
||||||
|
|
||||||
@@ -33,6 +36,7 @@ export interface PagerWithHeaderChildParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PagerWithHeaderProps {
|
export interface PagerWithHeaderProps {
|
||||||
|
ref?: React.Ref<PagerRef>
|
||||||
testID?: string
|
testID?: string
|
||||||
children:
|
children:
|
||||||
| (((props: PagerWithHeaderChildParams) => JSX.Element) | null)[]
|
| (((props: PagerWithHeaderChildParams) => JSX.Element) | null)[]
|
||||||
@@ -49,97 +53,94 @@ export interface PagerWithHeaderProps {
|
|||||||
onCurrentPageSelected?: (index: number) => void
|
onCurrentPageSelected?: (index: number) => void
|
||||||
allowHeaderOverScroll?: boolean
|
allowHeaderOverScroll?: boolean
|
||||||
}
|
}
|
||||||
export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
|
export function PagerWithHeader({
|
||||||
function PageWithHeaderImpl(
|
ref,
|
||||||
{
|
children,
|
||||||
children,
|
testID,
|
||||||
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,
|
items,
|
||||||
isHeaderReady,
|
isHeaderReady,
|
||||||
renderHeader,
|
renderHeader,
|
||||||
initialPage,
|
currentPage,
|
||||||
onPageSelected,
|
|
||||||
onCurrentPageSelected,
|
onCurrentPageSelected,
|
||||||
|
onTabBarLayout,
|
||||||
|
onHeaderOnlyLayout,
|
||||||
|
scrollY,
|
||||||
|
testID,
|
||||||
allowHeaderOverScroll,
|
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 scrollRefs = useSharedValue<Array<AnimatedRef<any> | null>>([])
|
||||||
const onTabBarLayout = useNonReactiveCallback((evt: LayoutChangeEvent) => {
|
const registerRef = useCallback(
|
||||||
const height = evt.nativeEvent.layout.height
|
(scrollRef: AnimatedRef<any> | null, atIndex: number) => {
|
||||||
if (height > 0) {
|
scrollRefs.modify(refs => {
|
||||||
// The rounding is necessary to prevent jumps on iOS
|
'worklet'
|
||||||
setTabBarHeight(Math.round(height * 2) / 2)
|
refs[atIndex] = scrollRef
|
||||||
}
|
return refs
|
||||||
})
|
})
|
||||||
const onHeaderOnlyLayout = useNonReactiveCallback((height: number) => {
|
},
|
||||||
if (height > 0) {
|
[scrollRefs],
|
||||||
// The rounding is necessary to prevent jumps on iOS
|
)
|
||||||
setHeaderOnlyHeight(Math.round(height * 2) / 2)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const renderTabBar = React.useCallback(
|
const lastForcedScrollY = useSharedValue(0)
|
||||||
(props: RenderTabBarFnProps) => {
|
const adjustScrollForOtherPages = useCallback(
|
||||||
return (
|
(scrollState: 'idle' | 'dragging' | 'settling') => {
|
||||||
<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 lastForcedScrollY = useSharedValue(0)
|
|
||||||
const adjustScrollForOtherPages = () => {
|
|
||||||
'worklet'
|
'worklet'
|
||||||
|
if (scrollState !== 'dragging') return
|
||||||
const currentScrollY = scrollY.get()
|
const currentScrollY = scrollY.get()
|
||||||
const forcedScrollY = Math.min(currentScrollY, headerOnlyHeight)
|
const forcedScrollY = Math.min(currentScrollY, headerOnlyHeight)
|
||||||
if (lastForcedScrollY.get() !== forcedScrollY) {
|
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>(
|
const onScrollWorklet = useCallback(
|
||||||
null,
|
(e: NativeScrollEvent) => {
|
||||||
)
|
'worklet'
|
||||||
const queueThrottledOnScroll = useNonReactiveCallback(() => {
|
const nextScrollY = e.contentOffset.y
|
||||||
if (!throttleTimeout.current) {
|
// HACK: onScroll is reporting some strange values on load (negative header height).
|
||||||
throttleTimeout.current = setTimeout(() => {
|
// Highly improbable that you'd be overscrolled by over 400px -
|
||||||
throttleTimeout.current = null
|
// in fact, I actually can't do it, so let's just ignore those. -sfn
|
||||||
runOnUI(adjustScrollForOtherPages)()
|
const isPossiblyInvalid =
|
||||||
}, 80 /* Sync often enough you're unlikely to catch it unsynced */)
|
headerHeight > 0 && Math.round(nextScrollY * 2) / 2 === -headerHeight
|
||||||
|
if (!isPossiblyInvalid) {
|
||||||
|
scrollY.set(nextScrollY)
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
|
[scrollY, headerHeight],
|
||||||
|
)
|
||||||
|
|
||||||
const onScrollWorklet = React.useCallback(
|
const onPageSelectedInner = useCallback(
|
||||||
(e: NativeScrollEvent) => {
|
(index: number) => {
|
||||||
'worklet'
|
setCurrentPage(index)
|
||||||
const nextScrollY = e.contentOffset.y
|
onPageSelected?.(index)
|
||||||
// HACK: onScroll is reporting some strange values on load (negative header height).
|
},
|
||||||
// Highly improbable that you'd be overscrolled by over 400px -
|
[onPageSelected, setCurrentPage],
|
||||||
// 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 = React.useCallback(
|
const onTabPressed = useCallback(() => {
|
||||||
(index: number) => {
|
runOnUI(adjustScrollForOtherPages)('dragging')
|
||||||
setCurrentPage(index)
|
}, [adjustScrollForOtherPages])
|
||||||
onPageSelected?.(index)
|
|
||||||
},
|
|
||||||
[onPageSelected, setCurrentPage],
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pager
|
<Pager
|
||||||
ref={ref}
|
ref={ref}
|
||||||
testID={testID}
|
testID={testID}
|
||||||
initialPage={initialPage}
|
initialPage={initialPage}
|
||||||
onPageSelected={onPageSelectedInner}
|
onTabPressed={onTabPressed}
|
||||||
renderTabBar={renderTabBar}>
|
onPageSelected={onPageSelectedInner}
|
||||||
{toArray(children)
|
renderTabBar={renderTabBar}
|
||||||
.filter(Boolean)
|
onPageScrollStateChanged={adjustScrollForOtherPages}>
|
||||||
.map((child, i) => {
|
{toArray(children)
|
||||||
const isReady =
|
.filter(Boolean)
|
||||||
isHeaderReady && headerOnlyHeight > 0 && tabBarHeight > 0
|
.map((child, i) => {
|
||||||
return (
|
const isReady =
|
||||||
<View key={i} collapsable={false}>
|
isHeaderReady && headerOnlyHeight > 0 && tabBarHeight > 0
|
||||||
<PagerItem
|
return (
|
||||||
headerHeight={headerHeight}
|
<View key={i} collapsable={false}>
|
||||||
index={i}
|
<PagerItem
|
||||||
isReady={isReady}
|
headerHeight={headerHeight}
|
||||||
isFocused={i === currentPage}
|
index={i}
|
||||||
onScrollWorklet={i === currentPage ? onScrollWorklet : noop}
|
isReady={isReady}
|
||||||
registerRef={registerRef}
|
isFocused={i === currentPage}
|
||||||
renderTab={child}
|
onScrollWorklet={i === currentPage ? onScrollWorklet : noop}
|
||||||
/>
|
registerRef={registerRef}
|
||||||
</View>
|
renderTab={child}
|
||||||
)
|
/>
|
||||||
})}
|
</View>
|
||||||
</Pager>
|
)
|
||||||
)
|
})}
|
||||||
},
|
</Pager>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
let PagerTabBar = ({
|
let PagerTabBar = ({
|
||||||
currentPage,
|
currentPage,
|
||||||
@@ -258,7 +253,7 @@ let PagerTabBar = ({
|
|||||||
dragState: SharedValue<'idle' | 'dragging' | 'settling'>
|
dragState: SharedValue<'idle' | 'dragging' | 'settling'>
|
||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const [minimumHeaderHeight, setMinimumHeaderHeight] = React.useState(0)
|
const [minimumHeaderHeight, setMinimumHeaderHeight] = useState(0)
|
||||||
const headerTransform = useAnimatedStyle(() => {
|
const headerTransform = useAnimatedStyle(() => {
|
||||||
const translateY =
|
const translateY =
|
||||||
Math.min(
|
Math.min(
|
||||||
@@ -275,7 +270,7 @@ let PagerTabBar = ({
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const headerRef = React.useRef(null)
|
const headerRef = useRef(null)
|
||||||
return (
|
return (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
pointerEvents={isIOS ? 'auto' : 'box-none'}
|
pointerEvents={isIOS ? 'auto' : 'box-none'}
|
||||||
@@ -327,7 +322,7 @@ let PagerTabBar = ({
|
|||||||
</Animated.View>
|
</Animated.View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
PagerTabBar = React.memo(PagerTabBar)
|
PagerTabBar = memo(PagerTabBar)
|
||||||
|
|
||||||
function PagerItem({
|
function PagerItem({
|
||||||
headerHeight,
|
headerHeight,
|
||||||
@@ -348,7 +343,7 @@ function PagerItem({
|
|||||||
}) {
|
}) {
|
||||||
const scrollElRef = useAnimatedRef()
|
const scrollElRef = useAnimatedRef()
|
||||||
|
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
registerRef(scrollElRef, index)
|
registerRef(scrollElRef, index)
|
||||||
return () => {
|
return () => {
|
||||||
registerRef(null, index)
|
registerRef(null, index)
|
||||||
|
|||||||
Reference in New Issue
Block a user