import React, { useEffect } from 'react'; import { StyleSheet, View } from 'react-native'; import Animated, { runOnJS, useAnimatedGestureHandler, useAnimatedStyle, useSharedValue, withTiming, } from 'react-native-reanimated'; import { PanGestureHandlerGestureEvent, PanGestureHandler, } from 'react-native-gesture-handler'; import { Binding } from '../../utils/binding'; import { Modifiers } from '../../utils/modifiers'; import { useLifecycle } from '../../hooks/useLifecycle'; import { getPadding } from '../../utils/padding'; import { getBorder } from '../../utils/border'; import { getShadow } from '../../utils/shadow'; import { getCornerRadius } from '../../utils/cornerRadius'; import { CIRCLE_WIDTH, getSliderWidth, position2Value, value2Position, } from './utils'; import { getTransform } from '../../utils/transform'; import { UIColor, getColor } from '../../utils/colors'; import { useColorScheme } from '../../hooks/useColorScheme'; import { useAlert } from '../../hooks/useAlert'; type SliderProps = Modifiers & { tint?: UIColor; trackTint?: UIColor; thumbTint?: UIColor; step?: number; range?: [number, number]; value: Binding<number>; updateOnSlide?: boolean; onChange?: (value?: number) => void; }; type GestureHandlerContext = { offsetX: number; }; export const Slider: React.FC<SliderProps> = ({ tint, trackTint, thumbTint, range = [0, 10], step = 1, value, updateOnSlide = true, frame, backgroundColor, style, padding, cornerRadius, rotationEffect, scaleEffect, shadow, border, opacity, zIndex, alert, onAppear, onDisappear, onChange, }) => { useAlert(alert); useLifecycle(onAppear, onDisappear); const colorScheme = useColorScheme(); const [sliderWidth, sliderHeight] = getSliderWidth(frame); const [from, through] = range; const midPoint = (through + from) / 2; const slope = (midPoint - from) / (sliderWidth / 2); const translateX = useSharedValue( value2Position(value.value, midPoint, slope) ); useEffect(() => { const newPos = value2Position(value.value, midPoint, slope); translateX.value = withTiming(newPos); }, [value.value]); const animatedCursorStyle = useAnimatedStyle(() => { return { transform: [ { translateX: translateX.value, }, ], }; }); const animatedFillStyle = useAnimatedStyle(() => { return { width: translateX.value + sliderWidth / 2, }; }); const gestureHandler = useAnimatedGestureHandler< PanGestureHandlerGestureEvent, GestureHandlerContext >({ onStart: (_, ctx) => { ctx.offsetX = translateX.value; }, onActive: (e, ctx) => { const prevPos = translateX.value; const newPos = e.translationX + ctx.offsetX; if (newPos < sliderWidth / 2 && newPos > -sliderWidth / 2) { translateX.value = newPos; const prevVal = position2Value(prevPos, midPoint, slope, step); const newVal = position2Value(newPos, midPoint, slope, step); if (updateOnSlide && prevVal !== newVal) { runOnJS(value.setValue)(newVal); if (onChange) runOnJS(onChange)(newVal); } } }, onEnd: () => { if (!updateOnSlide) { const newVal = position2Value(translateX.value, midPoint, slope, step); runOnJS(value.setValue)(newVal); if (onChange) runOnJS(onChange)(newVal); } }, }); return ( <View style={[ { opacity, zIndex, backgroundColor: getColor(backgroundColor, colorScheme), ...getCornerRadius(cornerRadius), ...getPadding(padding), ...getBorder(border), ...getShadow(shadow), ...getTransform(scaleEffect, rotationEffect), }, style, ]} > <View style={[ styles.slider, { width: sliderWidth, height: sliderHeight, marginTop: CIRCLE_WIDTH / 2, marginBottom: CIRCLE_WIDTH / 2, backgroundColor: getColor(trackTint, colorScheme, 'systemGray4'), }, ]} > <Animated.View style={[ { height: sliderHeight, borderRadius: 10, backgroundColor: getColor(tint, colorScheme, 'systemBlue'), }, animatedFillStyle, ]} /> <PanGestureHandler onGestureEvent={gestureHandler}> <Animated.View style={[ styles.cursor, { left: sliderWidth / 2 - CIRCLE_WIDTH / 2, top: -CIRCLE_WIDTH / 2, height: CIRCLE_WIDTH, width: CIRCLE_WIDTH, backgroundColor: getColor(thumbTint, colorScheme, '#fff'), }, animatedCursorStyle, ]} /> </PanGestureHandler> </View> </View> ); }; const styles = StyleSheet.create({ slider: { flexDirection: 'row', borderRadius: 10, }, cursor: { backgroundColor: '#fff', position: 'absolute', borderRadius: 100, shadowOffset: { width: 0, height: 2, }, shadowColor: '#000', shadowOpacity: 0.2, shadowRadius: 3, elevation: 5, }, });