Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Slider touch stops scrolling in scrollview #412

Open
angelzbg opened this issue Jul 14, 2023 · 6 comments
Open

Slider touch stops scrolling in scrollview #412

angelzbg opened this issue Jul 14, 2023 · 6 comments

Comments

@angelzbg
Copy link

angelzbg commented Jul 14, 2023

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])}

@rt012
Copy link

rt012 commented Aug 14, 2023

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
`

@angelzbg
Copy link
Author

@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.

@rt012
Copy link

rt012 commented Aug 15, 2023

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);

@angelzbg
Copy link
Author

Hello @miblanchard , can you consider adding such a behavior for the next verson/subversion with a click delay prop?

@lucyanddarlin
Copy link

@angelzbg Hi, I got the same problem, did u find the solution?

@angelzbg
Copy link
Author

angelzbg commented Feb 8, 2024

@lucyanddarlin Hi, I haven't found a solution to this problem still.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants