Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 64 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Perfect for apps that want to:
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.

## 📸 How it looks

https://github.com/user-attachments/assets/3b37176b-0ba3-43f7-b1e0-513fb514e825

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

## Installation

Expand Down Expand Up @@ -129,12 +132,11 @@ cd ..

```typescript
import { View } from 'react-native';

import { createMotionTabs } from 'react-native-motion-tabs';
import { NavigationContainer } from '@react-navigation/native';

function ExampleScreen() {
return <View style={{flex: 1}} />;
return <View style={{ flex: 1, backgroundColor: 'white' }} />;
}

const Tabs = createMotionTabs({
Expand All @@ -144,12 +146,33 @@ const Tabs = createMotionTabs({
component: ExampleScreen,
icon: 'home',
iconType: 'Ionicons',
animationConfig: {
stiffness: 100,
overshootClamping: false,
restDisplacementThreshold: 0.001,
restSpeedThreshold: 0.001,
},
animationStyle: {
scale: 1.2,
rotate: 360,
opacity: 0.8,
},
},
{
name: 'Search',
component: ExampleScreen,
icon: 'search',
iconType: 'Ionicons',
animationConfig: {
stiffness: 100,
overshootClamping: false,
restDisplacementThreshold: 0.001,
restSpeedThreshold: 0.001,
},
animationStyle: {
scale: 1.1,
rotate: 180,
},
},
{
name: 'Favorites',
Expand All @@ -169,6 +192,12 @@ const Tabs = createMotionTabs({
activeText: '#FFFFFF',
inactiveText: '#000000',
backgroundColor: '#FFFFFF',
animationConfig: {
stiffness: 100,
overshootClamping: false,
restDisplacementThreshold: 0.001,
restSpeedThreshold: 0.001,
},
},
});

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

Made with ❤️ by [@filipiRafael3](https://x.com/filipiRafael3)

## Animation Configuration

The library uses React Native Reanimated's `withSpring` for animations. Here are the available configuration options:

### Animation Config

- `stiffness`: Controls how "springy" the animation is (default: 100)
- `overshootClamping`: Prevents the animation from overshooting its target (default: false)
- `restDisplacementThreshold`: The minimum displacement from the target to consider the animation complete (default: 0.001)
- `restSpeedThreshold`: The minimum speed to consider the animation complete (default: 0.001)

### Animation Style

- `scale`: Scale factor for the icon when active (default: 1.2)
- `rotate`: Rotation in degrees for the icon when active (default: 0)
- `opacity`: Opacity value for the icon when active (default: 1)

Example:

```typescript
animationConfig: {
stiffness: 100,
overshootClamping: false,
restDisplacementThreshold: 0.001,
restSpeedThreshold: 0.001,
},
animationStyle: {
scale: 1.2,
rotate: 360,
opacity: 0.8,
}
```
25 changes: 0 additions & 25 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1343,27 +1343,6 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- react-native-unistyles (2.20.0):
- DoubleConversion
- glog
- hermes-engine
- RCT-Folly (= 2024.01.01.00)
- RCTRequired
- RCTTypeSafety
- React-Core
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-ImageManager
- React-NativeModulesApple
- React-RCTFabric
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- React-nativeconfig (0.76.5)
- React-NativeModulesApple (0.76.5):
- glog
Expand Down Expand Up @@ -1833,7 +1812,6 @@ DEPENDENCIES:
- React-Mapbuffer (from `../node_modules/react-native/ReactCommon`)
- React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`)
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
- react-native-unistyles (from `../node_modules/react-native-unistyles`)
- React-nativeconfig (from `../node_modules/react-native/ReactCommon`)
- React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
- React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`)
Expand Down Expand Up @@ -1958,8 +1936,6 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks"
react-native-safe-area-context:
:path: "../node_modules/react-native-safe-area-context"
react-native-unistyles:
:path: "../node_modules/react-native-unistyles"
React-nativeconfig:
:path: "../node_modules/react-native/ReactCommon"
React-NativeModulesApple:
Expand Down Expand Up @@ -2067,7 +2043,6 @@ SPEC CHECKSUMS:
React-Mapbuffer: c174e11bdea12dce07df8669d6c0dc97eb0c7706
React-microtasksnativemodule: 8a80099ad7391f4e13a48b12796d96680f120dc6
react-native-safe-area-context: 458f6b948437afcb59198016b26bbd02ff9c3b47
react-native-unistyles: 0eb1afdd80a5c6a408e60fb58516d44eb7fea30c
React-nativeconfig: f7ab6c152e780b99a8c17448f2d99cf5f69a2311
React-NativeModulesApple: 70600f7edfc2c2a01e39ab13a20fd59f4c60df0b
React-perflogger: ceb97dd4e5ca6ff20eebb5a6f9e00312dcdea872
Expand Down
27 changes: 22 additions & 5 deletions src/components/BottomTab/BottomTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ import Animated, {
import { isAndroid } from '../../config/platform';
import { BottomTabButton } from '../BottomTabButton/BottomTabButton';
import { stylesheet } from './styles';
import { defaultTheme, type StyleConfig } from '../../types';
import {
defaultTheme,
type StyleConfig,
type AnimationConfig,
type IconType,
} from '../../types';

type DimensionsProps = {
height: number;
Expand All @@ -26,7 +31,10 @@ export const BottomTab = ({
tabsConfig,
theme,
}: BottomTabBarProps & { theme?: StyleConfig } & {
tabsConfig: Record<string, { icon: string; iconType: string }>;
tabsConfig: Record<
string,
{ icon: string; iconType: IconType; animationConfig?: AnimationConfig }
>;
}) => {
const [dimensions, setDimensions] = useState<DimensionsProps>({
height: 20,
Expand Down Expand Up @@ -90,9 +98,17 @@ export const BottomTab = ({
const isFocused = state.index === index;

const onPress = () => {
tabPositionX.value = withSpring(buttonWidth * index, {
duration: 1500,
});
const config = {
stiffness: theme?.animationConfig?.stiffness || 100,
overshootClamping:
theme?.animationConfig?.overshootClamping || false,
restDisplacementThreshold:
theme?.animationConfig?.restDisplacementThreshold || 0.001,
restSpeedThreshold:
theme?.animationConfig?.restSpeedThreshold || 0.001,
};

tabPositionX.value = withSpring(buttonWidth * index, config);

const event = navigation.emit({
type: 'tabPress',
Expand Down Expand Up @@ -124,6 +140,7 @@ export const BottomTab = ({
}}
theme={theme || defaultTheme}
label={label}
animationConfig={tabsConfig[route.name]?.animationConfig}
/>
);
}
Expand Down
37 changes: 32 additions & 5 deletions src/components/BottomTabButton/BottomTabButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ import Animated, {
interpolate,
} from 'react-native-reanimated';

import type { TabRoute, StyleConfig } from '../../types';
import type {
TabRoute,
StyleConfig,
AnimationConfig,
AnimationStyleConfig,
} from '../../types';
import { stylesheet } from './styles';

type Props = {
Expand All @@ -22,6 +27,8 @@ type Props = {
route: TabRoute;
theme: StyleConfig;
label: string;
animationConfig?: AnimationConfig;
animationStyle?: AnimationStyleConfig;
};

export const BottomTabButton = ({
Expand All @@ -31,6 +38,8 @@ export const BottomTabButton = ({
route,
theme,
label,
animationConfig,
animationStyle,
}: Props) => {
const scale = useSharedValue(0);

Expand Down Expand Up @@ -69,20 +78,38 @@ export const BottomTabButton = ({
});

const animatedIconStyle = useAnimatedStyle(() => {
const scaleValue = interpolate(scale.value, [0, 1], [1, 1.2]);
const scaleValue = interpolate(
scale.value,
[0, 1],
[1, animationStyle?.scale || 1.2]
);
const top = interpolate(scale.value, [0, 1], [0, 9]);
const rotate = interpolate(
scale.value,
[0, 1],
[0, animationStyle?.rotate || 0]
);
return {
transform: [{ scale: scaleValue }],
transform: [{ scale: scaleValue }, { rotate: `${rotate}deg` }],
top,
opacity: animationStyle?.opacity || 1,
};
});

useEffect(() => {
const config = {
stiffness: animationConfig?.stiffness || 100,
overshootClamping: animationConfig?.overshootClamping || false,
restDisplacementThreshold:
animationConfig?.restDisplacementThreshold || 0.001,
restSpeedThreshold: animationConfig?.restSpeedThreshold || 0.001,
};

scale.value = withSpring(
typeof isFocused === 'boolean' ? (isFocused ? 1 : 0) : isFocused,
{ duration: 350 }
config
);
}, [scale, isFocused]);
}, [scale, isFocused, animationConfig]);

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

Expand Down
80 changes: 49 additions & 31 deletions src/navigation/createMotionTabs.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,57 @@
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import type { BottomTabBarProps } from '@react-navigation/bottom-tabs';

import { BottomTab } from '../components/BottomTab/BottomTab';
import type { MotionTabsConfig, TabRoute } from '../types';
import type {
MotionTabsConfig,
IconType,
AnimationConfig,
AnimationStyleConfig,
} from '../types';

const Tab = createBottomTabNavigator();

export function createMotionTabs({ tabs, style, options }: MotionTabsConfig) {
return function MotionTabs() {
const tabsConfig = tabs.reduce(
(acc, tab) => {
acc[tab.name] = {
name: tab.name,
icon: tab.icon || 'circle',
iconType: tab.iconType || 'Ionicons',
};
return acc;
},
{} as Record<string, TabRoute>
);
export function MotionTabs({ tabs, style, options }: MotionTabsConfig) {
const tabsConfig = tabs.reduce(
(acc, tab) => {
acc[tab.name] = {
icon: tab.icon || 'home',
iconType: tab.iconType || 'Ionicons',
animationConfig: tab.animationConfig,
animationStyle: tab.animationStyle,
};
return acc;
},
{} as Record<
string,
{
icon: string;
iconType: IconType;
animationConfig?: AnimationConfig;
animationStyle?: AnimationStyleConfig;
}
>
);

return (
<Tab.Navigator
screenOptions={{
headerShown: false,
...options,
}}
// eslint-disable-next-line react/no-unstable-nested-components
tabBar={(props: any) => (
<BottomTab {...props} theme={style} tabsConfig={tabsConfig} />
)}
>
{tabs.map(({ name, component }) => (
<Tab.Screen key={name} name={name} component={component} />
))}
</Tab.Navigator>
);
};
const renderTabBar = (props: BottomTabBarProps) => (
<BottomTab {...props} theme={style} tabsConfig={tabsConfig} />
);

return (
<Tab.Navigator
screenOptions={{
headerShown: false,
...options,
}}
tabBar={renderTabBar}
>
{tabs.map(({ name, component }) => (
<Tab.Screen key={name} name={name} component={component} />
))}
</Tab.Navigator>
);
}

export function createMotionTabs(config: MotionTabsConfig) {
return () => <MotionTabs {...config} />;
}
Loading