Keep layouts in a shared value

This commit is contained in:
Dan Abramov
2024-11-30 18:47:08 +00:00
parent c06eb2b2b1
commit 7a6d4ff91c
+59 -46
View File
@@ -1,5 +1,5 @@
import {useCallback, useState} from 'react' import {useCallback} from 'react'
import {ScrollView, StyleSheet, View} from 'react-native' import {LayoutChangeEvent, ScrollView, StyleSheet, View} from 'react-native'
import Animated, { import Animated, {
interpolate, interpolate,
runOnUI, runOnUI,
@@ -37,6 +37,7 @@ export function TabBar({
const contentSize = useSharedValue(0) const contentSize = useSharedValue(0)
const containerSize = useSharedValue(0) const containerSize = useSharedValue(0)
const scrollX = useSharedValue(0) const scrollX = useSharedValue(0)
const layouts = useSharedValue<{x: number; width: number}[]>([])
const itemsLength = items.length const itemsLength = items.length
const {dragProgress, dragState} = dragGesture const {dragProgress, dragState} = dragGesture
@@ -106,6 +107,48 @@ export function TabBar({
], ],
) )
const onItemLayout = useCallback(
(i: number, layout: {x: number; width: number}) => {
'worklet'
layouts.modify(ls => {
ls[i] = layout
return ls
})
},
[layouts],
)
const indicatorStyle = useAnimatedStyle(() => {
const layoutsValue = layouts.get()
if (
layoutsValue.length !== itemsLength ||
layoutsValue.some(l => l === undefined)
) {
return {
opacity: 0,
}
}
return {
opacity: 1,
transform: [
{
translateX: interpolate(
dragProgress.get(),
layoutsValue.map((l, i) => i),
layoutsValue.map(l => l.x + l.width / 2 - contentSize.get() / 2),
),
},
{
scaleX: interpolate(
dragProgress.get(),
layoutsValue.map((l, i) => i),
layoutsValue.map(l => (l.width - 12) / contentSize.get()),
),
},
],
}
})
const onPressItem = useCallback( const onPressItem = useCallback(
(index: number) => { (index: number) => {
runOnUI(onPressUIThread)(index) runOnUI(onPressUIThread)(index)
@@ -117,46 +160,6 @@ export function TabBar({
[onSelect, selectedPage, onPressSelected, onPressUIThread], [onSelect, selectedPage, onPressSelected, onPressUIThread],
) )
const [layouts, setLayouts] = useState([])
const didLayout =
layouts.length === items.length && layouts.every(l => l !== undefined)
const indicatorStyle = useAnimatedStyle(() => {
if (!didLayout) {
return {}
}
return {
transform: [
{
translateX: interpolate(
dragProgress.get(),
layouts.map((l, i) => i),
layouts.map(l => l.x + l.width / 2 - contentSize.get() / 2),
),
},
{
scaleX: interpolate(
dragProgress.get(),
layouts.map((l, i) => i),
layouts.map(l => (l.width - 12) / contentSize.get()),
),
},
],
}
})
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]
}
}),
)
}
return ( return (
<View <View
testID={testID} testID={testID}
@@ -186,18 +189,17 @@ export function TabBar({
style={{flexDirection: 'row'}}> style={{flexDirection: 'row'}}>
{items.map((item, i) => { {items.map((item, i) => {
return ( return (
<View key={i} onLayout={e => onItemLayout(e, i)}>
<TabBarItem <TabBarItem
key={i}
index={i} index={i}
testID={testID} testID={testID}
dragProgress={dragProgress} dragProgress={dragProgress}
item={item} item={item}
onPressItem={onPressItem} onPressItem={onPressItem}
onItemLayout={onItemLayout}
/> />
</View>
) )
})} })}
{didLayout && (
<Animated.View <Animated.View
style={[ style={[
indicatorStyle, indicatorStyle,
@@ -211,7 +213,6 @@ export function TabBar({
}, },
]} ]}
/> />
)}
</Animated.View> </Animated.View>
</ScrollView> </ScrollView>
<View style={[pal.border, styles.outerBottomBorder]} /> <View style={[pal.border, styles.outerBottomBorder]} />
@@ -225,12 +226,14 @@ function TabBarItem({
dragProgress, dragProgress,
item, item,
onPressItem, onPressItem,
onItemLayout,
}: { }: {
index: number index: number
testID: string | undefined testID: string | undefined
dragProgress: SharedValue<number> dragProgress: SharedValue<number>
item: string item: string
onPressItem: (index: number) => void onPressItem: (index: number) => void
onItemLayout: (index: number, layout: {x: number; width: number}) => void
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
const style = useAnimatedStyle(() => ({ const style = useAnimatedStyle(() => ({
@@ -241,7 +244,16 @@ function TabBarItem({
'clamp', 'clamp',
), ),
})) }))
const handleLayout = useCallback(
(e: LayoutChangeEvent) => {
runOnUI(onItemLayout)(index, e.nativeEvent.layout)
},
[index, onItemLayout],
)
return ( return (
<View onLayout={handleLayout}>
<PressableWithHover <PressableWithHover
testID={`${testID}-selector-${index}`} testID={`${testID}-selector-${index}`}
style={styles.item} style={styles.item}
@@ -258,6 +270,7 @@ function TabBarItem({
</Text> </Text>
</Animated.View> </Animated.View>
</PressableWithHover> </PressableWithHover>
</View>
) )
} }