diff --git a/LabelContainer.tsx b/LabelContainer.tsx index 1e36836..3dc8fce 100644 --- a/LabelContainer.tsx +++ b/LabelContainer.tsx @@ -1,24 +1,24 @@ -import React, { PureComponent } from 'react'; -import { View } from 'react-native'; +import React, {PureComponent, ReactNode} from 'react'; +import {View, ViewProps} from 'react-native'; -class LabelContainer extends PureComponent { +type Props = ViewProps & {renderContent: (value: number) => ReactNode}; +type State = { + value: number; +}; +class LabelContainer extends PureComponent { state = { value: Number.NaN, }; - - setValue = value => { - this.setState({ value }); - } + + setValue = (value: number) => { + this.setState({value}); + }; render() { - const { renderContent, ...restProps } = this.props; - const { value } = this.state; - return ( - - {renderContent(value)} - - ); + const {renderContent, ...restProps} = this.props; + const {value} = this.state; + return {renderContent(value)}; } } diff --git a/helpers.ts b/helpers/helpers.ts similarity index 100% rename from helpers.ts rename to helpers/helpers.ts diff --git a/helpers/index.ts b/helpers/index.ts new file mode 100644 index 0000000..5c0b65b --- /dev/null +++ b/helpers/index.ts @@ -0,0 +1,2 @@ +export * from './helpers'; +export * from './pan-responder-factory'; diff --git a/helpers/pan-responder-factory.ts b/helpers/pan-responder-factory.ts new file mode 100644 index 0000000..e2e368c --- /dev/null +++ b/helpers/pan-responder-factory.ts @@ -0,0 +1,58 @@ +import { useRef } from 'react'; +import { + GestureResponderEvent, + PanResponder, + PanResponderCallbacks, + PanResponderGestureState, +} from 'react-native'; + +const trueFunc = () => true; +const falseFunc = () => false; + +type Props = { + onPanResponderMove: NonNullable; + onPanResponderGrant: NonNullable< + PanResponderCallbacks['onPanResponderGrant'] + >; + onPanResponderRelease: NonNullable< + PanResponderCallbacks['onPanResponderRelease'] + >; +}; + +export class PanResponderFactory { + // external + private onPanResponderMove!: Props['onPanResponderMove']; + private onPanResponderGrant!: Props['onPanResponderGrant']; + private onPanResponderRelease!: Props['onPanResponderRelease']; + + constructor(props: Props) { + this.updateValues(props); + } + + public updateValues(props: Props) { + this.onPanResponderMove = props.onPanResponderMove; + this.onPanResponderGrant = props.onPanResponderGrant; + this.onPanResponderRelease = props.onPanResponderRelease; + } + + public usePanResponder = () => { + const panResponder = useRef( + PanResponder.create({ + onMoveShouldSetPanResponderCapture: falseFunc, + onPanResponderTerminationRequest: falseFunc, + onStartShouldSetPanResponderCapture: trueFunc, + onPanResponderTerminate: trueFunc, + onShouldBlockNativeResponder: trueFunc, + onMoveShouldSetPanResponder: ( + evt: GestureResponderEvent, + gestureState: PanResponderGestureState, + ) => Math.abs(gestureState.dx) > 2 * Math.abs(gestureState.dy), + onPanResponderGrant: (...args) => this.onPanResponderGrant(...args), + onPanResponderMove: (...args) => this.onPanResponderMove(...args), + onPanResponderRelease: (...args) => this.onPanResponderRelease(...args), + }), + ); + + return panResponder.current; + }; +} diff --git a/hooks.tsx b/hooks.tsx deleted file mode 100644 index 85214b2..0000000 --- a/hooks.tsx +++ /dev/null @@ -1,207 +0,0 @@ -import React, { - useCallback, - useState, - useRef, - useMemo, - MutableRefObject, - ReactNode, -} from 'react'; -import {Animated, I18nManager} from 'react-native'; -import {clamp} from './helpers'; -import styles from './styles'; -import FollowerContainer from './LabelContainer'; - -/** - * low and high state variables are fallbacks for props (props are not required). - * This hook ensures that current low and high are not out of [min, max] range. - * It returns an object which contains: - * - ref containing correct low, high, min, max and step to work with. - * - setLow and setHigh setters - * @param lowProp - * @param highProp - * @param min - * @param max - * @param step - * @returns {{inPropsRef: React.MutableRefObject<{high: (*|number), low: (*|number)}>, setLow: (function(number): undefined), setHigh: (function(number): undefined)}} - */ -export const useLowHigh = ( - lowProp: number | undefined, - highProp: number | undefined, - min: number, - max: number, - step: number, -) => { - const validLowProp = lowProp === undefined ? min : clamp(lowProp, min, max); - const validHighProp = - highProp === undefined ? max : clamp(highProp, min, max); - const inPropsRef = useRef({ - low: validLowProp, - high: validHighProp, - step, - // These 2 fields will be overwritten below. - min: validLowProp, - max: validHighProp, - }); - const {low: lowState, high: highState} = inPropsRef.current; - const inPropsRefPrev = {lowPrev: lowState, highPrev: highState}; - - // Props have higher priority. - // If no props are passed, use internal state variables. - const low = clamp(lowProp === undefined ? lowState : lowProp, min, max); - const high = clamp(highProp === undefined ? highState : highProp, min, max); - - // Always update values of refs so pan responder will have updated values - Object.assign(inPropsRef.current, {low, high, min, max}); - - const setLow = (value: number) => (inPropsRef.current.low = value); - const setHigh = (value: number) => (inPropsRef.current.high = value); - return {inPropsRef, inPropsRefPrev, setLow, setHigh}; -}; - -/** - * Sets the current value of widthRef and calls the callback with new width parameter. - * @param widthRef - * @param callback - * @returns {function({nativeEvent: *}): void} - */ -export const useWidthLayout = ( - widthRef: MutableRefObject, - callback?: (width: number) => void, -) => { - return useCallback( - ({nativeEvent}) => { - const { - layout: {width}, - } = nativeEvent; - const {current: w} = widthRef; - if (w !== width) { - widthRef.current = width; - if (callback) { - callback(width); - } - } - }, - [callback, widthRef], - ); -}; - -/** - * This hook creates a component which follows the thumb. - * Content renderer is passed to FollowerContainer which re-renders only it's content with setValue method. - * This allows to re-render only follower, instead of the whole slider with all children (thumb, rail, etc.). - * Returned update function should be called every time follower should be updated. - * @param containerWidthRef - * @param gestureStateRef - * @param renderContent - * @param isPressed - * @param allowOverflow - * @returns {[JSX.Element, function(*, *=): void]|*[]} - */ -export const useThumbFollower = ( - containerWidthRef: MutableRefObject, - gestureStateRef: MutableRefObject<{lastValue: number; lastPosition: number}>, - renderContent: undefined | ((value: number) => ReactNode), - isPressed: boolean, - allowOverflow: boolean, -) => { - const xRef = useRef(new Animated.Value(0)); - const widthRef = useRef(0); - const contentContainerRef = useRef(null); - - const {current: x} = xRef; - - const update = useCallback( - (thumbPositionInView, value) => { - const {current: width} = widthRef; - const {current: containerWidth} = containerWidthRef; - const position = thumbPositionInView - width / 2; - xRef.current.setValue( - allowOverflow ? position : clamp(position, 0, containerWidth - width), - ); - contentContainerRef.current?.setValue(value); - }, - [widthRef, containerWidthRef, allowOverflow], - ); - - const handleLayout = useWidthLayout(widthRef, () => { - update( - gestureStateRef.current.lastPosition, - gestureStateRef.current.lastValue, - ); - }); - - if (!renderContent) { - return []; - } - - const transform = {transform: [{translateX: x}]}; - const follower = ( - - - - ); - return [follower, update]; -}; - -interface InProps { - low: number; - high: number; - min: number; - max: number; - step: number; -} - -export const useSelectedRail = ( - inPropsRef: MutableRefObject, - containerWidthRef: MutableRefObject, - thumbWidth: number, - disableRange: boolean, -) => { - const {current: left} = useRef(new Animated.Value(0)); - const {current: right} = useRef(new Animated.Value(0)); - const update = useCallback(() => { - const {low, high, min, max} = inPropsRef.current; - const {current: containerWidth} = containerWidthRef; - const fullScale = (max - min) / (containerWidth - thumbWidth); - const leftValue = (low - min) / fullScale; - const rightValue = (max - high) / fullScale; - left.setValue(disableRange ? 0 : leftValue); - right.setValue( - disableRange ? containerWidth - thumbWidth - leftValue : rightValue, - ); - }, [inPropsRef, containerWidthRef, disableRange, thumbWidth, left, right]); - const styles = useMemo( - () => ({ - position: 'absolute', - left: I18nManager.isRTL ? right : left, - right: I18nManager.isRTL ? left : right, - }), - [left, right], - ); - return [styles, update]; -}; - -/** - * @param floating - * @returns {{onLayout: ((function({nativeEvent: *}): void)|undefined), style: [*, {top}]}} - */ -export const useLabelContainerProps = (floating: boolean) => { - const [labelContainerHeight, setLabelContainerHeight] = useState(0); - const onLayout = useCallback(({nativeEvent}) => { - const { - layout: {height}, - } = nativeEvent; - setLabelContainerHeight(height); - }, []); - - const top = floating ? -labelContainerHeight : 0; - const style = [ - floating ? styles.labelFloatingContainer : styles.labelFixedContainer, - {top}, - ]; - return {style, onLayout: onLayout}; -}; diff --git a/hooks/index.ts b/hooks/index.ts new file mode 100644 index 0000000..a6c6fd3 --- /dev/null +++ b/hooks/index.ts @@ -0,0 +1,5 @@ +export * from "./use-label-container-props"; +export * from "./use-low-high"; +export * from "./use-selected-rail"; +export * from "./use-thumb-follower"; +export * from "./use-width-layout"; diff --git a/hooks/types.ts b/hooks/types.ts new file mode 100644 index 0000000..949b656 --- /dev/null +++ b/hooks/types.ts @@ -0,0 +1,7 @@ +export interface InProps { + low: number; + high: number; + min: number; + max: number; + step: number; +} diff --git a/hooks/use-label-container-props.ts b/hooks/use-label-container-props.ts new file mode 100644 index 0000000..33ed469 --- /dev/null +++ b/hooks/use-label-container-props.ts @@ -0,0 +1,25 @@ +import { useCallback, useState } from 'react'; +import { LayoutChangeEvent } from 'react-native'; + +import styles from '../styles'; + +/** + * @param floating + * @returns {{onLayout: ((function({nativeEvent: *}): void)|undefined), style: [*, {top}]}} + */ +export const useLabelContainerProps = (floating: boolean) => { + const [labelContainerHeight, setLabelContainerHeight] = useState(0); + const onLayout = useCallback(({ nativeEvent }: LayoutChangeEvent) => { + const { + layout: { height }, + } = nativeEvent; + setLabelContainerHeight(height); + }, []); + + const top = floating ? -labelContainerHeight : 0; + const style = [ + floating ? styles.labelFloatingContainer : styles.labelFixedContainer, + { top }, + ]; + return { style, onLayout: onLayout }; +}; diff --git a/hooks/use-low-high.ts b/hooks/use-low-high.ts new file mode 100644 index 0000000..8749461 --- /dev/null +++ b/hooks/use-low-high.ts @@ -0,0 +1,50 @@ +import { useRef } from 'react'; + +import { clamp } from '../helpers'; + +/** + * low and high state variables are fallbacks for props (props are not required). + * This hook ensures that current low and high are not out of [min, max] range. + * It returns an object which contains: + * - ref containing correct low, high, min, max and step to work with. + * - setLow and setHigh setters + * @param lowProp + * @param highProp + * @param min + * @param max + * @param step + * @returns {{inPropsRef: React.MutableRefObject<{high: (*|number), low: (*|number)}>, setLow: (function(number): undefined), setHigh: (function(number): undefined)}} + */ +export const useLowHigh = ( + lowProp: number | undefined, + highProp: number | undefined, + min: number, + max: number, + step: number, +) => { + const validLowProp = lowProp === undefined ? min : clamp(lowProp, min, max); + const validHighProp = + highProp === undefined ? max : clamp(highProp, min, max); + const inPropsRef = useRef({ + low: validLowProp, + high: validHighProp, + step, + // These 2 fields will be overwritten below. + min: validLowProp, + max: validHighProp, + }); + const { low: lowState, high: highState } = inPropsRef.current; + const inPropsRefPrev = { lowPrev: lowState, highPrev: highState }; + + // Props have higher priority. + // If no props are passed, use internal state variables. + const low = clamp(lowProp === undefined ? lowState : lowProp, min, max); + const high = clamp(highProp === undefined ? highState : highProp, min, max); + + // Always update values of refs so pan responder will have updated values + Object.assign(inPropsRef.current, { low, high, min, max }); + + const setLow = (value: number) => (inPropsRef.current.low = value); + const setHigh = (value: number) => (inPropsRef.current.high = value); + return { inPropsRef, inPropsRefPrev, setLow, setHigh }; +}; diff --git a/hooks/use-selected-rail.ts b/hooks/use-selected-rail.ts new file mode 100644 index 0000000..2329989 --- /dev/null +++ b/hooks/use-selected-rail.ts @@ -0,0 +1,42 @@ +import { MutableRefObject, useCallback, useMemo, useRef } from 'react'; +import { Animated, I18nManager, ViewStyle } from 'react-native'; + +import { InProps } from './types'; + +type UseSelectedRail = ( + inPropsRef: MutableRefObject, + containerWidthRef: MutableRefObject, + thumbWidth: number, + disableRange: boolean, +) => [ViewStyle, () => void] | []; + +export const useSelectedRail: UseSelectedRail = ( + inPropsRef, + containerWidthRef, + thumbWidth, + disableRange, +) => { + const { current: left } = useRef(new Animated.Value(0)); + const { current: right } = useRef(new Animated.Value(0)); + const update = useCallback(() => { + const { low, high, min, max } = inPropsRef.current; + const { current: containerWidth } = containerWidthRef; + const fullScale = (max - min) / (containerWidth - thumbWidth); + const leftValue = (low - min) / fullScale; + const rightValue = (max - high) / fullScale; + left.setValue(disableRange ? 0 : leftValue); + right.setValue( + disableRange ? containerWidth - thumbWidth - leftValue : rightValue, + ); + }, [inPropsRef, containerWidthRef, disableRange, thumbWidth, left, right]); + const styles = useMemo( + () => + ({ + position: 'absolute', + left: I18nManager.isRTL ? right : left, + right: I18nManager.isRTL ? left : right, + } as any), + [left, right], + ); + return [styles, update]; +}; diff --git a/hooks/use-thumb-follower.tsx b/hooks/use-thumb-follower.tsx new file mode 100644 index 0000000..15fab9c --- /dev/null +++ b/hooks/use-thumb-follower.tsx @@ -0,0 +1,83 @@ +import React, { + MutableRefObject, + ReactElement, + ReactNode, + useCallback, + useRef, +} from 'react'; +import {Animated} from 'react-native'; + +import FollowerContainer from '../LabelContainer'; +import {clamp} from '../helpers'; + +import {useWidthLayout} from './use-width-layout'; + +type UseThumbFollower = ( + containerWidthRef: MutableRefObject, + gestureStateRef: MutableRefObject<{lastValue: number; lastPosition: number}>, + renderContent: undefined | ((value: number) => ReactNode), + isPressed: boolean, + allowOverflow: boolean, +) => [ReactElement, (thumbPositionInView: number, value: number) => void] | []; + +/** + * This hook creates a component which follows the thumb. + * Content renderer is passed to FollowerContainer which re-renders only it's content with setValue method. + * This allows to re-render only follower, instead of the whole slider with all children (thumb, rail, etc.). + * Returned update function should be called every time follower should be updated. + * @param containerWidthRef + * @param gestureStateRef + * @param renderContent + * @param isPressed + * @param allowOverflow + * @returns {[JSX.Element, function(*, *=): void]|*[]} + */ +export const useThumbFollower: UseThumbFollower = ( + containerWidthRef, + gestureStateRef, + renderContent, + isPressed, + allowOverflow, +) => { + const xRef = useRef(new Animated.Value(0)); + const widthRef = useRef(0); + const contentContainerRef = useRef(null); + + const {current: x} = xRef; + + const update = useCallback( + (thumbPositionInView: number = 0, value: number = 0) => { + const {current: width} = widthRef; + const {current: containerWidth} = containerWidthRef; + const position = thumbPositionInView - width / 2; + xRef.current.setValue( + allowOverflow ? position : clamp(position, 0, containerWidth - width), + ); + contentContainerRef.current?.setValue(value); + }, + [widthRef, containerWidthRef, allowOverflow], + ); + + const handleLayout = useWidthLayout(widthRef, () => { + update( + gestureStateRef.current.lastPosition, + gestureStateRef.current.lastValue, + ); + }); + + if (!renderContent) { + return []; + } + + const transform = {transform: [{translateX: x}]}; + const follower = ( + + + + ); + return [follower, update]; +}; diff --git a/hooks/use-width-layout.ts b/hooks/use-width-layout.ts new file mode 100644 index 0000000..1f803c5 --- /dev/null +++ b/hooks/use-width-layout.ts @@ -0,0 +1,29 @@ +import { MutableRefObject, useCallback } from 'react'; +import { LayoutChangeEvent } from 'react-native'; + +/** + * Sets the current value of widthRef and calls the callback with new width parameter. + * @param widthRef + * @param callback + * @returns {function({nativeEvent: *}): void} + */ +export const useWidthLayout = ( + widthRef: MutableRefObject, + callback?: (width: number) => void, +) => { + return useCallback( + ({ nativeEvent }: LayoutChangeEvent) => { + const { + layout: { width }, + } = nativeEvent; + const { current: w } = widthRef; + if (w !== width) { + widthRef.current = width; + if (callback) { + callback(width); + } + } + }, + [callback, widthRef], + ); +}; diff --git a/index.tsx b/index.tsx index 37163ae..2762f52 100644 --- a/index.tsx +++ b/index.tsx @@ -10,7 +10,7 @@ import React, { import { Animated, GestureResponderEvent, - PanResponder, + LayoutChangeEvent, PanResponderGestureState, View, ViewProps, @@ -24,10 +24,12 @@ import { useLabelContainerProps, useSelectedRail, } from './hooks'; -import {clamp, getValueForPosition, isLowCloser} from './helpers'; - -const trueFunc = () => true; -const falseFunc = () => false; +import { + PanResponderFactory, + clamp, + getValueForPosition, + isLowCloser, +} from './helpers'; export interface SliderProps extends ViewProps { min: number; @@ -80,11 +82,15 @@ const Slider: React.FC = ({ ); const lowThumbXRef = useRef(new Animated.Value(0)); const highThumbXRef = useRef(new Animated.Value(0)); - const pointerX = useRef(new Animated.Value(0)).current; + const pointerX = useRef(new Animated.Value(0)); const {current: lowThumbX} = lowThumbXRef; const {current: highThumbX} = highThumbXRef; - const gestureStateRef = useRef({isLow: true, lastValue: 0, lastPosition: 0}); + const gestureStateRef = useRef({ + isLow: true, + lastValue: 0, + lastPosition: 0, + }); const [isPressed, setPressed] = useState(false); const containerWidthRef = useRef(0); @@ -113,7 +119,7 @@ const Slider: React.FC = ({ const lowPosition = ((low - min) / (max - min)) * (containerWidth - thumbWidth); lowThumbX.setValue(lowPosition); - updateSelectedRail(); + updateSelectedRail && updateSelectedRail(); onValueChanged?.(low, high, false); }, [ disableRange, @@ -133,7 +139,7 @@ const Slider: React.FC = ({ ) { updateThumbs(); } - }, [highProp, inPropsRefPrev.lowPrev, inPropsRefPrev.highPrev, lowProp]); + }, [highProp, inPropsRefPrev, updateThumbs, lowProp]); useEffect(() => { updateThumbs(); @@ -141,7 +147,7 @@ const Slider: React.FC = ({ const handleContainerLayout = useWidthLayout(containerWidthRef, updateThumbs); const handleThumbLayout = useCallback( - ({nativeEvent}) => { + ({nativeEvent}: LayoutChangeEvent) => { const { layout: {width}, } = nativeEvent; @@ -185,116 +191,131 @@ const Slider: React.FC = ({ const labelContainerProps = useLabelContainerProps(floatingLabel); - const {panHandlers} = useMemo( - () => - PanResponder.create({ - onStartShouldSetPanResponderCapture: falseFunc, - onMoveShouldSetPanResponderCapture: falseFunc, - onPanResponderTerminationRequest: falseFunc, - onPanResponderTerminate: trueFunc, - onShouldBlockNativeResponder: trueFunc, - - onMoveShouldSetPanResponder: ( - evt: GestureResponderEvent, - gestureState: PanResponderGestureState, - ) => Math.abs(gestureState.dx) > 2 * Math.abs(gestureState.dy), - - onPanResponderGrant: ({nativeEvent}, gestureState) => { - if (disabled) { - return; - } - const {numberActiveTouches} = gestureState; - if (numberActiveTouches > 1) { - return; - } - setPressed(true); - const {current: lowThumbX} = lowThumbXRef; - const {current: highThumbX} = highThumbXRef; - const {locationX: downX, pageX} = nativeEvent; - const containerX = pageX - downX; - - const {low, high, min, max} = inPropsRef.current; - onSliderTouchStart?.(low, high); - const containerWidth = containerWidthRef.current; - - const lowPosition = - thumbWidth / 2 + - ((low - min) / (max - min)) * (containerWidth - thumbWidth); - const highPosition = - thumbWidth / 2 + - ((high - min) / (max - min)) * (containerWidth - thumbWidth); + const onPanResponderGrant = useCallback( + ( + {nativeEvent}: GestureResponderEvent, + gestureState: PanResponderGestureState, + ) => { + if (disabled) { + return; + } + const {numberActiveTouches} = gestureState; + if (numberActiveTouches > 1) { + return; + } + setPressed(true); + const {current: lowThumbX} = lowThumbXRef; + const {current: highThumbX} = highThumbXRef; + const {locationX: downX, pageX} = nativeEvent; + const containerX = pageX - downX; - const isLow = - disableRange || isLowCloser(downX, lowPosition, highPosition); - gestureStateRef.current.isLow = isLow; + const {low, high, min, max} = inPropsRef.current; + onSliderTouchStart?.(low, high); + const containerWidth = containerWidthRef.current; - const handlePositionChange = (positionInView: number) => { - const {low, high, min, max, step} = inPropsRef.current; - const minValue = isLow ? min : low + minRange; - const maxValue = isLow ? high - minRange : max; - const value = clamp( - getValueForPosition( - positionInView, - containerWidth, - thumbWidth, - min, - max, - step, - ), - minValue, - maxValue, - ); - if (gestureStateRef.current.lastValue === value) { - return; - } - const availableSpace = containerWidth - thumbWidth; - const absolutePosition = - ((value - min) / (max - min)) * availableSpace; - gestureStateRef.current.lastValue = value; - gestureStateRef.current.lastPosition = - absolutePosition + thumbWidth / 2; - (isLow ? lowThumbX : highThumbX).setValue(absolutePosition); - onValueChanged?.(isLow ? value : low, isLow ? high : value, true); - (isLow ? setLow : setHigh)(value); - labelUpdate && - labelUpdate(gestureStateRef.current.lastPosition, value); - notchUpdate && - notchUpdate(gestureStateRef.current.lastPosition, value); - updateSelectedRail(); - }; - handlePositionChange(downX); - pointerX.removeAllListeners(); - pointerX.addListener(({value: pointerPosition}) => { - const positionInView = pointerPosition - containerX; - handlePositionChange(positionInView); - }); - }, + const lowPosition = + thumbWidth / 2 + + ((low - min) / (max - min)) * (containerWidth - thumbWidth); + const highPosition = + thumbWidth / 2 + + ((high - min) / (max - min)) * (containerWidth - thumbWidth); - onPanResponderMove: disabled - ? undefined - : Animated.event([null, {moveX: pointerX}], {useNativeDriver: false}), + const isLow = + disableRange || isLowCloser(downX, lowPosition, highPosition); + gestureStateRef.current.isLow = isLow; - onPanResponderRelease: () => { - setPressed(false); - const {low, high} = inPropsRef.current; - onSliderTouchEnd?.(low, high); - }, - }), + const handlePositionChange = (positionInView: number) => { + const {low, high, min, max, step} = inPropsRef.current; + const minValue = isLow ? min : low + minRange; + const maxValue = isLow ? high - minRange : max; + const value = clamp( + getValueForPosition( + positionInView, + containerWidth, + thumbWidth, + min, + max, + step, + ), + minValue, + maxValue, + ); + if (gestureStateRef.current.lastValue === value) { + return; + } + const availableSpace = containerWidth - thumbWidth; + const absolutePosition = ((value - min) / (max - min)) * availableSpace; + gestureStateRef.current.lastValue = value; + gestureStateRef.current.lastPosition = + absolutePosition + thumbWidth / 2; + (isLow ? lowThumbX : highThumbX).setValue(absolutePosition); + onValueChanged?.(isLow ? value : low, isLow ? high : value, true); + (isLow ? setLow : setHigh)(value); + labelUpdate && labelUpdate(gestureStateRef.current.lastPosition, value); + notchUpdate && notchUpdate(gestureStateRef.current.lastPosition, value); + updateSelectedRail && updateSelectedRail(); + }; + handlePositionChange(downX); + pointerX.current.removeAllListeners(); + pointerX.current.addListener(({value: pointerPosition}) => { + const positionInView = pointerPosition - containerX; + handlePositionChange(positionInView); + }); + }, [ - pointerX, - inPropsRef, - thumbWidth, disableRange, disabled, - onValueChanged, - setLow, - setHigh, + inPropsRef, labelUpdate, + minRange, notchUpdate, + onSliderTouchStart, + onValueChanged, + setHigh, + setLow, + thumbWidth, updateSelectedRail, ], ); + const onPanResponderRelease = useCallback(() => { + setPressed(false); + const {low, high} = inPropsRef.current; + onSliderTouchEnd?.(low, high); + }, [onSliderTouchEnd, inPropsRef]); + + const onPanResponderMove: ( + e: GestureResponderEvent, + gestureState: PanResponderGestureState, + ) => void = useCallback( + (...args) => { + if (!disabled) { + Animated.event([null, {moveX: pointerX.current}], { + useNativeDriver: false, + })(...args); + } + }, + [disabled, pointerX], + ); + + const panResponderFactory = useRef( + new PanResponderFactory({ + onPanResponderGrant, + onPanResponderRelease, + onPanResponderMove, + }), + ); + + useEffect(() => { + panResponderFactory.current.updateValues({ + onPanResponderGrant, + onPanResponderRelease, + onPanResponderMove, + }); + }, [onPanResponderGrant, onPanResponderRelease, onPanResponderMove]); + + const {panHandlers} = panResponderFactory.current.usePanResponder(); + return (