Created
April 21, 2025 22:02
-
-
Save leonardof02/99ac6be58b19b621e8f4dcda5986d11f to your computer and use it in GitHub Desktop.
React Native Custom Input Slider
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import React, { useEffect, useState } from "react"; | |
| import Animated, { | |
| useSharedValue, | |
| useAnimatedStyle, | |
| runOnJS, | |
| } from "react-native-reanimated"; | |
| import Ionicons from "@expo/vector-icons/Ionicons"; | |
| import { Gesture, GestureDetector } from "react-native-gesture-handler"; | |
| import { theme } from "../../themes/theme"; | |
| import { Pressable } from "react-native"; | |
| type Props = { | |
| value?: number; | |
| onChange?: (value: number) => void; | |
| onSlidingComplete?: (value: number) => void; | |
| min?: number; | |
| max?: number; | |
| step?: number; | |
| }; | |
| export function Slider({ | |
| value = 0, | |
| onChange = () => {}, | |
| min = 0, | |
| max = 100, | |
| step = 20, | |
| }: Props) { | |
| const thumbWidth = 40; | |
| const throttleDelay = 100; | |
| const sliderWidth = useSharedValue(0); | |
| const currentX = useSharedValue(0); | |
| const initialX = useSharedValue(-thumbWidth / 2); | |
| const lastEmittedTime = useSharedValue(value); | |
| // Estado para guardar el layout del thumb | |
| const [thumbLayout, setThumbLayout] = useState<{ x: number; width: number }>({ | |
| x: 0, | |
| width: thumbWidth, | |
| }); | |
| useEffect(() => { | |
| currentX.value = valueToX(value); | |
| initialX.value = currentX.value; | |
| }, []); | |
| const xToValue = (x: number) => { | |
| "worklet"; | |
| return min + ((x + thumbWidth / 2) / sliderWidth.value) * (max - min); | |
| }; | |
| const valueToX = (val: number) => { | |
| "worklet"; | |
| return ((val - min) / (max - min)) * sliderWidth.value - thumbWidth / 2; | |
| }; | |
| const panGesture = Gesture.Pan() | |
| .onStart((event) => { | |
| initialX.value = currentX.value; | |
| }) | |
| .onUpdate((event) => { | |
| const newX = initialX.value + event.translationX; | |
| const clampedX = Math.min( | |
| Math.max(newX, 0 - thumbWidth / 2), | |
| sliderWidth.value - thumbWidth / 2 | |
| ); | |
| const rawValue = xToValue(clampedX); | |
| const steppedValue = Math.round(rawValue / step) * step; | |
| currentX.value = valueToX(steppedValue); | |
| requestAnimationFrame((now) => { | |
| "worklet"; | |
| if (now - lastEmittedTime.value > throttleDelay) { | |
| lastEmittedTime.value = now; | |
| runOnJS(onChange)(steppedValue); | |
| } | |
| }); | |
| }) | |
| .onEnd((event) => { | |
| const newX = initialX.value + event.translationX; | |
| const clampedX = Math.min( | |
| Math.max(newX, 0 - thumbWidth / 2), | |
| sliderWidth.value - thumbWidth / 2 | |
| ); | |
| const rawValue = xToValue(clampedX); | |
| const steppedValue = Math.round(rawValue / step) * step; | |
| currentX.value = valueToX(steppedValue); | |
| runOnJS(onChange)(steppedValue); | |
| }); | |
| function handlePressIn(event: any) { | |
| const touchX = event.nativeEvent.locationX; | |
| if ( | |
| touchX >= thumbLayout.x && | |
| touchX <= thumbLayout.x + thumbLayout.width | |
| ) { | |
| return; | |
| } | |
| const x = event.nativeEvent.locationX - thumbWidth / 2; | |
| const clampedX = Math.min( | |
| Math.max(x, 0 - thumbWidth / 2), | |
| sliderWidth.value - thumbWidth / 2 | |
| ); | |
| const rawValue = xToValue(clampedX); | |
| const steppedValue = Math.round(rawValue / step) * step; | |
| currentX.value = valueToX(steppedValue); | |
| initialX.value = currentX.value; | |
| onChange(steppedValue); | |
| } | |
| const thumbAnimatedStyle = useAnimatedStyle(() => { | |
| return { | |
| transform: [ | |
| { | |
| translateX: currentX.value, | |
| }, | |
| ], | |
| }; | |
| }, [currentX]); | |
| const trackAnimatedStyle = useAnimatedStyle(() => { | |
| const actualValue = xToValue(currentX.value); | |
| const percentage = ((actualValue - min) / (max - min)) * 100; | |
| return { width: `${percentage}%` }; | |
| }); | |
| return ( | |
| <Pressable | |
| style={{ | |
| width: "100%", | |
| height: 20, | |
| backgroundColor: theme.Colors["coolGray.300"], | |
| flexDirection: "row", | |
| alignItems: "center", | |
| borderRadius: 1000, | |
| position: "relative", | |
| }} | |
| onLayout={(event) => { | |
| sliderWidth.value = event.nativeEvent.layout.width; | |
| currentX.value = sliderWidth.value / 2; | |
| }} | |
| onPressIn={handlePressIn} | |
| > | |
| <Animated.View | |
| style={[ | |
| { | |
| position: "absolute", | |
| top: 0, | |
| left: 0, | |
| height: 20, | |
| backgroundColor: theme.Colors["primary.600"], | |
| borderRadius: 1000, | |
| borderTopRightRadius: 0, | |
| borderBottomRightRadius: 0, | |
| }, | |
| trackAnimatedStyle, | |
| ]} | |
| /> | |
| <GestureDetector gesture={panGesture}> | |
| <Animated.View | |
| style={[ | |
| { | |
| top: 0, | |
| left: 0, | |
| display: "flex", | |
| justifyContent: "center", | |
| alignItems: "center", | |
| width: thumbWidth, | |
| height: thumbWidth, | |
| borderRadius: 1000, | |
| backgroundColor: "white", | |
| borderWidth: 2, | |
| borderColor: theme.Colors["primary.600"], | |
| zIndex: 1, | |
| ...theme.Shadows[5], | |
| }, | |
| thumbAnimatedStyle, | |
| ]} | |
| > | |
| <Ionicons | |
| name="chevron-forward" | |
| size={20} | |
| color={theme.Colors["primary.600"]} | |
| /> | |
| </Animated.View> | |
| </GestureDetector> | |
| </Pressable> | |
| ); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment