@esteemapp/react-native-multi-slider
Version:
Android and iOS supported pure JS slider component with multiple markers for React Native
529 lines (485 loc) • 14.9 kB
JavaScript
import React from 'react';
import PropTypes from 'prop-types';
import {
StyleSheet,
PanResponder,
View,
TouchableHighlight,
Platform,
I18nManager,
} from 'react-native';
import DefaultMarker from './DefaultMarker';
import { createArray, valueToPosition, positionToValue } from './converters';
const ViewPropTypes = require('react-native').ViewPropTypes || View.propTypes;
export default class MultiSlider extends React.Component {
static defaultProps = {
values: [0],
onValuesChangeStart: () => {},
onValuesChange: values => {},
onValuesChangeFinish: values => {},
step: 1,
min: 0,
max: 10,
touchDimensions: {
borderRadius: 15,
slipDisplacement: 200,
},
customMarker: DefaultMarker,
customMarkerLeft: DefaultMarker,
customMarkerRight: DefaultMarker,
markerOffsetX: 0,
markerOffsetY: 0,
sliderLength: 280,
onToggleOne: undefined,
onToggleTwo: undefined,
enabledOne: true,
enabledTwo: true,
allowOverlap: false,
snapped: false,
vertical: false,
};
constructor(props) {
super(props);
this.optionsArray =
this.props.optionsArray ||
createArray(this.props.min, this.props.max, this.props.step);
this.stepLength = this.props.sliderLength / this.optionsArray.length;
var initialValues = this.props.values.map(value =>
valueToPosition(value, this.optionsArray, this.props.sliderLength),
);
this.state = {
pressedOne: true,
valueOne: this.props.values[0],
valueTwo: this.props.values[1],
pastOne: initialValues[0],
pastTwo: initialValues[1],
positionOne: initialValues[0],
positionTwo: initialValues[1],
};
}
UNSAFE_componentWillMount() {
var customPanResponder = (start, move, end) => {
return PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
onPanResponderGrant: (evt, gestureState) => start(),
onPanResponderMove: (evt, gestureState) => move(gestureState),
onPanResponderTerminationRequest: (evt, gestureState) => false,
onPanResponderRelease: (evt, gestureState) => end(gestureState),
onPanResponderTerminate: (evt, gestureState) => end(gestureState),
onShouldBlockNativeResponder: (evt, gestureState) => true,
});
};
this._panResponderOne = customPanResponder(
this.startOne,
this.moveOne,
this.endOne,
);
this._panResponderTwo = customPanResponder(
this.startTwo,
this.moveTwo,
this.endTwo,
);
}
UNSAFE_componentWillReceiveProps(nextProps) {
if (this.state.onePressed || this.state.twoPressed) {
return;
}
let nextState = {};
if (
nextProps.min !== this.props.min ||
nextProps.max !== this.props.max ||
nextProps.values[0] !== this.state.valueOne ||
nextProps.sliderLength !== this.props.sliderLength ||
nextProps.values[1] !== this.state.valueTwo ||
(nextProps.sliderLength !== this.props.sliderLength &&
nextProps.values[1])
) {
this.optionsArray =
this.props.optionsArray ||
createArray(nextProps.min, nextProps.max, nextProps.step);
this.stepLength = this.props.sliderLength / this.optionsArray.length;
var positionOne = valueToPosition(
nextProps.values[0],
this.optionsArray,
nextProps.sliderLength,
);
nextState.valueOne = nextProps.values[0];
nextState.pastOne = positionOne;
nextState.positionOne = positionOne;
var positionTwo = valueToPosition(
nextProps.values[1],
this.optionsArray,
nextProps.sliderLength,
);
nextState.valueTwo = nextProps.values[1];
nextState.pastTwo = positionTwo;
nextState.positionTwo = positionTwo;
}
if (nextState != {}) {
this.setState(nextState);
}
}
startOne = () => {
if (this.props.enabledOne) {
this.props.onValuesChangeStart();
this.setState({
onePressed: !this.state.onePressed,
});
}
};
startTwo = () => {
if (this.props.enabledTwo) {
this.props.onValuesChangeStart();
this.setState({
twoPressed: !this.state.twoPressed,
});
}
};
moveOne = gestureState => {
if (!this.props.enabledOne) {
return;
}
const accumDistance = this.props.vertical
? -gestureState.dy
: gestureState.dx;
const accumDistanceDisplacement = this.props.vertical
? gestureState.dx
: gestureState.dy;
const unconfined = I18nManager.isRTL
? this.state.pastOne - accumDistance
: accumDistance + this.state.pastOne;
var bottom = 0;
var trueTop =
this.state.positionTwo - (this.props.allowOverlap ? 0 : this.stepLength);
var top = trueTop === 0 ? 0 : trueTop || this.props.sliderLength;
var confined =
unconfined < bottom ? bottom : unconfined > top ? top : unconfined;
var slipDisplacement = this.props.touchDimensions.slipDisplacement;
if (
Math.abs(accumDistanceDisplacement) < slipDisplacement ||
!slipDisplacement
) {
var value = positionToValue(
confined,
this.optionsArray,
this.props.sliderLength,
);
var snapped = valueToPosition(
value,
this.optionsArray,
this.props.sliderLength,
);
this.setState({
positionOne: this.props.snapped ? snapped : confined,
});
if (value !== this.state.valueOne) {
this.setState(
{
valueOne: value,
},
() => {
var change = [this.state.valueOne];
if (this.state.valueTwo) {
change.push(this.state.valueTwo);
}
this.props.onValuesChange(change);
},
);
}
}
};
moveTwo = gestureState => {
if (!this.props.enabledTwo) {
return;
}
const accumDistance = this.props.vertical
? -gestureState.dy
: gestureState.dx;
const accumDistanceDisplacement = this.props.vertical
? gestureState.dx
: gestureState.dy;
const unconfined = I18nManager.isRTL
? this.state.pastTwo - accumDistance
: accumDistance + this.state.pastTwo;
var bottom =
this.state.positionOne + (this.props.allowOverlap ? 0 : this.stepLength);
var top = this.props.sliderLength;
var confined =
unconfined < bottom ? bottom : unconfined > top ? top : unconfined;
var slipDisplacement = this.props.touchDimensions.slipDisplacement;
if (
Math.abs(accumDistanceDisplacement) < slipDisplacement ||
!slipDisplacement
) {
var value = positionToValue(
confined,
this.optionsArray,
this.props.sliderLength,
);
var snapped = valueToPosition(
value,
this.optionsArray,
this.props.sliderLength,
);
this.setState({
positionTwo: this.props.snapped ? snapped : confined,
});
if (value !== this.state.valueTwo) {
this.setState(
{
valueTwo: value,
},
() => {
this.props.onValuesChange([
this.state.valueOne,
this.state.valueTwo,
]);
},
);
}
}
};
endOne = gestureState => {
if (gestureState.moveX === 0 && this.props.onToggleOne) {
this.props.onToggleOne();
return;
}
this.setState(
{
pastOne: this.state.positionOne,
onePressed: !this.state.onePressed,
},
() => {
var change = [this.state.valueOne];
if (this.state.valueTwo) {
change.push(this.state.valueTwo);
}
this.props.onValuesChangeFinish(change);
},
);
};
endTwo = gestureState => {
if (gestureState.moveX === 0 && this.props.onToggleTwo) {
this.props.onToggleTwo();
return;
}
this.setState(
{
twoPressed: !this.state.twoPressed,
pastTwo: this.state.positionTwo,
},
() => {
this.props.onValuesChangeFinish([
this.state.valueOne,
this.state.valueTwo,
]);
},
);
};
render() {
const { positionOne, positionTwo } = this.state;
const {
selectedStyle,
unselectedStyle,
sliderLength,
markerOffsetX,
markerOffsetY,
} = this.props;
const twoMarkers = this.props.values.length == 2; // when allowOverlap, positionTwo could be 0, identified as string '0' and throwing 'RawText 0 needs to be wrapped in <Text>' error
const trackOneLength = positionOne;
const trackOneStyle = twoMarkers
? unselectedStyle
: selectedStyle || styles.selectedTrack;
const trackThreeLength = twoMarkers ? sliderLength - positionTwo : 0;
const trackThreeStyle = unselectedStyle;
const trackTwoLength = sliderLength - trackOneLength - trackThreeLength;
const trackTwoStyle = twoMarkers
? selectedStyle || styles.selectedTrack
: unselectedStyle;
const Marker = this.props.customMarker;
const MarkerLeft = this.props.customMarkerLeft;
const MarkerRight = this.props.customMarkerRight;
const isMarkersSeparated = this.props.isMarkersSeparated || false;
const {
slipDisplacement,
height,
width,
borderRadius,
} = this.props.touchDimensions;
const touchStyle = {
borderRadius: borderRadius || 0,
};
const markerContainerOne = {
top: markerOffsetY - 24,
left: trackOneLength + markerOffsetX - 24,
};
const markerContainerTwo = {
top: markerOffsetY - 24,
right: trackThreeLength + markerOffsetX - 24,
};
const containerStyle = [styles.container, this.props.containerStyle];
if (this.props.vertical) {
containerStyle.push({
transform: [{ rotate: '-90deg' }],
});
}
return (
<View style={containerStyle}>
<View style={[styles.fullTrack, { width: sliderLength }]}>
<View
style={[
styles.track,
this.props.trackStyle,
trackOneStyle,
{ width: trackOneLength },
]}
/>
<View
style={[
styles.track,
this.props.trackStyle,
trackTwoStyle,
{ width: trackTwoLength },
]}
/>
{twoMarkers && (
<View
style={[
styles.track,
this.props.trackStyle,
trackThreeStyle,
{ width: trackThreeLength },
]}
/>
)}
<View
style={[
styles.markerContainer,
markerContainerOne,
this.props.markerContainerStyle,
positionOne > sliderLength / 2 && styles.topMarkerContainer,
]}
>
<View
style={[styles.touch, touchStyle]}
ref={component => (this._markerOne = component)}
{...this._panResponderOne.panHandlers}
>
{isMarkersSeparated === false ? (
<Marker
enabled={this.props.enabledOne}
pressed={this.state.onePressed}
markerStyle={[styles.marker, this.props.markerStyle]}
pressedMarkerStyle={this.props.pressedMarkerStyle}
currentValue={this.state.valueOne}
valuePrefix={this.props.valuePrefix}
valueSuffix={this.props.valueSuffix}
/>
) : (
<MarkerLeft
enabled={this.props.enabledOne}
pressed={this.state.onePressed}
markerStyle={[styles.marker, this.props.markerStyle]}
pressedMarkerStyle={this.props.pressedMarkerStyle}
currentValue={this.state.valueOne}
valuePrefix={this.props.valuePrefix}
valueSuffix={this.props.valueSuffix}
/>
)}
</View>
</View>
{twoMarkers && positionOne !== this.props.sliderLength && (
<View
style={[
styles.markerContainer,
markerContainerTwo,
this.props.markerContainerStyle,
]}
>
<View
style={[styles.touch, touchStyle]}
ref={component => (this._markerTwo = component)}
{...this._panResponderTwo.panHandlers}
>
{isMarkersSeparated === false ? (
<Marker
pressed={this.state.twoPressed}
markerStyle={this.props.markerStyle}
pressedMarkerStyle={this.props.pressedMarkerStyle}
currentValue={this.state.valueTwo}
enabled={this.props.enabledTwo}
valuePrefix={this.props.valuePrefix}
valueSuffix={this.props.valueSuffix}
/>
) : (
<MarkerRight
pressed={this.state.twoPressed}
markerStyle={this.props.markerStyle}
pressedMarkerStyle={this.props.pressedMarkerStyle}
currentValue={this.state.valueTwo}
enabled={this.props.enabledTwo}
valuePrefix={this.props.valuePrefix}
valueSuffix={this.props.valueSuffix}
/>
)}
</View>
</View>
)}
</View>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
position: 'relative',
height: 50,
justifyContent: 'center',
},
fullTrack: {
flexDirection: 'row',
},
track: {
...Platform.select({
ios: {
height: 2,
borderRadius: 2,
backgroundColor: '#A7A7A7',
},
android: {
height: 2,
backgroundColor: '#CECECE',
},
}),
},
selectedTrack: {
...Platform.select({
ios: {
backgroundColor: '#095FFF',
},
android: {
backgroundColor: '#0D8675',
},
}),
},
markerContainer: {
position: 'absolute',
width: 48,
height: 48,
backgroundColor: 'transparent',
justifyContent: 'center',
alignItems: 'center',
},
topMarkerContainer: {
zIndex: 1,
},
touch: {
backgroundColor: 'transparent',
justifyContent: 'center',
alignItems: 'center',
alignSelf: 'stretch',
},
});