Skip to content

Commit 8dcd126

Browse files
committed
fix(activity): tab indicator animation
1 parent 136a3d4 commit 8dcd126

File tree

1 file changed

+41
-21
lines changed

1 file changed

+41
-21
lines changed

src/components/Tabs.tsx

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import React, { ReactElement, memo, useState } from 'react';
1+
import React, { ReactElement, memo, useEffect, useState } from 'react';
22
import Animated, {
33
useAnimatedStyle,
4+
useSharedValue,
45
withTiming,
56
} from 'react-native-reanimated';
67
import {
@@ -18,12 +19,10 @@ import colors from '../styles/colors';
1819
import { CaptionB } from '../styles/text';
1920
import { TActivityFilter } from '../utils/activity';
2021

21-
const tabsGap = 4;
22-
23-
export type TTab = {
24-
id: string;
25-
filter: TActivityFilter;
26-
};
22+
export type TTab = { id: string; filter: TActivityFilter };
23+
type TTabLayout = { width: number; height: number; x: number; y: number };
24+
type TTabLayouts = Record<string, TTabLayout>;
25+
const initialTabLayout: TTabLayout = { width: 0, height: 0, x: 0, y: 0 };
2726

2827
const Tab = ({
2928
text,
@@ -62,28 +61,50 @@ const Tabs = ({
6261
onPress: (index: number) => void;
6362
}): ReactElement => {
6463
const { t } = useTranslation('wallet');
65-
const [tabWidth, setTabWidth] = useState(0);
64+
const activeTabLayout = useSharedValue<TTabLayout>(initialTabLayout);
65+
const [layouts, setLayouts] = useState<TTabLayouts>({});
66+
67+
useEffect(() => {
68+
// Set the active tab layout when the active tab changes
69+
const layout = layouts[tabs[activeTab].id];
70+
if (layout) {
71+
activeTabLayout.value = withTiming(layout);
72+
}
73+
}, [activeTab, layouts, tabs, activeTabLayout]);
6674

67-
const animatedTabStyle = useAnimatedStyle(() => {
68-
return { left: withTiming((tabWidth + tabsGap) * activeTab) };
69-
}, [tabWidth, activeTab]);
75+
const animatedStyle = useAnimatedStyle(() => {
76+
return {
77+
height: activeTabLayout.value.height,
78+
top: activeTabLayout.value.y,
79+
width: activeTabLayout.value.width,
80+
left: activeTabLayout.value.x,
81+
};
82+
});
7083

71-
const onLayout = (event: LayoutChangeEvent): void => {
72-
setTabWidth(event.nativeEvent.layout.width);
84+
const handleLayout = (
85+
id: string,
86+
event: LayoutChangeEvent,
87+
index: number,
88+
): void => {
89+
const { layout } = event.nativeEvent;
90+
setLayouts((prevLayouts) => ({ ...prevLayouts, [id]: layout }));
91+
92+
if (index === activeTab) {
93+
// Set the active tab layout on initial render
94+
activeTabLayout.value = layout;
95+
}
7396
};
7497

7598
return (
7699
<View style={[styles.root, style]} testID="Tabs">
77-
<Animated.View
78-
style={[styles.activeTab, animatedTabStyle, { width: tabWidth }]}
79-
/>
100+
<Animated.View style={[styles.activeTab, animatedStyle]} />
80101
{tabs.map((tab, index) => (
81102
<Tab
82103
key={tab.id}
83104
text={t('activity_tabs.' + tab.id)}
84105
active={activeTab === index}
85106
testID={`Tab-${tab.id}`}
86-
onLayout={onLayout}
107+
onLayout={(event) => handleLayout(tab.id, event, index)}
87108
onPress={(): void => onPress(index)}
88109
/>
89110
))}
@@ -94,7 +115,7 @@ const Tabs = ({
94115
const styles = StyleSheet.create({
95116
root: {
96117
flexDirection: 'row',
97-
gap: tabsGap,
118+
gap: 4,
98119
},
99120
tab: {
100121
flex: 1,
@@ -105,10 +126,9 @@ const styles = StyleSheet.create({
105126
paddingVertical: 10,
106127
},
107128
activeTab: {
108-
backgroundColor: colors.brand,
109-
height: 2,
129+
borderBottomWidth: 2,
130+
borderColor: colors.brand,
110131
position: 'absolute',
111-
top: '95%',
112132
zIndex: 1,
113133
},
114134
});

0 commit comments

Comments
 (0)