import React, { useState, useEffect } from 'react'; import { Animated, StyleSheet, View, LayoutChangeEvent, Text, TextInput, I18nManager, TextStyle, StyleProp } from 'react-native'; import { gestureHandlerRootHOC, GestureHandlerRootView, PanGestureHandler, PanGestureHandlerGestureEvent, State } from 'react-native-gesture-handler'; const osRtl = I18nManager.isRTL; const SMALL_SIZE = 24; const MEDIUM_SIZE = 34; const LARGE_SIZE = 44; const step = 1; const min = 0; export type itemType = { value: number, text: string } interface TextualSliderProps { values: itemType[]; valueOnChange: (value: itemType) => void; styleSize?: 'small' | 'medium' | 'large' | number; knobColor?: string; inRangeBarColor?: string; outOfRangeBarColor?: string; valueLabelsTextColor?: string; valueLabelsBackgroundColor?: string; rangeLabelsStyle?: StyleProp; showRangeLabels?: boolean; showValueLabels?: boolean; initialValue?: number; } const TextualSlider = gestureHandlerRootHOC(({ values, valueOnChange, styleSize = 'medium', knobColor = '#00a2ff', inRangeBarColor = 'rgb(200,200,200)', outOfRangeBarColor = 'rgb(100,100,100)', valueLabelsTextColor = 'white', valueLabelsBackgroundColor = '#3a4766', rangeLabelsStyle, showRangeLabels = true, showValueLabels = true, initialValue }: TextualSliderProps) => { // settings const [stepInPixels, setStepInPixels] = useState(0); const [knobSize, setknobSize] = useState(0); const [max, setMax] = useState(1); // rtl settings const [flexDirection, setFlexDirection] = useState<"row" | "row-reverse" | "column" | "column-reverse" | undefined>('row'); const [svgOffset, setSvgOffset] = useState({ left: (knobSize - 40) / 2 }); const [valueOffset, setValueOffset] = useState(0); const [TextualSliderWidth, setTextualSliderWidth] = useState(0); // animation values const [translateX] = useState(new Animated.Value(0)); const [valueLabelScale] = useState(new Animated.Value(0.01)); const [inRangeScaleX] = useState(new Animated.Value(0.01)); // refs const valueTextRef = React.createRef(); const opacity = React.useRef(new Animated.Value(0)).current; // initalizing settings useEffect(() => { setMax(values.length - 1); setFlexDirection(osRtl ? 'row-reverse' : 'row'); setSvgOffset(osRtl ? { right: (knobSize - 40) / 2 } : { left: (knobSize - 40) / 2 }); }, []); useEffect(() => { if (TextualSliderWidth > 0) { const stepSize = setStepSize(max, min, step); valueTextRef.current?.setNativeProps({ text: values[min].text }); if (typeof initialValue === 'number' && initialValue >= min && initialValue <= max) { const offset = ((initialValue - min) / step) * stepSize - (knobSize / 2); setValueStatic(offset, knobSize, stepSize); setValueText(offset); } Animated.timing(opacity, { toValue: 1, duration: 64, useNativeDriver: true }).start(); } }, [min, max, step, initialValue, TextualSliderWidth]); useEffect(() => { const size = typeof styleSize === 'number' ? styleSize : styleSize === 'small' ? SMALL_SIZE : styleSize === 'medium' ? MEDIUM_SIZE : LARGE_SIZE; setknobSize(size); translateX.setValue(-size / 4); }, [styleSize]); const setValueStatic = (newOffset: number, knobSize: number, stepInPixels: number) => { newOffset = Math.round((newOffset + (knobSize / 2)) / stepInPixels) * stepInPixels - (knobSize / 2); settingValue(newOffset); setValueOffset(newOffset); const index = Math.round(((newOffset + (knobSize / 2)) * (max - min) / TextualSliderWidth) / step) * step + min; valueOnChange(values[index]); } const settingValue = (newOffset: number) => { translateX.setValue(newOffset); inRangeScaleX.setValue((newOffset + (knobSize / 2)) / TextualSliderWidth + 0.01); } const setValueText = (totalOffset: number) => { const numericValue: number = Math.floor(((totalOffset + (knobSize / 2)) * (max - min) / TextualSliderWidth) / step) * step + min; valueTextRef.current?.setNativeProps({ text: values[numericValue].text }); } const setStepSize = (max: number, min: number, step: number) => { const numberOfSteps = ((max - min) / step); const stepSize = TextualSliderWidth / numberOfSteps; setStepInPixels(stepSize); return stepSize; } // value gesture events ------------------------------------------------------------------------ const onGestureEvent = (event: PanGestureHandlerGestureEvent) => { let totalOffset = event.nativeEvent.translationX + valueOffset; if (totalOffset >= - knobSize / 2 && totalOffset <= TextualSliderWidth - knobSize / 2) { translateX.setValue(totalOffset); if (valueTextRef != null) { const index = Math.round(((totalOffset + (knobSize / 2)) * (max - min) / TextualSliderWidth) / step) * step + min; valueTextRef.current?.setNativeProps({ text: values[index].text }); } inRangeScaleX.setValue((totalOffset + (knobSize / 2)) / TextualSliderWidth + 0.01); } } const onHandlerStateChange = (event: PanGestureHandlerGestureEvent) => { if (event.nativeEvent.state === State.BEGAN) { scaleTo(valueLabelScale, 1); } if (event.nativeEvent.state === State.END) { let newOffset = event.nativeEvent.translationX + valueOffset; newOffset = Math.round((newOffset + (knobSize / 2)) / stepInPixels) * stepInPixels - (knobSize / 2); if (newOffset < -knobSize / 2) { newOffset = -knobSize / 2; } else if (newOffset >= TextualSliderWidth - knobSize / 2) { newOffset = TextualSliderWidth - knobSize / 2; } setValueStatic(newOffset, knobSize, stepInPixels); scaleTo(valueLabelScale, 0.01); } } // ------------------------------------------------------------------------------------------------ // gesture events help functions ------------------------------------------------------------------ const scaleTo = (param: Animated.Value, toValue: number) => Animated.timing(param, { toValue, duration: 150, useNativeDriver: true } ).start(); // ------------------------------------------------------------------------------------------------ // setting bar width ------------------------------------------------------------------------------ const onLayout = (event: LayoutChangeEvent) => { setTextualSliderWidth(event.nativeEvent.layout.width); } // ------------------------------------------------------------------------------------------------ const labelOpacity = valueLabelScale.interpolate({ inputRange: [0.1, 1], outputRange: [0, 1] }) return ( { showValueLabels && } { showRangeLabels && {values.length > 1 ? values[0].text : ''} {values.length > 1 ? values[max].text : ''} } ); }); const styles = StyleSheet.create({ container: { height: 100, width: '100%', overflow: 'visible' }, knob: { position: 'absolute', elevation: 4 }, bar: { position: 'absolute', borderBottomRightRadius: 100, borderTopRightRadius: 100 } }); export default TextualSlider;