Compare commits

...

15 Commits

Author SHA1 Message Date
Dan Abramov 14b0bed36d Fix container calculation for web resize 2023-10-17 21:28:54 +01:00
Dan Abramov a1f6ed8519 Simplify the state machine to fix web animation 2023-10-17 21:17:13 +01:00
Dan Abramov 247df797c9 Fix viewport calc for web 2023-10-17 21:13:03 +01:00
Dan Abramov 3bc9a0120a Fix indicator on web 2023-10-17 20:25:05 +01:00
Dan Abramov 05d92799ff Fix first render flash 2023-10-17 17:52:06 +01:00
Dan Abramov caa55cba7a Handle press after scroll 2023-10-17 16:46:48 +01:00
Dan Abramov 262125cd88 Detach syncing on tab scroll 2023-10-17 16:46:48 +01:00
Dan Abramov 828e0b0d22 Simplify the calculation 2023-10-17 16:46:48 +01:00
Dan Abramov 493b40a28f Fix the first render 2023-10-17 16:46:48 +01:00
Dan Abramov 386a2448a3 Keep the color in sync too 2023-10-17 16:46:48 +01:00
Dan Abramov cafa1ffb61 Interpolate the indicator 2023-10-17 16:46:48 +01:00
Dan Abramov c884ab93b7 Adjust scroll on feed drag 2023-10-17 16:46:46 +01:00
Dan Abramov e2706f37ba Track progress in a shared value 2023-10-17 16:43:40 +01:00
Dan Abramov 62ac40cb39 Add onPageScroll prop to Pager 2023-10-17 16:42:27 +01:00
Dan Abramov bf7e127cfb Remove existing tab scroll sync logic 2023-10-17 16:42:27 +01:00
5 changed files with 166 additions and 46 deletions
+2 -1
View File
@@ -20,7 +20,6 @@ export const FeedsTabBar = observer(function FeedsTabBarImpl(
) {
const store = useStores()
const pal = usePalette('default')
const brandBlue = useColorSchemeStyle(s.brandBlue, s.blue3)
const {headerMinimalShellTransform} = useMinimalShellMode()
@@ -87,6 +86,8 @@ export const FeedsTabBar = observer(function FeedsTabBarImpl(
onSelect={props.onSelect}
testID={props.testID}
items={items}
dragProgress={props.dragProgress}
dragState={props.dragState}
indicatorColor={pal.colors.link}
/>
</Animated.View>
+27 -2
View File
@@ -1,6 +1,7 @@
import React, {forwardRef} from 'react'
import {Animated, View} from 'react-native'
import {View} from 'react-native'
import PagerView, {PagerViewOnPageSelectedEvent} from 'react-native-pager-view'
import Animated, { useHandler, useEvent } from 'react-native-reanimated';
import {s} from 'lib/styles'
export type PageSelectedEvent = PagerViewOnPageSelectedEvent
@@ -20,9 +21,11 @@ interface Props {
tabBarPosition?: 'top' | 'bottom'
initialPage?: number
renderTabBar: RenderTabBarFn
onPageScroll?: (e: any) => void
onPageSelected?: (index: number) => void
testID?: string
}
export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
function PagerImpl(
{
@@ -30,6 +33,8 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
tabBarPosition = 'top',
initialPage = 0,
renderTabBar,
onPageScroll,
onPageScrollStateChanged,
onPageSelected,
testID,
}: React.PropsWithChildren<Props>,
@@ -57,6 +62,7 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
[pagerView],
)
const handlePageScroll = usePageScrollHandler({ onPageScroll });
return (
<View testID={testID}>
{tabBarPosition === 'top' &&
@@ -68,7 +74,9 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
ref={pagerView}
style={s.h100pct}
initialPage={initialPage}
onPageSelected={onPageSelectedInner}>
onPageSelected={onPageSelectedInner}
onPageScroll={handlePageScroll}
onPageScrollStateChanged={onPageScrollStateChanged}>
{children}
</AnimatedPagerView>
{tabBarPosition === 'bottom' &&
@@ -80,3 +88,20 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
)
},
)
function usePageScrollHandler(handlers, dependencies) {
const { context, doDependenciesDiffer } = useHandler(handlers, dependencies);
const subscribeForEvents = ['onPageScroll'];
return useEvent(
(event) => {
'worklet';
const { onPageScroll } = handlers;
if (onPageScroll && event.eventName.endsWith('onPageScroll')) {
onPageScroll(event, context);
}
},
subscribeForEvents,
doDependenciesDiffer
);
}
+6 -1
View File
@@ -21,6 +21,7 @@ export const Pager = React.forwardRef(function PagerImpl(
initialPage = 0,
renderTabBar,
onPageSelected,
onPageScroll,
}: React.PropsWithChildren<Props>,
ref,
) {
@@ -34,8 +35,12 @@ export const Pager = React.forwardRef(function PagerImpl(
(index: number) => {
setSelectedPage(index)
onPageSelected?.(index)
onPageScroll?.({
position: index,
offset: 0
})
},
[setSelectedPage, onPageSelected],
[setSelectedPage, onPageSelected, onPageScroll],
)
return (
+110 -41
View File
@@ -1,8 +1,9 @@
import React, {useRef, useMemo, useEffect, useState, useCallback} from 'react'
import {StyleSheet, View, ScrollView, LayoutChangeEvent} from 'react-native'
import {Text} from '../util/text/Text'
import React, {useMemo, useCallback, useEffect, useState} from 'react'
import Animated, {useAnimatedReaction, useAnimatedRef, useAnimatedStyle, useDerivedValue, useSharedValue, measure, interpolate, interpolateColor, scrollTo, withSpring} from 'react-native-reanimated'
import {Dimensions, StyleSheet, View, ScrollView} from 'react-native'
import {PressableWithHover} from '../util/PressableWithHover'
import {usePalette} from 'lib/hooks/usePalette'
import {useTheme} from 'lib/ThemeContext'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {isWeb} from 'platform/detection'
import {DraggableScrollView} from './DraggableScrollView'
@@ -18,6 +19,8 @@ export interface TabBarProps {
export function TabBar({
testID,
dragProgress,
dragState,
selectedPage,
items,
indicatorColor,
@@ -25,43 +28,71 @@ export function TabBar({
onPressSelected,
}: TabBarProps) {
const pal = usePalette('default')
const scrollElRef = useRef<ScrollView>(null)
const [itemXs, setItemXs] = useState<number[]>([])
const indicatorStyle = useMemo(
() => ({borderBottomColor: indicatorColor || pal.colors.link}),
[indicatorColor, pal],
)
const contentSize = useSharedValue(0)
const containerSize = useSharedValue(0)
const scrollElRef = useAnimatedRef(null)
const {isDesktop, isTablet} = useWebMediaQueries()
const [layouts, setLayouts] = useState([])
const scrollX = useSharedValue(0)
const didScroll = useSharedValue(false)
// scrolls to the selected item when the page changes
useEffect(() => {
scrollElRef.current?.scrollTo({
x: itemXs[selectedPage] || 0,
})
}, [scrollElRef, itemXs, selectedPage])
const didLayout = (
layouts.length === items.length &&
layouts.every(l => l !== undefined)
);
const onPressItem = useCallback(
(index: number) => {
onSelect?.(index)
if (index === selectedPage) {
onPressSelected?.()
}
},
[onSelect, selectedPage, onPressSelected],
)
const indicatorStyle = useAnimatedStyle(() => {
if (!didLayout) {
return {}
}
return {
width: interpolate(dragProgress.value, layouts.map((l, i) => i), layouts.map(l => l.width)),
left: interpolate(dragProgress.value, layouts.map((l, i) => i), layouts.map(l => l.x)),
}
})
// calculates the x position of each item on mount and on layout change
const onItemLayout = React.useCallback(
(e: LayoutChangeEvent, index: number) => {
const x = e.nativeEvent.layout.x
setItemXs(prev => {
const Xs = [...prev]
Xs[index] = x
return Xs
const onPressItem = (index: number) => {
if (!didScroll.value) {
scrollElRef.current.scrollTo({
x: scrollX.value + ((index - selectedPage) / (items.length - 1)) * (contentSize.value - containerSize.value),
animated: true
})
},
[],
)
}
didScroll.value = false
onSelect?.(index)
if (index === selectedPage) {
onPressSelected?.()
}
};
useAnimatedReaction(() => {
return (dragProgress.value / (items.length - 1)) * (contentSize.value - containerSize.value)
}, (nextX, prevX) => {
if (prevX !== nextX && dragState.value !== 'idle' && !didScroll.value) {
scrollTo(scrollElRef, nextX, 0, false);
}
})
useAnimatedReaction(() => {
return dragState.value
}, (nextDragState, prevDragState) => {
if (nextDragState === 'idle' && nextDragState !== prevDragState) {
const nextX = (dragProgress.value / (items.length - 1)) * (contentSize.value - containerSize.value)
scrollTo(scrollElRef, nextX, 0, true);
didScroll.value = false
}
})
const onItemLayout = (e: LayoutChangeEvent, index: number) => {
const l = e.nativeEvent.layout
setLayouts(ls => items.map((item, i) => {
if (i === index) {
return l
} else {
return ls[i]
}
}))
}
const styles = isDesktop || isTablet ? desktopStyles : mobileStyles
@@ -71,30 +102,68 @@ export function TabBar({
horizontal={true}
showsHorizontalScrollIndicator={false}
ref={scrollElRef}
contentContainerStyle={styles.contentContainer}>
contentContainerStyle={styles.contentContainer}
onContentSizeChange={e => {
contentSize.value = e
}}
onScrollBeginDrag={e => {
didScroll.value = true
}}
onLayout={e => {
containerSize.value = e.nativeEvent.layout.width
}}
onScroll={e => {
scrollX.value = Math.round(e.nativeEvent.contentOffset.x)
}}
scrollEventThrottle={16}>
{items.map((item, i) => {
const selected = i === selectedPage
const isSelected = i === selectedPage
return (
<PressableWithHover
key={item}
onLayout={e => onItemLayout(e, i)}
style={[styles.item, selected && indicatorStyle]}
style={[styles.item, {
borderBottomColor: (!didLayout && isSelected) ? indicatorColor : 'transparent',
borderBottomWidth: 3
}]}
hoverStyle={pal.viewLight}
onPress={() => onPressItem(i)}>
<Text
<MaybeHighlightedText
type={isDesktop || isTablet ? 'xl-bold' : 'lg-bold'}
testID={testID ? `${testID}-${item}` : undefined}
style={selected ? pal.text : pal.textLight}>
approxIndex={dragProgress}
index={i}>
{item}
</Text>
</MaybeHighlightedText>
</PressableWithHover>
)
})}
{didLayout && <Animated.View
style={[{
position: 'absolute',
bottom: 0,
height: 3,
backgroundColor: indicatorColor,
}, indicatorStyle]}
/>}
</DraggableScrollView>
</View>
)
}
export function MaybeHighlightedText({ approxIndex, index, type, ...rest }) {
const pal = usePalette('default')
const theme = useTheme()
const typography = theme.typography[type]
const animatedStyle = useAnimatedStyle(() => ({
color: interpolateColor(Math.min(Math.abs(approxIndex.value - index), 1), [0, 1], [pal.text.color, pal.textLight.color])
}))
return (
<Animated.Text {...rest} style={[typography, animatedStyle]} />
)
}
const desktopStyles = StyleSheet.create({
outer: {
flexDirection: 'row',
+21 -1
View File
@@ -1,6 +1,7 @@
import React from 'react'
import {useWindowDimensions} from 'react-native'
import {useFocusEffect} from '@react-navigation/native'
import {useSharedValue} from 'react-native-reanimated'
import {AppBskyFeedGetFeed as GetCustomFeed} from '@atproto/api'
import {observer} from 'mobx-react-lite'
import isEqual from 'lodash.isequal'
@@ -28,6 +29,8 @@ export const HomeScreen = withAuthRequired(
const [requestedCustomFeeds, setRequestedCustomFeeds] = React.useState<
string[]
>([])
const dragProgress = useSharedValue(selectedPage)
const dragState = useSharedValue('idle')
React.useEffect(() => {
const {pinned} = store.me.savedFeeds
@@ -79,6 +82,19 @@ export const HomeScreen = withAuthRequired(
[store, setSelectedPage],
)
const onPageScroll = React.useCallback(
e => {
'worklet'
const progress = e.offset + e.position
dragProgress.value = progress
},
[dragProgress],
)
const onPageScrollStateChanged = React.useCallback(e => {
dragState.value = e.nativeEvent.pageScrollState
})
const onPressSelected = React.useCallback(() => {
store.emitScreenSoftReset()
}, [store])
@@ -90,12 +106,14 @@ export const HomeScreen = withAuthRequired(
key="FEEDS_TAB_BAR"
selectedPage={props.selectedPage}
onSelect={props.onSelect}
dragProgress={dragProgress}
dragState={dragState}
testID="homeScreenFeedTabs"
onPressSelected={onPressSelected}
/>
)
},
[onPressSelected],
[dragProgress, onPressSelected],
)
const renderFollowingEmptyState = React.useCallback(() => {
@@ -111,6 +129,8 @@ export const HomeScreen = withAuthRequired(
ref={pagerRef}
testID="homeScreen"
onPageSelected={onPageSelected}
onPageScroll={onPageScroll}
onPageScrollStateChanged={onPageScrollStateChanged}
renderTabBar={renderTabBar}
tabBarPosition="top">
<FeedPage