about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/view/com/pager/PagerWithHeader.tsx179
1 files changed, 117 insertions, 62 deletions
diff --git a/src/view/com/pager/PagerWithHeader.tsx b/src/view/com/pager/PagerWithHeader.tsx
index 2c7640c43..487c589e3 100644
--- a/src/view/com/pager/PagerWithHeader.tsx
+++ b/src/view/com/pager/PagerWithHeader.tsx
@@ -11,14 +11,17 @@ import Animated, {
   useAnimatedStyle,
   useSharedValue,
   runOnJS,
+  runOnUI,
   scrollTo,
   useAnimatedRef,
   AnimatedRef,
+  SharedValue,
 } from 'react-native-reanimated'
 import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager'
 import {TabBar} from './TabBar'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {OnScrollHandler} from 'lib/hooks/useOnMainScroll'
+import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
 
 const SCROLLED_DOWN_LIMIT = 200
 
@@ -56,7 +59,6 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
     }: PagerWithHeaderProps,
     ref,
   ) {
-    const {isMobile} = useWebMediaQueries()
     const [currentPage, setCurrentPage] = React.useState(0)
     const [tabBarHeight, setTabBarHeight] = React.useState(0)
     const [headerOnlyHeight, setHeaderOnlyHeight] = React.useState(0)
@@ -78,56 +80,34 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
       [setHeaderOnlyHeight],
     )
 
-    // render the the header and tab bar
-    const headerTransform = useAnimatedStyle(() => ({
-      transform: [
-        {
-          translateY: Math.min(
-            Math.min(scrollY.value, headerOnlyHeight) * -1,
-            0,
-          ),
-        },
-      ],
-    }))
-
     const renderTabBar = React.useCallback(
       (props: RenderTabBarFnProps) => {
         return (
-          <Animated.View
-            style={[
-              isMobile ? styles.tabBarMobile : styles.tabBarDesktop,
-              headerTransform,
-            ]}>
-            <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
-                testID={testID}
-                items={items}
-                selectedPage={currentPage}
-                onSelect={props.onSelect}
-                onPressSelected={onCurrentPageSelected}
-              />
-            </View>
-          </Animated.View>
+          <PagerTabBar
+            headerOnlyHeight={headerOnlyHeight}
+            items={items}
+            isHeaderReady={isHeaderReady}
+            renderHeader={renderHeader}
+            currentPage={currentPage}
+            onCurrentPageSelected={onCurrentPageSelected}
+            onTabBarLayout={onTabBarLayout}
+            onHeaderOnlyLayout={onHeaderOnlyLayout}
+            onSelect={props.onSelect}
+            scrollY={scrollY}
+            testID={testID}
+          />
         )
       },
       [
+        headerOnlyHeight,
         items,
         isHeaderReady,
         renderHeader,
-        headerTransform,
         currentPage,
         onCurrentPageSelected,
-        isMobile,
         onTabBarLayout,
         onHeaderOnlyLayout,
+        scrollY,
         testID,
       ],
     )
@@ -142,36 +122,50 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
     }
 
     const lastForcedScrollY = useSharedValue(0)
+    const adjustScrollForOtherPages = () => {
+      'worklet'
+      const currentScrollY = scrollY.value
+      const forcedScrollY = Math.min(currentScrollY, headerOnlyHeight)
+      if (lastForcedScrollY.value !== forcedScrollY) {
+        lastForcedScrollY.value = forcedScrollY
+        const refs = scrollRefs.value
+        for (let i = 0; i < refs.length; i++) {
+          if (i !== currentPage) {
+            // This needs to run on the UI thread.
+            scrollTo(refs[i], 0, forcedScrollY, false)
+          }
+        }
+      }
+    }
+
+    const throttleTimeout = React.useRef<ReturnType<typeof setTimeout> | null>(
+      null,
+    )
+    const queueThrottledOnScroll = useNonReactiveCallback(() => {
+      if (!throttleTimeout.current) {
+        throttleTimeout.current = setTimeout(() => {
+          throttleTimeout.current = null
+
+          runOnUI(adjustScrollForOtherPages)()
+
+          const nextIsScrolledDown = scrollY.value > SCROLLED_DOWN_LIMIT
+          if (isScrolledDown !== nextIsScrolledDown) {
+            React.startTransition(() => {
+              setIsScrolledDown(nextIsScrolledDown)
+            })
+          }
+        }, 80 /* Sync often enough you're unlikely to catch it unsynced */)
+      }
+    })
+
     const onScrollWorklet = React.useCallback(
       (e: NativeScrollEvent) => {
         'worklet'
         const nextScrollY = e.contentOffset.y
         scrollY.value = nextScrollY
-
-        const forcedScrollY = Math.min(nextScrollY, headerOnlyHeight)
-        if (lastForcedScrollY.value !== forcedScrollY) {
-          lastForcedScrollY.value = forcedScrollY
-          const refs = scrollRefs.value
-          for (let i = 0; i < refs.length; i++) {
-            if (i !== currentPage) {
-              scrollTo(refs[i], 0, forcedScrollY, false)
-            }
-          }
-        }
-
-        const nextIsScrolledDown = nextScrollY > SCROLLED_DOWN_LIMIT
-        if (isScrolledDown !== nextIsScrolledDown) {
-          runOnJS(setIsScrolledDown)(nextIsScrolledDown)
-        }
+        runOnJS(queueThrottledOnScroll)()
       },
-      [
-        currentPage,
-        headerOnlyHeight,
-        isScrolledDown,
-        scrollRefs,
-        scrollY,
-        lastForcedScrollY,
-      ],
+      [scrollY, queueThrottledOnScroll],
     )
 
     const onPageSelectedInner = React.useCallback(
@@ -219,6 +213,67 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
   },
 )
 
+let PagerTabBar = ({
+  currentPage,
+  headerOnlyHeight,
+  isHeaderReady,
+  items,
+  scrollY,
+  testID,
+  renderHeader,
+  onHeaderOnlyLayout,
+  onTabBarLayout,
+  onCurrentPageSelected,
+  onSelect,
+}: {
+  currentPage: number
+  headerOnlyHeight: number
+  isHeaderReady: boolean
+  items: string[]
+  testID?: string
+  scrollY: SharedValue<number>
+  renderHeader?: () => JSX.Element
+  onHeaderOnlyLayout: (e: LayoutChangeEvent) => void
+  onTabBarLayout: (e: LayoutChangeEvent) => void
+  onCurrentPageSelected?: (index: number) => void
+  onSelect?: (index: number) => void
+}): React.ReactNode => {
+  const {isMobile} = useWebMediaQueries()
+  const headerTransform = useAnimatedStyle(() => ({
+    transform: [
+      {
+        translateY: Math.min(Math.min(scrollY.value, headerOnlyHeight) * -1, 0),
+      },
+    ],
+  }))
+  return (
+    <Animated.View
+      style={[
+        isMobile ? styles.tabBarMobile : styles.tabBarDesktop,
+        headerTransform,
+      ]}>
+      <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
+          testID={testID}
+          items={items}
+          selectedPage={currentPage}
+          onSelect={onSelect}
+          onPressSelected={onCurrentPageSelected}
+        />
+      </View>
+    </Animated.View>
+  )
+}
+PagerTabBar = React.memo(PagerTabBar)
+
 function PagerItem({
   headerHeight,
   isReady,