Open angelzbg opened 1 year ago
I just had the same problem. My solution is to handle "clicks" with a little delay of 100ms. If a movement is in between I ignore the click.
`import React, { PureComponent } from 'react'; import { Animated, Easing, I18nManager, Image, PanResponder, View, } from 'react-native'; // styles import { defaultStyles as styles } from './styles'; const Rect = ({ height, width, x, y, }) => ({ containsPoint: (nativeX, nativeY) => nativeX >= x && nativeY >= y && nativeX <= x + width && nativeY <= y + height, height, trackDistanceToPoint: (nativeX) => { if (nativeX < x) { return x - nativeX; } if (nativeX > x + width) { return nativeX - (x + width); } return 0; }, width, x, y, }); const DEFAULT_ANIMATION_CONFIGS = { spring: { friction: 7, tension: 100, }, timing: { duration: 150, easing: Easing.inOut(Easing.ease), delay: 0, }, }; const normalizeValue = (props, value) => { if (!value || (Array.isArray(value) && value.length === 0)) { return [0]; } const { maximumValue, minimumValue } = props; const getBetweenValue = (inputValue) => Math.max(Math.min(inputValue, maximumValue), minimumValue); if (!Array.isArray(value)) { return [getBetweenValue(value)]; } return value.map(getBetweenValue).sort((a, b) => a - b); }; const updateValues = ({ values, newValues = values, }) => { if (Array.isArray(newValues) && Array.isArray(values) && newValues.length !== values.length) { return updateValues({ values: newValues }); } if (Array.isArray(values) && Array.isArray(newValues)) { return values?.map((value, index) => { let valueToSet = newValues[index]; if (value instanceof Animated.Value) { if (valueToSet instanceof Animated.Value) { valueToSet = valueToSet.getValue(); } value.setValue(valueToSet); return value; } if (valueToSet instanceof Animated.Value) { return valueToSet; } return new Animated.Value(valueToSet); }); } return [new Animated.Value(0)]; }; const indexOfLowest = (values) => { let lowestIndex = 0; values.forEach((value, index, array) => { if (value < array[lowestIndex]) { lowestIndex = index; } }); return lowestIndex; }; export class Slider extends PureComponent { constructor(props) { super(props); this._panResponder = PanResponder.create({ onStartShouldSetPanResponder: this._handleStartShouldSetPanResponder, onMoveShouldSetPanResponder: this._handleMoveShouldSetPanResponder, onPanResponderGrant: this._handlePanResponderGrant, onPanResponderMove: this._handlePanResponderMove, onPanResponderRelease: this._handlePanResponderEnd, onPanResponderTerminationRequest: this._handlePanResponderRequestEnd, onPanResponderTerminate: this._handlePanResponderEnd, }); this.state = { allMeasured: false, containerSize: { width: 0, height: 0, }, thumbSize: { width: 0, height: 0, }, trackMarksValues: updateValues({ values: normalizeValue(this.props, this.props.trackMarks), }), values: updateValues({ values: normalizeValue(this.props, this.props.value instanceof Animated.Value ? this.props.value.getValue() : this.props.value), }), }; } static defaultProps = { animationType: 'timing', debugTouchArea: false, trackMarks: [], maximumTrackTintColor: '#b3b3b3', maximumValue: 1, minimumTrackTintColor: '#3f3f3f', minimumValue: 0, step: 0, thumbTintColor: '#343434', trackClickable: true, value: 0, vertical: false, startFromZero: false, }; static getDerivedStateFromProps(props, state) { if (props.trackMarks && !!state.trackMarksValues && state.trackMarksValues.length > 0) { const newTrackMarkValues = normalizeValue(props, props.trackMarks); const statePatch = {}; if (state.trackMarksValues) { statePatch.trackMarksValues = updateValues({ values: state.trackMarksValues, newValues: newTrackMarkValues, }); } return statePatch; } } componentDidUpdate() { const newValues = normalizeValue(this.props, this.props.value instanceof Animated.Value ? this.props.value.getValue() : this.props.value); newValues.forEach((value, i) => { if (!this.state.values[i]) { this._setCurrentValue(value, i); } else if (value !== this.state.values[i].__getValue()) { if (this.props.animateTransitions) { this._setCurrentValueAnimated(value, i); } else { this._setCurrentValue(value, i); } } }); } _getRawValues(values) { return values.map((value) => value.getValue()); } _handleStartShouldSetPanResponder = (e) => this._thumbHitTest(e); // Should we become active when the user presses down on the thumb? _handleMoveShouldSetPanResponder() { // Should we become active when the user moves a touch over the thumb? return false; } _handlePanResponderGrant = (e, gestureState) => { const { thumbSize } = this.state; const { nativeEvent } = e; this._previousLeft = this.props.trackClickable ? nativeEvent.locationX - thumbSize.width : this._getThumbLeft(this._getCurrentValue(this._activeThumbIndex)); this.props?.onSlidingStart?.(this._getRawValues(this.state.values)); this.long_press_timeout = setTimeout(() => {
this._setCurrentValue(this._getValue(gestureState), this._activeThumbIndex, () => {
if (this.props.trackClickable) {
this.props?.onValueChange?.(this._getRawValues(this.state.values));
}
this.props?.onSlidingComplete?.(this._getRawValues(this.state.values));
});
},
100);
};
_handlePanResponderMove = (_e, gestureState) => {
clearTimeout(this.long_press_timeout);
this.long_press_timeout = null;
if(!this.hitThumb) {
return;
}
if (this.props.disabled) {
return;
}
this._setCurrentValue(this._getValue(gestureState), this._activeThumbIndex, () => {
this.props?.onValueChange?.(this._getRawValues(this.state.values));
});
};
_handlePanResponderRequestEnd = () => {
// Should we allow another component to take over this pan?
return false;
};
_handlePanResponderEnd = (_e, gestureState) => {
clearTimeout(this.long_press_timeout);
this.long_press_timeout = null;
this.hitThumb = false;
return;
if (this.props.disabled) {
return;
}
this._setCurrentValue(this._getValue(gestureState), this._activeThumbIndex, () => {
if (this.props.trackClickable) {
this.props?.onValueChange?.(this._getRawValues(this.state.values));
}
this.props?.onSlidingComplete?.(this._getRawValues(this.state.values));
});
this._activeThumbIndex = 0;
};
_measureContainer = (e) => {
this._handleMeasure('_containerSize', e);
};
_measureTrack = (e) => {
this._handleMeasure('_trackSize', e);
};
_measureThumb = (e) => {
this._handleMeasure('_thumbSize', e);
};
_handleMeasure = (name, e) => {
const { width, height } = e.nativeEvent.layout;
const size = {
width,
height,
};
const currentSize = this[name];
if (currentSize &&
width === currentSize.width &&
height === currentSize.height) {
return;
}
this[name] = size;
if (this._containerSize && this._thumbSize) {
this.setState({
containerSize: this._containerSize,
thumbSize: this._thumbSize,
allMeasured: true,
});
}
};
_getRatio = (value) => {
const { maximumValue, minimumValue } = this.props;
return (value - minimumValue) / (maximumValue - minimumValue);
};
_getThumbLeft = (value) => {
const { containerSize, thumbSize } = this.state;
const { vertical } = this.props;
const standardRatio = this._getRatio(value);
const ratio = I18nManager.isRTL ? 1 - standardRatio : standardRatio;
return (ratio *
((vertical ? containerSize.height : containerSize.width) -
thumbSize.width));
};
_getValue = (gestureState) => {
const { containerSize, thumbSize, values } = this.state;
const { maximumValue, minimumValue, step, vertical } = this.props;
const length = containerSize.width - thumbSize.width;
const thumbLeft = vertical
? this._previousLeft + gestureState.dy * -1
: this._previousLeft + gestureState.dx;
const nonRtlRatio = thumbLeft / length;
const ratio = I18nManager.isRTL ? 1 - nonRtlRatio : nonRtlRatio;
let minValue = minimumValue;
let maxValue = maximumValue;
const rawValues = this._getRawValues(values);
const buffer = step ? step : 0.1;
if (values.length === 2) {
if (this._activeThumbIndex === 1) {
minValue = rawValues[0] + buffer;
}
else {
maxValue = rawValues[1] - buffer;
}
}
if (step) {
return Math.max(minValue, Math.min(maxValue, minimumValue +
Math.round((ratio * (maximumValue - minimumValue)) / step) *
step));
}
return Math.max(minValue, Math.min(maxValue, ratio * (maximumValue - minimumValue) + minimumValue));
};
_getCurrentValue = (thumbIndex = 0) => this.state.values[thumbIndex].__getValue();
_setCurrentValue = (value, thumbIndex, callback) => {
const safeIndex = thumbIndex ?? 0;
const animatedValue = this.state.values[safeIndex];
if (animatedValue) {
animatedValue.setValue(value);
if (callback) {
callback();
}
}
else {
this.setState((prevState) => {
const newValues = [...prevState.values];
newValues[safeIndex] = new Animated.Value(value);
return {
values: newValues,
};
}, callback);
}
};
_setCurrentValueAnimated = (value, thumbIndex = 0) => {
const { animationType } = this.props;
const animationConfig = {
...DEFAULT_ANIMATION_CONFIGS[animationType],
...this.props.animationConfig,
toValue: value,
useNativeDriver: false,
};
Animated[animationType](this.state.values[thumbIndex], animationConfig).start();
};
_getTouchOverflowSize = () => {
const { allMeasured, containerSize, thumbSize } = this.state;
const { thumbTouchSize } = this.props;
const size = {
width: 40,
height: 40,
};
if (allMeasured) {
size.width = Math.max(0, thumbTouchSize?.width || 0 - thumbSize.width);
size.height = Math.max(0, thumbTouchSize?.height || 0 - containerSize.height);
}
return size;
};
_getTouchOverflowStyle = () => {
const { width, height } = this._getTouchOverflowSize();
const touchOverflowStyle = {};
if (width !== undefined && height !== undefined) {
const verticalMargin = -height / 2;
touchOverflowStyle.marginTop = verticalMargin;
touchOverflowStyle.marginBottom = verticalMargin;
const horizontalMargin = -width / 2;
touchOverflowStyle.marginLeft = horizontalMargin;
touchOverflowStyle.marginRight = horizontalMargin;
}
if (this.props.debugTouchArea === true) {
touchOverflowStyle.backgroundColor = 'blue';
touchOverflowStyle.opacity = 0.5;
}
return touchOverflowStyle;
};
_thumbHitTest = (e, gestureState) => {
const { nativeEvent } = e;
const { trackClickable } = this.props;
const { values } = this.state;
const hitThumb = values.find((_, i) => {
const thumbTouchRect = this._getThumbTouchRect(i);
const containsPoint = thumbTouchRect.containsPoint(nativeEvent.locationX, nativeEvent.locationY);
if (containsPoint) {
this._activeThumbIndex = i;
}
return containsPoint;
});
if (hitThumb) {
this.hitThumb = true;
return true;
}
return true;
if (false) {
// set the active thumb index
if (values.length === 1) {
this._activeThumbIndex = 0;
}
else {
// we will find the closest thumb and that will be the active thumb
const thumbDistances = values.map((_value, index) => {
const thumbTouchRect = this._getThumbTouchRect(index);
return thumbTouchRect.trackDistanceToPoint(nativeEvent.locationX);
});
this._activeThumbIndex = indexOfLowest(thumbDistances);
}
return true;
}
return false;
};
_getThumbTouchRect = (thumbIndex = 0) => {
const { containerSize, thumbSize } = this.state;
const { thumbTouchSize } = this.props;
const { height, width } = thumbTouchSize || { height: 40, width: 40 };
const touchOverflowSize = this._getTouchOverflowSize();
return Rect({
height,
width,
x: touchOverflowSize.width / 2 +
this._getThumbLeft(this._getCurrentValue(thumbIndex)) +
(thumbSize.width - width) / 2,
y: touchOverflowSize.height / 2 +
(containerSize.height - height) / 2,
});
};
_activeThumbIndex = 0;
_containerSize;
_panResponder;
_previousLeft = 0;
_thumbSize;
_trackSize;
_renderDebugThumbTouchRect = (thumbLeft, index) => {
const { height, y, width } = this._getThumbTouchRect() || {};
const positionStyle = {
height,
left: thumbLeft,
top: y,
width,
};
return (React.createElement(Animated.View, { key: `debug-thumb-${index}`, pointerEvents: "none", style: [styles.debugThumbTouchArea, positionStyle] }));
};
_renderThumbImage = (thumbIndex = 0) => {
const { thumbImage } = this.props;
if (!thumbImage) {
return null;
}
return (React.createElement(Image, { source: (Array.isArray(thumbImage)
? thumbImage[thumbIndex]
: thumbImage) }));
};
render() {
const { containerStyle, debugTouchArea, maximumTrackTintColor, maximumValue, minimumTrackTintColor, minimumValue, renderAboveThumbComponent, renderBelowThumbComponent, renderTrackMarkComponent, renderThumbComponent, renderMinimumTrackComponent, renderMaximumTrackComponent, thumbStyle, thumbTintColor, trackStyle, minimumTrackStyle: propMinimumTrackStyle, maximumTrackStyle: propMaximumTrackStyle, vertical, startFromZero, step = 0, ...other } = this.props;
const { allMeasured, containerSize, thumbSize, trackMarksValues, values, } = this.state;
const _startFromZero = values.length === 1 && minimumValue < 0 && maximumValue > 0
? startFromZero
: false;
const interpolatedThumbValues = values.map((value) => value.interpolate({
inputRange: [minimumValue, maximumValue],
outputRange: I18nManager.isRTL
? [0, -(containerSize.width - thumbSize.width)]
: [0, containerSize.width - thumbSize.width],
}));
const interpolatedTrackValues = values.map((value) => value.interpolate({
inputRange: [minimumValue, maximumValue],
outputRange: [0, containerSize.width - thumbSize.width],
}));
const interpolatedTrackMarksValues = trackMarksValues &&
trackMarksValues.map((v) => v.interpolate({
inputRange: [minimumValue, maximumValue],
outputRange: I18nManager.isRTL
? [0, -(containerSize.width - thumbSize.width)]
: [0, containerSize.width - thumbSize.width],
}));
const valueVisibleStyle = {};
if (!allMeasured) {
valueVisibleStyle.opacity = 0;
}
const interpolatedRawValues = this._getRawValues(interpolatedTrackValues);
const minRawValue = Math.min(...interpolatedRawValues);
const minThumbValue = new Animated.Value(minRawValue);
const maxRawValue = Math.max(...interpolatedRawValues);
const maxThumbValue = new Animated.Value(maxRawValue);
const _value = values[0].__getValue();
const sliderWidthCoefficient = containerSize.width /
(Math.abs(minimumValue) + Math.abs(maximumValue));
const startPositionOnTrack = _startFromZero
? _value < 0 + step
? (_value + Math.abs(minimumValue)) * sliderWidthCoefficient
: Math.abs(minimumValue) * sliderWidthCoefficient
: 0;
const minTrackWidth = _startFromZero
? Math.abs(_value) * sliderWidthCoefficient - thumbSize.width / 2
: interpolatedTrackValues[0];
const clearBorderRadius = {};
if (_startFromZero && _value < 0 + step) {
clearBorderRadius.borderBottomRightRadius = 0;
clearBorderRadius.borderTopRightRadius = 0;
}
if (_startFromZero && _value > 0) {
clearBorderRadius.borderTopLeftRadius = 0;
clearBorderRadius.borderBottomLeftRadius = 0;
}
const minimumTrackStyle = {
position: 'absolute',
left: interpolatedTrackValues.length === 1
? new Animated.Value(startPositionOnTrack)
: Animated.add(minThumbValue, thumbSize.width / 2),
width: interpolatedTrackValues.length === 1
? Animated.add(minTrackWidth, thumbSize.width / 2)
: Animated.add(Animated.multiply(minThumbValue, -1), maxThumbValue),
backgroundColor: minimumTrackTintColor,
...valueVisibleStyle,
...clearBorderRadius,
};
const touchOverflowStyle = this._getTouchOverflowStyle();
return (React.createElement(React.Fragment, null,
renderAboveThumbComponent && (React.createElement(View, { style: styles.aboveThumbComponentsContainer }, interpolatedThumbValues.map((interpolationValue, i) => {
const animatedValue = values[i] || 0;
const value = animatedValue instanceof Animated.Value
? animatedValue.__getValue()
: animatedValue;
return (React.createElement(Animated.View, { key: `slider-above-thumb-${i}`, style: [
styles.renderThumbComponent,
{
bottom: 0,
left: thumbSize.width / 2,
transform: [
{
translateX: interpolationValue,
},
{
translateY: 0,
},
],
...valueVisibleStyle,
},
] }, renderAboveThumbComponent(i, value)));
}))),
React.createElement(View, { ...other, style: [
styles.container,
vertical ? { transform: [{ rotate: '-90deg' }] } : {},
containerStyle,
], onLayout: this._measureContainer },
React.createElement(View, { renderToHardwareTextureAndroid: true, style: [
styles.track,
{
backgroundColor: maximumTrackTintColor,
},
trackStyle,
propMaximumTrackStyle,
], onLayout: this._measureTrack }, renderMaximumTrackComponent
? renderMaximumTrackComponent()
: null),
React.createElement(Animated.View, { renderToHardwareTextureAndroid: true, style: [
styles.track,
trackStyle,
minimumTrackStyle,
propMinimumTrackStyle,
] }, renderMinimumTrackComponent
? renderMinimumTrackComponent()
: null),
renderTrackMarkComponent &&
interpolatedTrackMarksValues &&
interpolatedTrackMarksValues.map((value, i) => (React.createElement(Animated.View, { key: `track-mark-${i}`, style: [
styles.renderThumbComponent,
{
transform: [
{
translateX: value,
},
{
translateY: 0,
},
],
...valueVisibleStyle,
},
] }, renderTrackMarkComponent(i)))),
interpolatedThumbValues.map((value, i) => (React.createElement(Animated.View, { key: `slider-thumb-${i}`, style: [
renderThumbComponent
? styles.renderThumbComponent
: styles.thumb,
renderThumbComponent
? {}
: {
backgroundColor: thumbTintColor,
...thumbStyle,
},
{
transform: [
{
translateX: value,
},
{
translateY: 0,
},
],
...valueVisibleStyle,
},
], onLayout: this._measureThumb }, renderThumbComponent
? Array.isArray(renderThumbComponent)
? renderThumbComponent[i](i)
: renderThumbComponent(i)
: this._renderThumbImage(i)))),
React.createElement(View, { style: [styles.touchArea, touchOverflowStyle], ...this._panResponder.panHandlers }, !!debugTouchArea &&
interpolatedThumbValues.map((value, i) => this._renderDebugThumbTouchRect(value, i)))),
renderBelowThumbComponent && (React.createElement(View, { style: styles.belowThumbComponentsContainer }, interpolatedThumbValues.map((interpolationValue, i) => {
const animatedValue = values[i] || 0;
const value = animatedValue instanceof Animated.Value
? animatedValue.__getValue()
: animatedValue;
return (React.createElement(Animated.View, { key: `slider-below-thumb-${i}`, style: [
styles.renderThumbComponent,
{
top: 0,
left: thumbSize.width / 2,
transform: [
{
translateX: interpolationValue,
},
{
translateY: 0,
},
],
...valueVisibleStyle,
},
] }, renderBelowThumbComponent(i, value)));
})))));
}
} //# sourceMappingURL=index.js.map `
@rt012 thank you for your suggestion. Do you know if the Slider has such a prop for click delay? I can't see the import of it in your comment.
Not for now.. you can add if you want.. at the moment the delay is fixed to 100ms , see the code block here:
this.long_press_timeout = setTimeout(() => { this._setCurrentValue(this._getValue(gestureState), this._activeThumbIndex, () => { if (this.props.trackClickable) { this.props?.onValueChange?.(this._getRawValues(this.state.values)); } this.props?.onSlidingComplete?.(this._getRawValues(this.state.values)); }); }, 100);
Hello @miblanchard , can you consider adding such a behavior for the next verson/subversion with a click delay prop?
@angelzbg Hi, I got the same problem, did u find the solution?
@lucyanddarlin Hi, I haven't found a solution to this problem still.
In my app I have the Slider component implemented in a ScrollView. When I try to scroll up or down my finger accidentally falls into the slider and instead of scrolling it starts sliding while my fingers moves up/down. It's a very unpleasant user experience. Can someone suggest a way to fix that behavior?
Used props: value={value} animateTransitions minimumValue={0} maximumValue={sliderMarks.length - 1} step={1} trackClickable={true} onValueChange={(value) => setValue(value[0])}