Skip to content

Commit f0c2778

Browse files
Merge pull request #2 from gustavoabel/feat/add-custom-animations
feat: add custom animation styles and improve types
2 parents 45d4c00 + 6810029 commit f0c2778

File tree

6 files changed

+212
-72
lines changed

6 files changed

+212
-72
lines changed

README.md

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Perfect for apps that want to:
1212
Powered by React Native Reanimated, it provides butter-smooth animations while maintaining 60 FPS. The library seamlessly integrates with React Navigation's ecosystem while adding a layer of motion and interactivity that makes your app feel more dynamic and responsive.
1313

1414
## 📸 How it looks
15+
1516
https://github.com/user-attachments/assets/3b37176b-0ba3-43f7-b1e0-513fb514e825
1617

1718
## Features
@@ -21,6 +22,8 @@ https://github.com/user-attachments/assets/3b37176b-0ba3-43f7-b1e0-513fb514e825
2122
- Built-in icon support
2223
- TypeScript support
2324
- Works with React Navigation
25+
- Advanced animation configurations
26+
- Custom animation styles per tab
2427

2528
## Installation
2629

@@ -129,12 +132,11 @@ cd ..
129132

130133
```typescript
131134
import { View } from 'react-native';
132-
133135
import { createMotionTabs } from 'react-native-motion-tabs';
134136
import { NavigationContainer } from '@react-navigation/native';
135137

136138
function ExampleScreen() {
137-
return <View style={{flex: 1}} />;
139+
return <View style={{ flex: 1, backgroundColor: 'white' }} />;
138140
}
139141

140142
const Tabs = createMotionTabs({
@@ -144,12 +146,33 @@ const Tabs = createMotionTabs({
144146
component: ExampleScreen,
145147
icon: 'home',
146148
iconType: 'Ionicons',
149+
animationConfig: {
150+
stiffness: 100,
151+
overshootClamping: false,
152+
restDisplacementThreshold: 0.001,
153+
restSpeedThreshold: 0.001,
154+
},
155+
animationStyle: {
156+
scale: 1.2,
157+
rotate: 360,
158+
opacity: 0.8,
159+
},
147160
},
148161
{
149162
name: 'Search',
150163
component: ExampleScreen,
151164
icon: 'search',
152165
iconType: 'Ionicons',
166+
animationConfig: {
167+
stiffness: 100,
168+
overshootClamping: false,
169+
restDisplacementThreshold: 0.001,
170+
restSpeedThreshold: 0.001,
171+
},
172+
animationStyle: {
173+
scale: 1.1,
174+
rotate: 180,
175+
},
153176
},
154177
{
155178
name: 'Favorites',
@@ -169,6 +192,12 @@ const Tabs = createMotionTabs({
169192
activeText: '#FFFFFF',
170193
inactiveText: '#000000',
171194
backgroundColor: '#FFFFFF',
195+
animationConfig: {
196+
stiffness: 100,
197+
overshootClamping: false,
198+
restDisplacementThreshold: 0.001,
199+
restSpeedThreshold: 0.001,
200+
},
172201
},
173202
});
174203

@@ -220,3 +249,36 @@ MIT © [Filipi Rafael](https://github.com/filipirafael)
220249
---
221250

222251
Made with ❤️ by [@filipiRafael3](https://x.com/filipiRafael3)
252+
253+
## Animation Configuration
254+
255+
The library uses React Native Reanimated's `withSpring` for animations. Here are the available configuration options:
256+
257+
### Animation Config
258+
259+
- `stiffness`: Controls how "springy" the animation is (default: 100)
260+
- `overshootClamping`: Prevents the animation from overshooting its target (default: false)
261+
- `restDisplacementThreshold`: The minimum displacement from the target to consider the animation complete (default: 0.001)
262+
- `restSpeedThreshold`: The minimum speed to consider the animation complete (default: 0.001)
263+
264+
### Animation Style
265+
266+
- `scale`: Scale factor for the icon when active (default: 1.2)
267+
- `rotate`: Rotation in degrees for the icon when active (default: 0)
268+
- `opacity`: Opacity value for the icon when active (default: 1)
269+
270+
Example:
271+
272+
```typescript
273+
animationConfig: {
274+
stiffness: 100,
275+
overshootClamping: false,
276+
restDisplacementThreshold: 0.001,
277+
restSpeedThreshold: 0.001,
278+
},
279+
animationStyle: {
280+
scale: 1.2,
281+
rotate: 360,
282+
opacity: 0.8,
283+
}
284+
```

example/ios/Podfile.lock

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1343,27 +1343,6 @@ PODS:
13431343
- ReactCommon/turbomodule/bridging
13441344
- ReactCommon/turbomodule/core
13451345
- Yoga
1346-
- react-native-unistyles (2.20.0):
1347-
- DoubleConversion
1348-
- glog
1349-
- hermes-engine
1350-
- RCT-Folly (= 2024.01.01.00)
1351-
- RCTRequired
1352-
- RCTTypeSafety
1353-
- React-Core
1354-
- React-debug
1355-
- React-Fabric
1356-
- React-featureflags
1357-
- React-graphics
1358-
- React-ImageManager
1359-
- React-NativeModulesApple
1360-
- React-RCTFabric
1361-
- React-rendererdebug
1362-
- React-utils
1363-
- ReactCodegen
1364-
- ReactCommon/turbomodule/bridging
1365-
- ReactCommon/turbomodule/core
1366-
- Yoga
13671346
- React-nativeconfig (0.76.5)
13681347
- React-NativeModulesApple (0.76.5):
13691348
- glog
@@ -1833,7 +1812,6 @@ DEPENDENCIES:
18331812
- React-Mapbuffer (from `../node_modules/react-native/ReactCommon`)
18341813
- React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`)
18351814
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
1836-
- react-native-unistyles (from `../node_modules/react-native-unistyles`)
18371815
- React-nativeconfig (from `../node_modules/react-native/ReactCommon`)
18381816
- React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
18391817
- React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`)
@@ -1958,8 +1936,6 @@ EXTERNAL SOURCES:
19581936
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks"
19591937
react-native-safe-area-context:
19601938
:path: "../node_modules/react-native-safe-area-context"
1961-
react-native-unistyles:
1962-
:path: "../node_modules/react-native-unistyles"
19631939
React-nativeconfig:
19641940
:path: "../node_modules/react-native/ReactCommon"
19651941
React-NativeModulesApple:
@@ -2067,7 +2043,6 @@ SPEC CHECKSUMS:
20672043
React-Mapbuffer: c174e11bdea12dce07df8669d6c0dc97eb0c7706
20682044
React-microtasksnativemodule: 8a80099ad7391f4e13a48b12796d96680f120dc6
20692045
react-native-safe-area-context: 458f6b948437afcb59198016b26bbd02ff9c3b47
2070-
react-native-unistyles: 0eb1afdd80a5c6a408e60fb58516d44eb7fea30c
20712046
React-nativeconfig: f7ab6c152e780b99a8c17448f2d99cf5f69a2311
20722047
React-NativeModulesApple: 70600f7edfc2c2a01e39ab13a20fd59f4c60df0b
20732048
React-perflogger: ceb97dd4e5ca6ff20eebb5a6f9e00312dcdea872

src/components/BottomTab/BottomTab.tsx

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ import Animated, {
1212
import { isAndroid } from '../../config/platform';
1313
import { BottomTabButton } from '../BottomTabButton/BottomTabButton';
1414
import { stylesheet } from './styles';
15-
import { defaultTheme, type StyleConfig } from '../../types';
15+
import {
16+
defaultTheme,
17+
type StyleConfig,
18+
type AnimationConfig,
19+
type IconType,
20+
} from '../../types';
1621

1722
type DimensionsProps = {
1823
height: number;
@@ -26,7 +31,10 @@ export const BottomTab = ({
2631
tabsConfig,
2732
theme,
2833
}: BottomTabBarProps & { theme?: StyleConfig } & {
29-
tabsConfig: Record<string, { icon: string; iconType: string }>;
34+
tabsConfig: Record<
35+
string,
36+
{ icon: string; iconType: IconType; animationConfig?: AnimationConfig }
37+
>;
3038
}) => {
3139
const [dimensions, setDimensions] = useState<DimensionsProps>({
3240
height: 20,
@@ -90,9 +98,17 @@ export const BottomTab = ({
9098
const isFocused = state.index === index;
9199

92100
const onPress = () => {
93-
tabPositionX.value = withSpring(buttonWidth * index, {
94-
duration: 1500,
95-
});
101+
const config = {
102+
stiffness: theme?.animationConfig?.stiffness || 100,
103+
overshootClamping:
104+
theme?.animationConfig?.overshootClamping || false,
105+
restDisplacementThreshold:
106+
theme?.animationConfig?.restDisplacementThreshold || 0.001,
107+
restSpeedThreshold:
108+
theme?.animationConfig?.restSpeedThreshold || 0.001,
109+
};
110+
111+
tabPositionX.value = withSpring(buttonWidth * index, config);
96112

97113
const event = navigation.emit({
98114
type: 'tabPress',
@@ -124,6 +140,7 @@ export const BottomTab = ({
124140
}}
125141
theme={theme || defaultTheme}
126142
label={label}
143+
animationConfig={tabsConfig[route.name]?.animationConfig}
127144
/>
128145
);
129146
}

src/components/BottomTabButton/BottomTabButton.tsx

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ import Animated, {
1212
interpolate,
1313
} from 'react-native-reanimated';
1414

15-
import type { TabRoute, StyleConfig } from '../../types';
15+
import type {
16+
TabRoute,
17+
StyleConfig,
18+
AnimationConfig,
19+
AnimationStyleConfig,
20+
} from '../../types';
1621
import { stylesheet } from './styles';
1722

1823
type Props = {
@@ -22,6 +27,8 @@ type Props = {
2227
route: TabRoute;
2328
theme: StyleConfig;
2429
label: string;
30+
animationConfig?: AnimationConfig;
31+
animationStyle?: AnimationStyleConfig;
2532
};
2633

2734
export const BottomTabButton = ({
@@ -31,6 +38,8 @@ export const BottomTabButton = ({
3138
route,
3239
theme,
3340
label,
41+
animationConfig,
42+
animationStyle,
3443
}: Props) => {
3544
const scale = useSharedValue(0);
3645

@@ -69,20 +78,38 @@ export const BottomTabButton = ({
6978
});
7079

7180
const animatedIconStyle = useAnimatedStyle(() => {
72-
const scaleValue = interpolate(scale.value, [0, 1], [1, 1.2]);
81+
const scaleValue = interpolate(
82+
scale.value,
83+
[0, 1],
84+
[1, animationStyle?.scale || 1.2]
85+
);
7386
const top = interpolate(scale.value, [0, 1], [0, 9]);
87+
const rotate = interpolate(
88+
scale.value,
89+
[0, 1],
90+
[0, animationStyle?.rotate || 0]
91+
);
7492
return {
75-
transform: [{ scale: scaleValue }],
93+
transform: [{ scale: scaleValue }, { rotate: `${rotate}deg` }],
7694
top,
95+
opacity: animationStyle?.opacity || 1,
7796
};
7897
});
7998

8099
useEffect(() => {
100+
const config = {
101+
stiffness: animationConfig?.stiffness || 100,
102+
overshootClamping: animationConfig?.overshootClamping || false,
103+
restDisplacementThreshold:
104+
animationConfig?.restDisplacementThreshold || 0.001,
105+
restSpeedThreshold: animationConfig?.restSpeedThreshold || 0.001,
106+
};
107+
81108
scale.value = withSpring(
82109
typeof isFocused === 'boolean' ? (isFocused ? 1 : 0) : isFocused,
83-
{ duration: 350 }
110+
config
84111
);
85-
}, [scale, isFocused]);
112+
}, [scale, isFocused, animationConfig]);
86113

87114
const buttonStyle = StyleSheet.flatten([stylesheet.button]);
88115

Lines changed: 49 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,57 @@
11
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
2+
import type { BottomTabBarProps } from '@react-navigation/bottom-tabs';
23

34
import { BottomTab } from '../components/BottomTab/BottomTab';
4-
import type { MotionTabsConfig, TabRoute } from '../types';
5+
import type {
6+
MotionTabsConfig,
7+
IconType,
8+
AnimationConfig,
9+
AnimationStyleConfig,
10+
} from '../types';
511

612
const Tab = createBottomTabNavigator();
713

8-
export function createMotionTabs({ tabs, style, options }: MotionTabsConfig) {
9-
return function MotionTabs() {
10-
const tabsConfig = tabs.reduce(
11-
(acc, tab) => {
12-
acc[tab.name] = {
13-
name: tab.name,
14-
icon: tab.icon || 'circle',
15-
iconType: tab.iconType || 'Ionicons',
16-
};
17-
return acc;
18-
},
19-
{} as Record<string, TabRoute>
20-
);
14+
export function MotionTabs({ tabs, style, options }: MotionTabsConfig) {
15+
const tabsConfig = tabs.reduce(
16+
(acc, tab) => {
17+
acc[tab.name] = {
18+
icon: tab.icon || 'home',
19+
iconType: tab.iconType || 'Ionicons',
20+
animationConfig: tab.animationConfig,
21+
animationStyle: tab.animationStyle,
22+
};
23+
return acc;
24+
},
25+
{} as Record<
26+
string,
27+
{
28+
icon: string;
29+
iconType: IconType;
30+
animationConfig?: AnimationConfig;
31+
animationStyle?: AnimationStyleConfig;
32+
}
33+
>
34+
);
2135

22-
return (
23-
<Tab.Navigator
24-
screenOptions={{
25-
headerShown: false,
26-
...options,
27-
}}
28-
// eslint-disable-next-line react/no-unstable-nested-components
29-
tabBar={(props: any) => (
30-
<BottomTab {...props} theme={style} tabsConfig={tabsConfig} />
31-
)}
32-
>
33-
{tabs.map(({ name, component }) => (
34-
<Tab.Screen key={name} name={name} component={component} />
35-
))}
36-
</Tab.Navigator>
37-
);
38-
};
36+
const renderTabBar = (props: BottomTabBarProps) => (
37+
<BottomTab {...props} theme={style} tabsConfig={tabsConfig} />
38+
);
39+
40+
return (
41+
<Tab.Navigator
42+
screenOptions={{
43+
headerShown: false,
44+
...options,
45+
}}
46+
tabBar={renderTabBar}
47+
>
48+
{tabs.map(({ name, component }) => (
49+
<Tab.Screen key={name} name={name} component={component} />
50+
))}
51+
</Tab.Navigator>
52+
);
53+
}
54+
55+
export function createMotionTabs(config: MotionTabsConfig) {
56+
return () => <MotionTabs {...config} />;
3957
}

0 commit comments

Comments
 (0)