import { Animated, LayoutChangeEvent, PanResponder, PanResponderGestureState, StyleSheet, View, } from 'react-native'; import React, {useEffect, useMemo, useRef, useState,} from 'react'; import {DefaultTheme} from 'styled-components'; import linear from '../algorithms/linear'; import DefaultHandler from '../components/DefaultHandler'; import {PERCENT_EMPTY, PERCENT_FULL, VERTICAL} from '../constants/SliderConstants'; import DefaultProgressBar from '../components/DefaultProgressBar'; export type HandlersState = { min?: number; max?: number; values: number[]; }; type RheostatTypes = { disabled?: boolean; max: number; min: number; onPress?: () => void; onChange?: () => void; onSliderDragEnd?: () => void; onSliderDragMove?: () => void; onSliderDragStart?: () => void; onValuesUpdated?: (state:HandlersState) => void, orientation?: 'horizontal'| 'vertical'; algorithm?: { getPosition: (value:number, min:number, max:number)=> number; getValue: (pos:number, min:number, max:number) => number; }; handle?: React.ElementType; pitComponent?: React.ElementType; pitPoints?: number[]; progressBar?: React.ElementType; children?: React.ReactNode; snap?: boolean; snapPoints?: number[]; values: number[]; getNextHandlePosition?: null, theme?: DefaultTheme, svgData?: number[], }; const withRheostat = (ChartCompo: any = null) => React.memo((props: RheostatTypes) => { const { progressBar: ProgressBar = DefaultProgressBar, handle: Handle = DefaultHandler, pitPoints, pitComponent: PitComponent, algorithm = linear, orientation = 'horizontal', max, min, values: inputValues, snap, snapPoints, children = null, svgData, theme, } = props; let previousHandlePos: number[]; // logging start coords at start of each onPanResponderGrant const [handlePos, setHandlePos] = useState(() => inputValues.map((value) => new Animated.Value( algorithm.getPosition(value, min, max), ))); const [values, setValues] = useState(() => inputValues.map((value) => new Animated.Value( value, ))); useEffect(() => { setHandlePos(inputValues.map((value) => new Animated.Value( algorithm.getPosition(value, min, max), ))); setValues(inputValues.map((value) => new Animated.Value(value))); }, [JSON.stringify(inputValues)]); const [containerSize, setContainerSize] = useState({ width: 0, height: 0 }); const [handleDimensions, setHandleDimensions] = useState({ width: 0, height: 0 }); const handlePercentage = orientation === VERTICAL ? ((handleDimensions.width / containerSize.height) * PERCENT_FULL) / 2 : ((handleDimensions.width / containerSize.width) * PERCENT_FULL) / 2; const customPanResponder = (idx: number, start: () => void, move: (idx: number, gestureState: PanResponderGestureState) => void, end: () => void) => PanResponder.create({ // Ask to be the responder: onStartShouldSetPanResponder: () => true, onStartShouldSetPanResponderCapture: () => true, onMoveShouldSetPanResponder: () => false, onMoveShouldSetPanResponderCapture: () => true, onPanResponderGrant: () => start(), onPanResponderMove: (_evt, gestureState) => move(idx, gestureState), onPanResponderTerminationRequest: () => false, onPanResponderRelease: () => end(), onPanResponderTerminate: () => end(), }); const getPublicState = () => ({ max, min, values: values.map((value) => (value as any).__getValue()), }); const getClosestSnapPoint = (value: number, idx: number) => { if (!snapPoints?.length) return value; let validatedSnapPoints = snapPoints; const offsetSnap = algorithm.getValue(handlePercentage, min, max); if (handlePos[idx + 1] !== undefined) { const maxSnap = (values[idx + 1] as any).__getValue(); validatedSnapPoints = snapPoints.filter((snapP) => snapP < maxSnap - offsetSnap); } else if (handlePos[idx - 1] !== undefined) { const minSnap = (values[idx - 1] as any).__getValue(); validatedSnapPoints = snapPoints.filter((snapP) => snapP > minSnap + offsetSnap); } return validatedSnapPoints.reduce((snapTo, snapP) => ( Math.abs(snapTo - value) < Math.abs(snapP - value) ? snapTo : snapP )); }; const getSnapPosition = (positionPercent: number, idx: number) => { if (!snap) return positionPercent; const value = algorithm.getValue(positionPercent, min, max); const snapValue = getClosestSnapPoint(value, idx); return algorithm.getPosition(snapValue, min, max); }; const startSlide = () => { const { onSliderDragStart, } = props; onSliderDragStart && onSliderDragStart(); //eslint-disable-line previousHandlePos = handlePos.map((value) => (value as any).__getValue()); }; const validatePosition = (idx: number, proposedPosition: number) => { return Math.max( Math.min( proposedPosition, handlePos[idx + 1] !== undefined ? (handlePos[idx + 1] as any).__getValue() - handlePercentage : PERCENT_FULL, // 100% is the highest value ), handlePos[idx - 1] !== undefined ? (handlePos[idx - 1] as any).__getValue() + handlePercentage : PERCENT_EMPTY, // 0% is the lowest value ); }; const getNextState = (idx: number, proposedPosition: number) => { const actualPosition = validatePosition(idx, proposedPosition); values.forEach((value, index) => ( index === idx && value.setValue(algorithm.getValue(actualPosition, min, max)) )); handlePos.forEach((value, index) => ( index === idx && value.setValue(actualPosition) )); return { handlePos, // values: nextHandlePos.map(pos => algorithm.getValue(pos, min, max)), values, }; }; const moveSlide = (idx: number, gestureState: PanResponderGestureState) => { const { onValuesUpdated, } = props; const { width, } = containerSize; const proposedPosition = (gestureState.dx / width) * PERCENT_FULL + previousHandlePos[idx]; const snapPosition = getSnapPosition(proposedPosition, idx); getNextState(idx, snapPosition); if (onValuesUpdated) onValuesUpdated(getPublicState()); }; const endSlide = () => { const { onSliderDragEnd } = props; if (onSliderDragEnd) onSliderDragEnd(); }; const panResponders = useMemo(() => handlePos.map( (_val, idx) => customPanResponder(idx, startSlide, moveSlide, endSlide), ), [containerSize, handleDimensions]); const pitStyleCache = useRef<{ [key: number]: any; }>({}); const getHandleDimensions = (event: LayoutChangeEvent) => { const { width, height, } = event.nativeEvent.layout; setHandleDimensions({ width, height }); }; const getRheostatDimensions = (event: LayoutChangeEvent) => { const { width, height, } = event.nativeEvent.layout; setContainerSize({ width, height }); }; const getProgressStyle = (idx: number) => { const { width } = containerSize; const pos = handlePos[idx].interpolate({ inputRange: [0, 100], outputRange: [0, width], }); if (idx === 0) { return orientation === 'vertical' ? { height: pos, top: 0 } : { left: 0, width: pos }; } const prevValue = handlePos[idx - 1].interpolate({ inputRange: [0, 100], outputRange: [0, -width], }); const prevPos = handlePos[idx - 1].interpolate({ inputRange: [0, 100], outputRange: [0, width], }); const diffValue = Animated.add(pos, prevValue); return orientation === 'vertical' ? { transform: [{ translateY: prevPos }], height: diffValue } : { transform: [{ translateX: prevPos }], width: diffValue }; }; return ( {ChartCompo && ( )} {handlePos.map((value, idx) => { const pos = value.interpolate({ inputRange: [0, 100], outputRange: [0, containerSize.width], }); const handleStyle = orientation === 'vertical' ? { transform: [{ translateY: pos }] } : { transform: [{ translateX: pos }] }; return ( ); })} {handlePos.map((_value, idx, arr) => { if (idx === 0 && arr.length > 1) { return null; } return ( ); })} {PitComponent && pitPoints?.map((n) => { let pitStyle = pitStyleCache.current[n]; if (!pitStyle) { const pos = algorithm.getPosition(n, min, max); pitStyle = orientation === 'vertical' ? { top: `${pos}%`, position: 'absolute' } : { left: `${pos}%`, position: 'absolute' }; pitStyleCache.current[n] = pitStyle; } return ( {n} ); })} {children} ); }); const Style = StyleSheet.create({ rheostatHorizontal: { position: 'relative', }, handle: { backgroundColor: 'white', width: '100%', height: '100%', }, handleContainer: { zIndex: 3, position: 'absolute', width: 30, height: 30, marginLeft: -15, bottom: -15, }, container: { justifyContent: 'center', }, rheostatBackground: { // backgroundColor: '#fcfcfc', borderColor: '#d8d8d8', borderBottomWidth: 1, borderTopWidth: 1, position: 'relative', }, rheostatHorizontalBackground: { height: 1, width: '100%', }, rheostatProgress: { position: 'absolute', }, rheostatHorizontalProgress: { height: 13, top: 2, }, fullTrack: { flexDirection: 'row', }, track: { justifyContent: 'center', }, touch: { justifyContent: 'center', alignItems: 'center', backgroundColor: 'transparent', }, }); export default withRheostat;