/ components / FloatingActionButton.tsx
FloatingActionButton.tsx
1 import React, { useRef, useState } from 'react'; 2 import { 3 View, 4 TouchableOpacity, 5 Animated, 6 StyleSheet, 7 ViewStyle, 8 } from 'react-native'; 9 import { Ionicons } from '@expo/vector-icons'; 10 import { useTheme } from '@/constants/ThemeContext'; 11 import { Colors } from '@/constants/Colors'; 12 import { useHapticFeedback } from '@/utils/haptics'; 13 import { useSafeAreaInsets } from 'react-native-safe-area-context'; 14 15 interface FloatingActionButtonProps { 16 onPress?: () => void; 17 onScrollToTop?: () => void; 18 onRefresh?: () => void; 19 style?: ViewStyle; 20 visible?: boolean; 21 } 22 23 export const FloatingActionButton: React.FC<FloatingActionButtonProps> = ({ 24 onPress, 25 onScrollToTop, 26 onRefresh, 27 style, 28 visible = true, 29 }) => { 30 const { actualTheme } = useTheme(); 31 const colors = Colors[actualTheme]; 32 const haptics = useHapticFeedback(); 33 const insets = useSafeAreaInsets(); 34 const [expanded, setExpanded] = useState(false); 35 36 const scaleAnim = useRef(new Animated.Value(1)).current; 37 const rotateAnim = useRef(new Animated.Value(0)).current; 38 const actionScale1 = useRef(new Animated.Value(0)).current; 39 const actionScale2 = useRef(new Animated.Value(0)).current; 40 41 const toggleExpanded = () => { 42 haptics.onPress(); 43 setExpanded(!expanded); 44 45 Animated.parallel([ 46 Animated.spring(rotateAnim, { 47 toValue: expanded ? 0 : 1, 48 useNativeDriver: true, 49 }), 50 Animated.stagger(50, [ 51 Animated.spring(actionScale1, { 52 toValue: expanded ? 0 : 1, 53 useNativeDriver: true, 54 }), 55 Animated.spring(actionScale2, { 56 toValue: expanded ? 0 : 1, 57 useNativeDriver: true, 58 }), 59 ]), 60 ]).start(); 61 }; 62 63 const handleActionPress = (action: () => void) => { 64 haptics.onSelection(); 65 action(); 66 toggleExpanded(); 67 }; 68 69 const handleMainPress = () => { 70 if (expanded) { 71 toggleExpanded(); 72 } else { 73 haptics.onPress(); 74 onPress?.(); 75 } 76 }; 77 78 const rotation = rotateAnim.interpolate({ 79 inputRange: [0, 1], 80 outputRange: ['0deg', '45deg'], 81 }); 82 83 if (!visible) return null; 84 85 return ( 86 <View style={[styles.container, { bottom: insets.bottom + 100 }, style]}> 87 {/* Action Buttons */} 88 {onScrollToTop && ( 89 <Animated.View 90 style={[ 91 styles.actionButton, 92 { 93 transform: [{ scale: actionScale1 }], 94 backgroundColor: colors.card, 95 }, 96 ]} 97 > 98 <TouchableOpacity 99 onPress={() => handleActionPress(onScrollToTop)} 100 style={styles.actionTouchable} 101 > 102 <Ionicons name="arrow-up" size={20} color={colors.primary} /> 103 </TouchableOpacity> 104 </Animated.View> 105 )} 106 107 {onRefresh && ( 108 <Animated.View 109 style={[ 110 styles.actionButton, 111 { 112 transform: [{ scale: actionScale2 }], 113 backgroundColor: colors.card, 114 }, 115 ]} 116 > 117 <TouchableOpacity 118 onPress={() => handleActionPress(onRefresh)} 119 style={styles.actionTouchable} 120 > 121 <Ionicons name="refresh" size={20} color={colors.primary} /> 122 </TouchableOpacity> 123 </Animated.View> 124 )} 125 126 {/* Main Button */} 127 <Animated.View 128 style={[ 129 styles.mainButton, 130 { 131 backgroundColor: colors.primary, 132 transform: [{ scale: scaleAnim }], 133 }, 134 ]} 135 > 136 <TouchableOpacity 137 onPress={handleMainPress} 138 onPressIn={() => { 139 Animated.spring(scaleAnim, { 140 toValue: 0.9, 141 useNativeDriver: true, 142 }).start(); 143 }} 144 onPressOut={() => { 145 Animated.spring(scaleAnim, { 146 toValue: 1, 147 useNativeDriver: true, 148 }).start(); 149 }} 150 style={styles.mainTouchable} 151 > 152 <Animated.View style={{ transform: [{ rotate: rotation }] }}> 153 <Ionicons name="add" size={24} color="#FFFFFF" /> 154 </Animated.View> 155 </TouchableOpacity> 156 </Animated.View> 157 </View> 158 ); 159 }; 160 161 const styles = StyleSheet.create({ 162 container: { 163 position: 'absolute', 164 right: 20, 165 alignItems: 'center', 166 }, 167 mainButton: { 168 width: 56, 169 height: 56, 170 borderRadius: 28, 171 elevation: 8, 172 shadowColor: '#000', 173 shadowOffset: { 174 width: 0, 175 height: 4, 176 }, 177 shadowOpacity: 0.3, 178 shadowRadius: 8, 179 }, 180 mainTouchable: { 181 flex: 1, 182 justifyContent: 'center', 183 alignItems: 'center', 184 }, 185 actionButton: { 186 width: 44, 187 height: 44, 188 borderRadius: 22, 189 marginBottom: 12, 190 elevation: 4, 191 shadowColor: '#000', 192 shadowOffset: { 193 width: 0, 194 height: 2, 195 }, 196 shadowOpacity: 0.2, 197 shadowRadius: 4, 198 }, 199 actionTouchable: { 200 flex: 1, 201 justifyContent: 'center', 202 alignItems: 'center', 203 }, 204 });