Skip to content

Instantly share code, notes, and snippets.

@leonardof02
Created April 21, 2025 22:02
Show Gist options
  • Select an option

  • Save leonardof02/99ac6be58b19b621e8f4dcda5986d11f to your computer and use it in GitHub Desktop.

Select an option

Save leonardof02/99ac6be58b19b621e8f4dcda5986d11f to your computer and use it in GitHub Desktop.
React Native Custom Input Slider
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