// Import required dependencies and components. import { Icon } from "iconsax-react-native"; import { useRef, useState, useMemo } from "react"; import { Animated, StyleProp, TouchableOpacity, ViewStyle } from "react-native"; // Default values for the component props. const DEFAULT_SCALE_FACTOR: number = 1.25; const DEFAULT_ANIMATION_DURATION: number = 250; const DEFAULT_PADDING: number = 0; // Define the type for the IconButton props. type IconButtonProps = { /** * Indicates if the button starts in an active state. * @default false */ isActive?: boolean; /** * Color of the icon. */ color: string; /** * Optional color of the icon when active. */ colorActive?: string; /** * Callback function triggered when the button is pressed. */ onPress?: () => void | Promise; /** * Size of the icon. * @default 24 */ size?: number; /** * Icon to display. */ icon: Icon; /** * Optional icon to display when active. */ iconActive?: Icon; /** * Variant style of the icon. * @default "Outline" */ variant?: "Outline" | "Bold" | "Bulk" | "Broken" | "TwoTone"; /** * Variant style of the icon when active. * @default "Outline" */ activeVariant?: "Outline" | "Bold" | "Bulk" | "Broken" | "TwoTone"; /** * Custom style for the icon. */ style?: ViewStyle | ViewStyle[]; /** * Custom style for the icon's container. */ containerStyle?: ViewStyle | ViewStyle[]; /** * Scale factor when the icon animates on press. * @default 1.25 */ scaleFactor?: number; /** * Duration of the icon animation in milliseconds. * @default 250 */ animationDuration?: number; /** * Padding around the icon. * @default 0 */ padding?: number; }; /** * `IconButton` Component * * Render an interactive icon that can toggle between active and inactive states. * Supports customization of colors, icons, animation speed, size, and more. * * Features: * - Can render two different icons for active and inactive states. * - Includes an animation that scales the icon when pressed. * - Supports custom styling for both the icon and its container. * - Accepts various icon variants for flexibility in design. * * Usage: * * ```jsx * console.log('Icon pressed!')} * /> * ``` * * To use with active states: * * ```jsx * console.log('Icon toggled!')} * /> * ``` * * Note: If `iconActive` is provided but `colorActive` is not, * the `color` prop will be used for both active and inactive states. */ const IconButton: React.FC = ({ isActive = false, color, colorActive, onPress, size = 24, icon: Icon, iconActive: IconActive, variant = "Outline", activeVariant = "Outline", style, containerStyle, scaleFactor = DEFAULT_SCALE_FACTOR, animationDuration = DEFAULT_ANIMATION_DURATION, padding = DEFAULT_PADDING, }) => { // State to track active status. const [isActiveState, setIsActiveState] = useState(isActive); // Animation values. const scaleValue: Animated.Value = useRef(new Animated.Value(1)).current; const animateValue: Animated.Value = useRef(new Animated.Value(isActive ? 0 : 1)).current; // Check for active icon. const hasActiveIcon: boolean = !!IconActive; // Interpolation for icon colors between active and inactive states. const colorValue: Animated.AnimatedInterpolation = animateValue.interpolate({ inputRange: [0, 1], outputRange: [color, colorActive || color], }); // Combine styles for the icon container. const combinedContainerStyle: StyleProp = useMemo<(ViewStyle | ViewStyle[] | undefined)[]>( () => [ { position: 'relative', // Adjust width and height to account for specified padding. Since the icons // are absolutely positioned inside the container, the container's size needs // to include the padding to ensure the icons are properly centered and spaced. width: size + (padding * 2), height: size + (padding * 2), justifyContent: 'center', alignItems: 'center', }, containerStyle ], [padding, containerStyle] ); /** * Toggles the active state of the icon. * Executes the onPress callback and triggers the animation. */ const toggleState = async () => { // Execute onPress if provided. if (onPress) { await onPress(); } // Toggle isActiveState. setIsActiveState(!isActiveState); // Animation sequences. const animations: Animated.CompositeAnimation[] = [ Animated.sequence([ Animated.timing(scaleValue, { toValue: scaleFactor, duration: animationDuration, useNativeDriver: true, }), Animated.timing(scaleValue, { toValue: 1, duration: animationDuration, useNativeDriver: true, }), ]), ]; if (hasActiveIcon || colorActive) { animations.push( Animated.timing(animateValue, { toValue: isActiveState ? 1 : 0, duration: animationDuration, useNativeDriver: true, }) ); } // Start animations in parallel. Animated.parallel(animations).start(); }; return ( {hasActiveIcon && ( )} ); }; export default IconButton;