about summary refs log tree commit diff
path: root/src/view
diff options
context:
space:
mode:
authordan <dan.abramov@gmail.com>2023-11-06 22:30:10 +0000
committerGitHub <noreply@github.com>2023-11-06 22:30:10 +0000
commitd715246e26728ef55408deeb09a3721e39c5031e (patch)
treeafc873d7833c1407c3e154c3132fc849df77dcf1 /src/view
parent4c00fc576d66b0a348b4fb13b901a464da64cf37 (diff)
downloadvoidsky-d715246e26728ef55408deeb09a3721e39c5031e.tar.zst
Fix sticky pager jumps (#1825)
* Defer showing pager content until its header settles

* Introduce the concept of headerOnlyHeight

* Keep headerOnlyHeight in state, make headerHeight derived

* Hide content until *both* header (only) and tabbar are measured

* Hide tabbar to read its layout earlier

* Give consistent keys to pages
Diffstat (limited to 'src/view')
-rw-r--r--src/view/com/pager/PagerWithHeader.tsx64
-rw-r--r--src/view/com/pager/TabBar.tsx4
-rw-r--r--src/view/screens/ProfileFeed.tsx3
-rw-r--r--src/view/screens/ProfileList.tsx5
4 files changed, 48 insertions, 28 deletions
diff --git a/src/view/com/pager/PagerWithHeader.tsx b/src/view/com/pager/PagerWithHeader.tsx
index 3cdd3ab2e..842a4574e 100644
--- a/src/view/com/pager/PagerWithHeader.tsx
+++ b/src/view/com/pager/PagerWithHeader.tsx
@@ -1,5 +1,5 @@
 import * as React from 'react'
-import {LayoutChangeEvent, StyleSheet} from 'react-native'
+import {LayoutChangeEvent, StyleSheet, View} from 'react-native'
 import Animated, {
   Easing,
   useAnimatedReaction,
@@ -28,6 +28,7 @@ export interface PagerWithHeaderProps {
     | (((props: PagerWithHeaderChildParams) => JSX.Element) | null)[]
     | ((props: PagerWithHeaderChildParams) => JSX.Element)
   items: string[]
+  isHeaderReady: boolean
   renderHeader?: () => JSX.Element
   initialPage?: number
   onPageSelected?: (index: number) => void
@@ -39,6 +40,7 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
       children,
       testID,
       items,
+      isHeaderReady,
       renderHeader,
       initialPage,
       onPageSelected,
@@ -51,15 +53,17 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
     const scrollYs = React.useRef<Record<number, number>>({})
     const scrollY = useSharedValue(scrollYs.current[currentPage] || 0)
     const [tabBarHeight, setTabBarHeight] = React.useState(0)
-    const [headerHeight, setHeaderHeight] = React.useState(0)
+    const [headerOnlyHeight, setHeaderOnlyHeight] = React.useState(0)
     const [isScrolledDown, setIsScrolledDown] = React.useState(
       scrollYs.current[currentPage] > SCROLLED_DOWN_LIMIT,
     )
 
+    const headerHeight = headerOnlyHeight + tabBarHeight
+
     // react to scroll updates
     function onScrollUpdate(v: number) {
       // track each page's current scroll position
-      scrollYs.current[currentPage] = Math.min(v, headerHeight - tabBarHeight)
+      scrollYs.current[currentPage] = Math.min(v, headerOnlyHeight)
       // update the 'is scrolled down' value
       setIsScrolledDown(v > SCROLLED_DOWN_LIMIT)
     }
@@ -75,11 +79,11 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
       },
       [setTabBarHeight],
     )
-    const onHeaderLayout = React.useCallback(
+    const onHeaderOnlyLayout = React.useCallback(
       (evt: LayoutChangeEvent) => {
-        setHeaderHeight(evt.nativeEvent.layout.height)
+        setHeaderOnlyHeight(evt.nativeEvent.layout.height)
       },
-      [setHeaderHeight],
+      [setHeaderOnlyHeight],
     )
 
     // render the the header and tab bar
@@ -88,7 +92,7 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
         transform: [
           {
             translateY: Math.min(
-              Math.min(scrollY.value, headerHeight - tabBarHeight) * -1,
+              Math.min(scrollY.value, headerOnlyHeight) * -1,
               0,
             ),
           },
@@ -100,31 +104,39 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
       (props: RenderTabBarFnProps) => {
         return (
           <Animated.View
-            onLayout={onHeaderLayout}
             style={[
               isMobile ? styles.tabBarMobile : styles.tabBarDesktop,
               headerTransform,
             ]}>
-            {renderHeader?.()}
-            <TabBar
-              items={items}
-              selectedPage={currentPage}
-              onSelect={props.onSelect}
-              onPressSelected={onCurrentPageSelected}
+            <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
+                items={items}
+                selectedPage={currentPage}
+                onSelect={props.onSelect}
+                onPressSelected={onCurrentPageSelected}
+              />
+            </View>
           </Animated.View>
         )
       },
       [
         items,
+        isHeaderReady,
         renderHeader,
         headerTransform,
         currentPage,
         onCurrentPageSelected,
         isMobile,
         onTabBarLayout,
-        onHeaderLayout,
+        onHeaderOnlyLayout,
       ],
     )
 
@@ -175,11 +187,23 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
         tabBarPosition="top">
         {toArray(children)
           .filter(Boolean)
-          .map(child => {
-            if (child) {
-              return child(childProps)
+          .map((child, i) => {
+            let output = null
+            if (
+              child != null &&
+              // Defer showing content until we know it won't jump.
+              isHeaderReady &&
+              headerOnlyHeight > 0 &&
+              tabBarHeight > 0
+            ) {
+              output = child(childProps)
             }
-            return null
+            // Pager children must be noncollapsible plain <View>s.
+            return (
+              <View key={i} collapsable={false}>
+                {output}
+              </View>
+            )
           })}
       </Pager>
     )
diff --git a/src/view/com/pager/TabBar.tsx b/src/view/com/pager/TabBar.tsx
index 662d73668..0e08b22d8 100644
--- a/src/view/com/pager/TabBar.tsx
+++ b/src/view/com/pager/TabBar.tsx
@@ -14,7 +14,6 @@ export interface TabBarProps {
   indicatorColor?: string
   onSelect?: (index: number) => void
   onPressSelected?: (index: number) => void
-  onLayout?: (evt: LayoutChangeEvent) => void
 }
 
 export function TabBar({
@@ -24,7 +23,6 @@ export function TabBar({
   indicatorColor,
   onSelect,
   onPressSelected,
-  onLayout,
 }: TabBarProps) {
   const pal = usePalette('default')
   const scrollElRef = useRef<ScrollView>(null)
@@ -68,7 +66,7 @@ export function TabBar({
   const styles = isDesktop || isTablet ? desktopStyles : mobileStyles
 
   return (
-    <View testID={testID} style={[pal.view, styles.outer]} onLayout={onLayout}>
+    <View testID={testID} style={[pal.view, styles.outer]}>
       <DraggableScrollView
         horizontal={true}
         showsHorizontalScrollIndicator={false}
diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx
index 6a3da665e..dcfec1166 100644
--- a/src/view/screens/ProfileFeed.tsx
+++ b/src/view/screens/ProfileFeed.tsx
@@ -332,11 +332,11 @@ export const ProfileFeedScreenInner = observer(
       <View style={s.hContentRegion}>
         <PagerWithHeader
           items={SECTION_TITLES}
+          isHeaderReady={feedInfo?.hasLoaded ?? false}
           renderHeader={renderHeader}
           onCurrentPageSelected={onCurrentPageSelected}>
           {({onScroll, headerHeight, isScrolledDown}) => (
             <FeedSection
-              key="1"
               ref={feedSectionRef}
               feed={feed}
               onScroll={onScroll}
@@ -346,7 +346,6 @@ export const ProfileFeedScreenInner = observer(
           )}
           {({onScroll, headerHeight}) => (
             <ScrollView
-              key="2"
               onScroll={onScroll}
               scrollEventThrottle={1}
               contentContainerStyle={{paddingTop: headerHeight}}>
diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx
index cfe9c4182..624bea027 100644
--- a/src/view/screens/ProfileList.tsx
+++ b/src/view/screens/ProfileList.tsx
@@ -165,11 +165,11 @@ export const ProfileListScreenInner = observer(
         <View style={s.hContentRegion}>
           <PagerWithHeader
             items={SECTION_TITLES_CURATE}
+            isHeaderReady={list.hasLoaded}
             renderHeader={renderHeader}
             onCurrentPageSelected={onCurrentPageSelected}>
             {({onScroll, headerHeight, isScrolledDown}) => (
               <FeedSection
-                key="1"
                 ref={feedSectionRef}
                 feed={feed}
                 onScroll={onScroll}
@@ -179,7 +179,6 @@ export const ProfileListScreenInner = observer(
             )}
             {({onScroll, headerHeight, isScrolledDown}) => (
               <AboutSection
-                key="2"
                 ref={aboutSectionRef}
                 list={list}
                 descriptionRT={list.descriptionRT}
@@ -215,10 +214,10 @@ export const ProfileListScreenInner = observer(
         <View style={s.hContentRegion}>
           <PagerWithHeader
             items={SECTION_TITLES_MOD}
+            isHeaderReady={list.hasLoaded}
             renderHeader={renderHeader}>
             {({onScroll, headerHeight, isScrolledDown}) => (
               <AboutSection
-                key="2"
                 list={list}
                 descriptionRT={list.descriptionRT}
                 creator={list.data ? list.data.creator : undefined}