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
+95 -82
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,32 +189,30 @@ 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, {
{ position: 'absolute',
position: 'absolute', left: 0,
left: 0, bottom: 0,
bottom: 0, right: 0,
right: 0, borderBottomWidth: 3,
borderBottomWidth: 3, borderColor: pal.link.color,
borderColor: pal.link.color, },
}, ]}
]} />
/>
)}
</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,23 +244,33 @@ function TabBarItem({
'clamp', 'clamp',
), ),
})) }))
const handleLayout = useCallback(
(e: LayoutChangeEvent) => {
runOnUI(onItemLayout)(index, e.nativeEvent.layout)
},
[index, onItemLayout],
)
return ( return (
<PressableWithHover <View onLayout={handleLayout}>
testID={`${testID}-selector-${index}`} <PressableWithHover
style={styles.item} testID={`${testID}-selector-${index}`}
hoverStyle={pal.viewLight} style={styles.item}
onPress={() => onPressItem(index)} hoverStyle={pal.viewLight}
accessibilityRole="tab"> onPress={() => onPressItem(index)}
<Animated.View style={[style, styles.itemInner]}> accessibilityRole="tab">
<Text <Animated.View style={[style, styles.itemInner]}>
emoji <Text
type="lg-bold" emoji
testID={testID ? `${testID}-${item}` : undefined} type="lg-bold"
style={[pal.text, {lineHeight: 20}]}> testID={testID ? `${testID}-${item}` : undefined}
{item} style={[pal.text, {lineHeight: 20}]}>
</Text> {item}
</Animated.View> </Text>
</PressableWithHover> </Animated.View>
</PressableWithHover>
</View>
) )
} }