diff --git a/App.tsx b/App.tsx index 8f6cf70..36249d2 100644 --- a/App.tsx +++ b/App.tsx @@ -1,6 +1,6 @@ /* eslint-disable prettier/prettier */ import { CircleUser, CreditCard, Settings } from 'lucide-react-native'; -import { useState } from 'react'; +import React, { useState } from 'react'; import { Alert, ScrollView, Text, TouchableOpacity, View } from 'react-native'; import { Avatar, AvatarFallback, AvatarImage } from './components/Avatar'; @@ -32,6 +32,7 @@ import { RadioGroupLabel, } from './components/RadioGroup'; import { Skeleton } from './components/Skeleton'; +import { Slider } from './components/Slider'; import { Switch } from './components/Switch'; import { Tabs, TabsContent, TabsList, TabsTrigger } from './components/Tabs'; import { ToastProvider, ToastVariant, useToast } from './components/Toast'; @@ -41,6 +42,8 @@ export default function App() { const [inputText, onChangeText] = useState(''); const [isEnabled, setIsEnabled] = useState(false); + const [sliderValue, setSliderValeu] = useState(50); + return ( @@ -255,12 +258,24 @@ export default function App() { - + Progress + + Slider + + setSliderValeu(v)} + // thumbVisible={false} + /> + + diff --git a/components/Slider.tsx b/components/Slider.tsx new file mode 100644 index 0000000..d4c8a90 --- /dev/null +++ b/components/Slider.tsx @@ -0,0 +1,130 @@ +import debounce from 'lodash.debounce'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { View } from 'react-native'; +import { + Gesture, + GestureDetector, + GestureHandlerRootView, +} from 'react-native-gesture-handler'; +import Animated, { + useAnimatedStyle, + useSharedValue, +} from 'react-native-reanimated'; + +import { cn } from '../lib/utils'; + +interface SliderProps { + minimumValue: number; + maximumValue: number; + value: number; + onValueChange?: (value: number) => void; + thumbVisible?: boolean; +} + +const clamp = (value: number, min: number, max: number) => + Math.max(min, Math.min(value, max)); + +function Slider({ + value, + onValueChange, + minimumValue = 0, + maximumValue = 100, + thumbVisible = true, +}: SliderProps) { + const [sliderWidth, setSliderWidth] = useState(0); + + const calcPosition = useCallback( + (v: number) => + (sliderWidth * (v - minimumValue)) / (maximumValue - minimumValue), + [sliderWidth, minimumValue, maximumValue] + ); + + const translationX = useSharedValue(calcPosition(value)); + const prevTranslationX = useSharedValue(translationX.value); + const isDragging = useSharedValue(false); + + const debounceOnValueChange = useCallback( + debounce((value: number) => { + if (onValueChange) onValueChange(value); + }, 100), + [onValueChange] + ); + + useEffect(() => { + translationX.value = calcPosition(value); + prevTranslationX.value = translationX.value; + return () => debounceOnValueChange.cancel(); + }, [value, calcPosition, debounceOnValueChange]); + + const panGesture = useMemo( + () => + Gesture.Pan() + .minDistance(0) + .onStart(() => { + prevTranslationX.value = translationX.value; + isDragging.value = true; + }) + .onUpdate(event => { + const positionValue = prevTranslationX.value + event.translationX; + const clampedPosition = clamp(positionValue, 0, sliderWidth); + translationX.value = clampedPosition; + }) + .onEnd(async () => { + isDragging.value = false; + const calcReturn = + ((maximumValue - minimumValue) * translationX.value) / sliderWidth; + if (onValueChange) await debounceOnValueChange(calcReturn); + }) + .runOnJS(true), + [calcPosition, sliderWidth, value, debounceOnValueChange] + ); + + const animatedStyles = useAnimatedStyle( + () => ({ + transform: [{ translateX: translationX.value }], + }), + [translationX] + ); + const sizeAnimatedStyles = useAnimatedStyle( + () => ({ + width: translationX.value, + }), + [translationX] + ); + + return ( + + setSliderWidth(e.nativeEvent.layout.width)} + className={cn( + 'mx-3 h-2 rounded-xl w-auto bg-foreground/20 justify-center' + )} + > + + + + + + + ); +} + +export { Slider }; diff --git a/package.json b/package.json index 395c942..a92742d 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "nativewind": "^4.0.1", "react": "18.2.0", "react-native": "0.73.6", + "react-native-gesture-handler": "~2.16.1", "react-native-reanimated": "~3.6.2", "react-native-svg": "^14.1.0", "react-native-vector-icons": "^10.0.3", @@ -30,7 +31,9 @@ }, "devDependencies": { "@babel/core": "^7.23.9", + "@types/lodash.debounce": "^4.0.9", "@trivago/prettier-plugin-sort-imports": "^4.3.0", + "@types/lodash.debounce": "^4.0.9", "@types/react": "~18.2.55", "@types/react-native-vector-icons": "^6.4.18", "eslint": "^8.56.0",