You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.09 21:17:37