From 8d6d60056bd1f98b790533fab7f88b9347d98fe2 Mon Sep 17 00:00:00 2001 From: gustavoabel Date: Sat, 5 Apr 2025 15:14:26 -0300 Subject: [PATCH 1/6] feat: implement customizable animated navigation --- src/components/BottomTab/BottomTab.tsx | 27 +++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/components/BottomTab/BottomTab.tsx b/src/components/BottomTab/BottomTab.tsx index 755b619..afdfa65 100644 --- a/src/components/BottomTab/BottomTab.tsx +++ b/src/components/BottomTab/BottomTab.tsx @@ -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; @@ -26,7 +31,10 @@ export const BottomTab = ({ tabsConfig, theme, }: BottomTabBarProps & { theme?: StyleConfig } & { - tabsConfig: Record; + tabsConfig: Record< + string, + { icon: string; iconType: IconType; animationConfig?: AnimationConfig } + >; }) => { const [dimensions, setDimensions] = useState({ height: 20, @@ -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', @@ -124,6 +140,7 @@ export const BottomTab = ({ }} theme={theme || defaultTheme} label={label} + animationConfig={tabsConfig[route.name]?.animationConfig} /> ); } From 09fa74ed6866d57e773fa781a618003c4e04ba9b Mon Sep 17 00:00:00 2001 From: gustavoabel Date: Sat, 5 Apr 2025 15:15:22 -0300 Subject: [PATCH 2/6] feat: add animation support for tab transitions --- .../BottomTabButton/BottomTabButton.tsx | 37 ++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/src/components/BottomTabButton/BottomTabButton.tsx b/src/components/BottomTabButton/BottomTabButton.tsx index 049a042..1280e98 100644 --- a/src/components/BottomTabButton/BottomTabButton.tsx +++ b/src/components/BottomTabButton/BottomTabButton.tsx @@ -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 = { @@ -22,6 +27,8 @@ type Props = { route: TabRoute; theme: StyleConfig; label: string; + animationConfig?: AnimationConfig; + animationStyle?: AnimationStyleConfig; }; export const BottomTabButton = ({ @@ -31,6 +38,8 @@ export const BottomTabButton = ({ route, theme, label, + animationConfig, + animationStyle, }: Props) => { const scale = useSharedValue(0); @@ -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]); From 83c20d6bbaef63e16cdf89c53eddae80134faa3a Mon Sep 17 00:00:00 2001 From: gustavoabel Date: Sat, 5 Apr 2025 15:16:56 -0300 Subject: [PATCH 3/6] feat: add support for custom animations --- src/navigation/createMotionTabs.tsx | 80 ++++++++++++++++++----------- 1 file changed, 49 insertions(+), 31 deletions(-) diff --git a/src/navigation/createMotionTabs.tsx b/src/navigation/createMotionTabs.tsx index 7ca6875..1bd900b 100644 --- a/src/navigation/createMotionTabs.tsx +++ b/src/navigation/createMotionTabs.tsx @@ -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 - ); +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 ( - ( - - )} - > - {tabs.map(({ name, component }) => ( - - ))} - - ); - }; + const renderTabBar = (props: BottomTabBarProps) => ( + + ); + + return ( + + {tabs.map(({ name, component }) => ( + + ))} + + ); +} + +export function createMotionTabs(config: MotionTabsConfig) { + return () => ; } From 9a959de4a840aba7fa7e8cf116eb26d90027ca63 Mon Sep 17 00:00:00 2001 From: gustavoabel Date: Sat, 5 Apr 2025 15:17:36 -0300 Subject: [PATCH 4/6] feat: improve type safety across the library --- src/types/index.ts | 49 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/src/types/index.ts b/src/types/index.ts index bd4992f..efc5e27 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,12 +1,42 @@ -import type { ComponentType } from 'react'; +import type { ComponentType, ReactNode } from 'react'; import type { BottomTabNavigationOptions } from '@react-navigation/bottom-tabs'; +export type IconType = + | 'Ionicons' + | 'MaterialIcons' + | 'MaterialCommunityIcons' + | 'Entypo' + | 'FontAwesome' + | 'AntDesign' + | 'Feather' + | 'SimpleLineIcons' + | 'Octicons' + | 'Zocial'; + +export type AnimationConfig = { + duration?: number; + damping?: number; + stiffness?: number; + mass?: number; + overshootClamping?: boolean; + restDisplacementThreshold?: number; + restSpeedThreshold?: number; +}; + +export type AnimationStyleConfig = { + scale?: number; + rotate?: number; + opacity?: number; +}; + export type TabConfig = { name: string; - component: ComponentType; + component: ComponentType<{ children?: ReactNode }>; icon: string; - iconType: string; + iconType: IconType; + animationConfig?: AnimationConfig; + animationStyle?: AnimationStyleConfig; }; export type StyleConfig = { @@ -17,6 +47,7 @@ export type StyleConfig = { shadowColor?: string; tabBarHeight?: number; marginHorizontal?: number; + animationConfig?: AnimationConfig; }; export const defaultTheme: StyleConfig = { @@ -27,6 +58,12 @@ export const defaultTheme: StyleConfig = { shadowColor: '#000000', tabBarHeight: 60, marginHorizontal: 40, + animationConfig: { + stiffness: 100, + overshootClamping: false, + restDisplacementThreshold: 0.001, + restSpeedThreshold: 0.001, + }, }; export type MotionTabsConfig = { @@ -38,7 +75,9 @@ export type MotionTabsConfig = { export type TabRoute = { name: string; icon?: string; - iconType?: string; + iconType?: IconType; + animationConfig?: AnimationConfig; + animationStyle?: AnimationStyleConfig; }; export type BottomTabButtonProps = { @@ -48,4 +87,6 @@ export type BottomTabButtonProps = { route: TabRoute; theme: StyleConfig; label: string; + animationConfig?: AnimationConfig; + animationStyle?: AnimationStyleConfig; }; From be8e7e00fb4e17a3630350ee399cd476ef724f67 Mon Sep 17 00:00:00 2001 From: gustavoabel Date: Sat, 5 Apr 2025 15:18:38 -0300 Subject: [PATCH 5/6] build(deps): remove unused dependecy --- example/ios/Podfile.lock | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index aaed4d2..c8c643b 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -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 @@ -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`) @@ -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: @@ -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 From 68100294dff13c192f825043163196a32f9e27a3 Mon Sep 17 00:00:00 2001 From: gustavoabel Date: Sat, 5 Apr 2025 15:18:52 -0300 Subject: [PATCH 6/6] docs: update README.md --- README.md | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a3cb049..7df5483 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -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 ; + return ; } const Tabs = createMotionTabs({ @@ -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', @@ -169,6 +192,12 @@ const Tabs = createMotionTabs({ activeText: '#FFFFFF', inactiveText: '#000000', backgroundColor: '#FFFFFF', + animationConfig: { + stiffness: 100, + overshootClamping: false, + restDisplacementThreshold: 0.001, + restSpeedThreshold: 0.001, + }, }, }); @@ -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, +} +```