React Native可复用TabBar组件改造:添加带动画的可配置按钮
Got it, let's fix this TabBar add button component to be reusable and support dynamic icon counts without hardcoding! Here's how we can refactor your code step by step to meet all your requirements:
Step 1: Define a Reusable Props Structure
First, we'll set up a way to pass in button configurations via props—this lets you use any number of icons without touching the component's internal code. We'll define types for clarity (you can skip these if you're using plain JavaScript).
// Type definitions for our button configs (TypeScript only) type SecondaryButtonConfig = { icon: React.ReactNode; // Accept any icon component (Feather, Foundation, etc.) onPress: () => void; }; type TabAddButtonProps = { secondaryButtons: SecondaryButtonConfig[]; onPrimaryPress?: () => void; // Optional callback for the plus button };
Step 2: Refactor Animation for Dynamic Positioning
Instead of hardcoding positions for specific icons, we'll calculate offsets dynamically based on the number of secondary buttons. We'll also add a listener to update positioning if the screen size changes (like on rotation).
Step 3: Render Buttons Dynamically
Use map() to loop through the prop-provided button configs and render each animated button. We'll spread buttons evenly around the primary plus button based on their index in the array.
Full Refactored Component
Here's the complete reusable component that supports 1, 2, 3, or more icons:
import React, { useRef, useEffect } from 'react'; import { View, TouchableOpacity, StyleSheet, useWindowDimensions, Animated } from 'react-native'; import Feather from '@expo/vector-icons/Feather'; import Foundation from '@expo/vector-icons/Foundation'; import FontAwesome5 from '@expo/vector-icons/FontAwesome5'; // Type definitions (skip for JavaScript) type SecondaryButtonConfig = { icon: React.ReactNode; onPress: () => void; }; type TabAddButtonProps = { secondaryButtons: SecondaryButtonConfig[]; onPrimaryPress?: () => void; }; export const TabAddButton = ({ secondaryButtons, onPrimaryPress }: TabAddButtonProps) => { const { width: windowWidth } = useWindowDimensions(); const buttonSize = useRef(new Animated.Value(1)).current; const mode = useRef(new Animated.Value(0)).current; const isExpanded = useRef(false); // Reset animation if window size changes (e.g., screen rotation) useEffect(() => { if (isExpanded.current) { Animated.timing(mode, { toValue: 0, duration: 150, useNativeDriver: false, }).start(() => isExpanded.current = false); } }, [windowWidth]); const handlePrimaryPress = () => { // Trigger user's custom press handler if provided onPrimaryPress?.(); // Run smooth animation sequence Animated.sequence([ Animated.timing(buttonSize, { toValue: 0.95, duration: 50, useNativeDriver: true, }), Animated.timing(buttonSize, { toValue: 1, duration: 50, useNativeDriver: true, }), Animated.timing(mode, { toValue: isExpanded.current ? 0 : 1, duration: 150, useNativeDriver: false, }), ]).start(() => { isExpanded.current = !isExpanded.current; }); }; // Calculate position for each secondary button based on its index const getButtonPosition = (index: number) => { const totalButtons = secondaryButtons.length; const baseX = windowWidth / 2 - 22; const horizontalSpacing = 60; // Adjust to change how far buttons spread apart const verticalOffset = -75; // Adjust to change how far buttons move up // Center buttons horizontally relative to the primary button const offsetX = baseX - ((totalButtons - 1) * horizontalSpacing) / 2 + (index * horizontalSpacing); return { x: mode.interpolate({ inputRange: [0, 1], outputRange: [baseX, offsetX], }), y: mode.interpolate({ inputRange: [0, 1], outputRange: [-20, verticalOffset], }), }; }; return ( <View {...StyleSheet.absoluteFillObject} alignItems="center"> {/* Render dynamic secondary buttons */} {secondaryButtons.map((button, index) => { const { x, y } = getButtonPosition(index); return ( <Animated.View key={index} style={[styles.secondaryView, { left: x, top: y }]} > <TouchableOpacity style={styles.secondaryButton} onPress={button.onPress} > {button.icon} </TouchableOpacity> </Animated.View> ); })} {/* Primary plus button */} <View style={styles.buttonContainer}> <Animated.View style={{ transform: [{ scale: buttonSize }] }}> <TouchableOpacity activeOpacity={1} onPress={handlePrimaryPress} style={styles.primaryButton} > <FontAwesome5 name="plus" size={24} color="#FFF" /> </TouchableOpacity> </Animated.View> </View> </View> ); }; const styles = StyleSheet.create({ secondaryView: { position: 'absolute', }, secondaryButton: { width: 44, height: 44, borderRadius: 22, backgroundColor: '#333', // Customize secondary button color justifyContent: 'center', alignItems: 'center', shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.3, shadowRadius: 4, elevation: 5, }, buttonContainer: { position: 'absolute', bottom: 20, // Adjust to position relative to your TabBar }, primaryButton: { width: 44, height: 44, borderRadius: 22, backgroundColor: '#007AFF', // Customize primary button color justifyContent: 'center', alignItems: 'center', shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.3, shadowRadius: 4, elevation: 5, }, });
How to Use the Component
Now you can use this component anywhere, passing in any number of secondary buttons:
// Example with 3 buttons <TabAddButton secondaryButtons={[ { icon: <Feather name="lock" size={24} color="#FFF" />, onPress: () => console.log('Lock pressed'), }, { icon: <Foundation name="clipboard-notes" size={24} color="#FFF" />, onPress: () => console.log('Notes pressed'), }, { icon: <Feather name="camera" size={24} color="#FFF" />, onPress: () => console.log('Camera pressed'), }, ]} onPrimaryPress={() => console.log('Plus button tapped')} />
Key Improvements
- Fully Reusable: No hardcoded icons—pass any number of buttons via props
- Dynamic Positioning: Buttons auto-space evenly based on the number of items
- Responsive: Adjusts positioning if the screen size changes
- Customizable: Tweak colors, spacing, and animation timings via styles
- Smoother Feedback: Added a subtle bounce effect to the primary button
内容的提问来源于stack exchange,提问作者Oğulcan Karayel

