import { Ionicons } from "@expo/vector-icons"; import type { ComponentProps } from "react"; import { useRef, useState } from "react"; import { Animated, OpaqueColorValue, Pressable, StyleProp, StyleSheet, View, ViewStyle, } from "react-native"; const SIZE_PRESETS = { sm: { width: 64, height: 32 }, md: { width: 80, height: 40 }, lg: { width: 96, height: 48 }, } as const; type SwitchSize = keyof typeof SIZE_PRESETS; const DEFAULT_TOGGLE_DURATION = 400; // Default both backgrounds to a grey tone when not provided const DEFAULT_INACTIVE_BG = "#D3DAD9"; const DEFAULT_ACTIVE_BG = "#D3DAD9"; const PRESSED_SCALE = 0.96; const PRESS_FEEDBACK_DURATION = 120; type IoniconName = ComponentProps["name"]; type RotateSwitchProps = { size?: SwitchSize; leftIcon?: IoniconName; leftIconColor?: string | OpaqueColorValue | undefined; rightIconColor?: string | OpaqueColorValue | undefined; rightIcon?: IoniconName; duration?: number; activeBackgroundColor?: string; inactiveBackgroundColor?: string; inactiveOverlayColor?: string; activeOverlayColor?: string; style?: StyleProp; onChange?: (value: boolean) => void; }; const SliceSwitch = ({ size = "md", leftIcon, rightIcon, duration, activeBackgroundColor = DEFAULT_ACTIVE_BG, inactiveBackgroundColor = DEFAULT_INACTIVE_BG, leftIconColor = "#fff", rightIconColor = "#fff", inactiveOverlayColor = "#000", activeOverlayColor = "#000", style, onChange, }: RotateSwitchProps) => { const { width: containerWidth, height: containerHeight } = SIZE_PRESETS[size] ?? SIZE_PRESETS.md; const animationDuration = Math.max(duration ?? DEFAULT_TOGGLE_DURATION, 0); const [isOn, setIsOn] = useState(false); const [bgOn, setBgOn] = useState(false); const progress = useRef(new Animated.Value(0)).current; const pressScale = useRef(new Animated.Value(1)).current; const overlayTranslateX = useRef(new Animated.Value(0)).current; const listenerIdRef = useRef(null); const handleToggle = () => { const next = !isOn; const targetValue = next ? 1 : 0; const overlayTarget = next ? containerWidth / 2 : 0; progress.stopAnimation(); overlayTranslateX.stopAnimation(); if (animationDuration <= 0) { progress.setValue(targetValue); overlayTranslateX.setValue(overlayTarget); setIsOn(next); setBgOn(next); onChange?.(next); return; } setIsOn(next); Animated.parallel([ Animated.timing(progress, { toValue: targetValue, duration: animationDuration, useNativeDriver: true, }), Animated.timing(overlayTranslateX, { toValue: overlayTarget, duration: animationDuration, useNativeDriver: true, }), ]).start(() => { setBgOn(next); onChange?.(next); }); // Remove any previous listener if (listenerIdRef.current != null) { progress.removeListener(listenerIdRef.current as string); listenerIdRef.current = null; } // Swap image & background exactly at 50% progress let swapped = false; listenerIdRef.current = progress.addListener(({ value }) => { if (swapped) return; if (next && value >= 0.5) { swapped = true; setBgOn(next); if (listenerIdRef.current != null) { progress.removeListener(listenerIdRef.current as string); listenerIdRef.current = null; } } if (!next && value <= 0.5) { swapped = true; setBgOn(next); if (listenerIdRef.current != null) { progress.removeListener(listenerIdRef.current as string); listenerIdRef.current = null; } } }); }; const handlePressIn = () => { pressScale.stopAnimation(); Animated.timing(pressScale, { toValue: PRESSED_SCALE, duration: PRESS_FEEDBACK_DURATION, useNativeDriver: true, }).start(); }; const handlePressOut = () => { pressScale.stopAnimation(); Animated.timing(pressScale, { toValue: 1, duration: PRESS_FEEDBACK_DURATION, useNativeDriver: true, }).start(); }; return ( ); }; const styles = StyleSheet.create({ pressable: { alignSelf: "flex-start", }, shadowWrapper: { justifyContent: "center", position: "relative", shadowColor: "#000", shadowOpacity: 0.15, shadowOffset: { width: 0, height: 4 }, shadowRadius: 6, elevation: 6, backgroundColor: "transparent", }, container: { flex: 1, justifyContent: "center", position: "relative", overflow: "hidden", }, knob: { position: "absolute", top: 0, left: 0, }, }); export default SliceSwitch;