1 | import React from "react";
|
2 | import PropTypes from "prop-types";
|
3 |
|
4 | import {
|
5 | StyleSheet,
|
6 | PanResponder,
|
7 | View,
|
8 | TouchableHighlight,
|
9 | Platform,
|
10 | I18nManager
|
11 | } from "react-native";
|
12 |
|
13 | import DefaultMarker from "./DefaultMarker";
|
14 | import { createArray, valueToPosition, positionToValue } from "./converters";
|
15 |
|
16 | const ViewPropTypes = require("react-native").ViewPropTypes || View.propTypes;
|
17 |
|
18 | export default class MultiSlider extends React.Component {
|
19 | static defaultProps = {
|
20 | values: [0],
|
21 | onValuesChangeStart: () => {},
|
22 | onValuesChange: values => {},
|
23 | onValuesChangeFinish: values => {},
|
24 | step: 1,
|
25 | min: 0,
|
26 | max: 10,
|
27 | touchDimensions: {
|
28 | borderRadius: 15,
|
29 | slipDisplacement: 200
|
30 | },
|
31 | customMarker: DefaultMarker,
|
32 | customMarkerLeft: DefaultMarker,
|
33 | customMarkerRight: DefaultMarker,
|
34 | markerOffsetX: 0,
|
35 | markerOffsetY: 0,
|
36 | sliderLength: 280,
|
37 | onToggleOne: undefined,
|
38 | onToggleTwo: undefined,
|
39 | enabledOne: true,
|
40 | enabledTwo: true,
|
41 | allowOverlap: false,
|
42 | snapped: false,
|
43 | vertical: false,
|
44 | minMarkerOverlapDistance: 0
|
45 | };
|
46 |
|
47 | constructor(props) {
|
48 | super(props);
|
49 |
|
50 | this.optionsArray =
|
51 | this.props.optionsArray ||
|
52 | createArray(this.props.min, this.props.max, this.props.step);
|
53 | this.stepLength = this.props.sliderLength / this.optionsArray.length;
|
54 |
|
55 | var initialValues = this.props.values.map(value =>
|
56 | valueToPosition(value, this.optionsArray, this.props.sliderLength)
|
57 | );
|
58 |
|
59 | this.state = {
|
60 | pressedOne: true,
|
61 | valueOne: this.props.values[0],
|
62 | valueTwo: this.props.values[1],
|
63 | pastOne: initialValues[0],
|
64 | pastTwo: initialValues[1],
|
65 | positionOne: initialValues[0],
|
66 | positionTwo: initialValues[1]
|
67 | };
|
68 | }
|
69 |
|
70 | componentWillMount() {
|
71 | var customPanResponder = (start, move, end) => {
|
72 | return PanResponder.create({
|
73 | onStartShouldSetPanResponder: (evt, gestureState) => true,
|
74 | onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
|
75 | onMoveShouldSetPanResponder: (evt, gestureState) => true,
|
76 | onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
|
77 | onPanResponderGrant: (evt, gestureState) => start(),
|
78 | onPanResponderMove: (evt, gestureState) => move(gestureState),
|
79 | onPanResponderTerminationRequest: (evt, gestureState) => false,
|
80 | onPanResponderRelease: (evt, gestureState) => end(gestureState),
|
81 | onPanResponderTerminate: (evt, gestureState) => end(gestureState),
|
82 | onShouldBlockNativeResponder: (evt, gestureState) => true
|
83 | });
|
84 | };
|
85 |
|
86 | this._panResponderOne = customPanResponder(
|
87 | this.startOne,
|
88 | this.moveOne,
|
89 | this.endOne
|
90 | );
|
91 | this._panResponderTwo = customPanResponder(
|
92 | this.startTwo,
|
93 | this.moveTwo,
|
94 | this.endTwo
|
95 | );
|
96 | }
|
97 |
|
98 | componentWillReceiveProps(nextProps) {
|
99 | if (this.state.onePressed || this.state.twoPressed) {
|
100 | return;
|
101 | }
|
102 |
|
103 | let nextState = {};
|
104 | if (
|
105 | nextProps.min !== this.props.min ||
|
106 | nextProps.max !== this.props.max ||
|
107 | nextProps.step !== this.props.step ||
|
108 | nextProps.values[0] !== this.state.valueOne ||
|
109 | nextProps.sliderLength !== this.props.sliderLength ||
|
110 | nextProps.values[1] !== this.state.valueTwo ||
|
111 | (nextProps.sliderLength !== this.props.sliderLength &&
|
112 | nextProps.values[1])
|
113 | ) {
|
114 | this.optionsArray =
|
115 | this.props.optionsArray ||
|
116 | createArray(nextProps.min, nextProps.max, nextProps.step);
|
117 |
|
118 | this.stepLength = this.props.sliderLength / this.optionsArray.length;
|
119 |
|
120 | var positionOne = valueToPosition(
|
121 | nextProps.values[0],
|
122 | this.optionsArray,
|
123 | nextProps.sliderLength
|
124 | );
|
125 | nextState.valueOne = nextProps.values[0];
|
126 | nextState.pastOne = positionOne;
|
127 | nextState.positionOne = positionOne;
|
128 |
|
129 | var positionTwo = valueToPosition(
|
130 | nextProps.values[1],
|
131 | this.optionsArray,
|
132 | nextProps.sliderLength
|
133 | );
|
134 | nextState.valueTwo = nextProps.values[1];
|
135 | nextState.pastTwo = positionTwo;
|
136 | nextState.positionTwo = positionTwo;
|
137 | }
|
138 |
|
139 | if (nextState != {}) {
|
140 | this.setState(nextState);
|
141 | }
|
142 | }
|
143 |
|
144 | startOne = () => {
|
145 | if (this.props.enabledOne) {
|
146 | this.props.onValuesChangeStart();
|
147 | this.setState({
|
148 | onePressed: !this.state.onePressed
|
149 | });
|
150 | }
|
151 | };
|
152 |
|
153 | startTwo = () => {
|
154 | if (this.props.enabledTwo) {
|
155 | this.props.onValuesChangeStart();
|
156 | this.setState({
|
157 | twoPressed: !this.state.twoPressed
|
158 | });
|
159 | }
|
160 | };
|
161 |
|
162 | moveOne = gestureState => {
|
163 | if (!this.props.enabledOne) {
|
164 | return;
|
165 | }
|
166 |
|
167 | const accumDistance = this.props.vertical
|
168 | ? -gestureState.dy
|
169 | : gestureState.dx;
|
170 | const accumDistanceDisplacement = this.props.vertical
|
171 | ? gestureState.dx
|
172 | : gestureState.dy;
|
173 |
|
174 | const unconfined = I18nManager.isRTL
|
175 | ? this.state.pastOne - accumDistance
|
176 | : accumDistance + this.state.pastOne;
|
177 | var bottom = 0;
|
178 | var trueTop =
|
179 | this.state.positionTwo -
|
180 | (this.props.allowOverlap
|
181 | ? 0
|
182 | : this.props.minMarkerOverlapDistance > 0
|
183 | ? this.props.minMarkerOverlapDistance
|
184 | : this.stepLength);
|
185 | var top = trueTop === 0 ? 0 : trueTop || this.props.sliderLength;
|
186 | var confined =
|
187 | unconfined < bottom ? bottom : unconfined > top ? top : unconfined;
|
188 | var slipDisplacement = this.props.touchDimensions.slipDisplacement;
|
189 |
|
190 | if (
|
191 | Math.abs(accumDistanceDisplacement) < slipDisplacement ||
|
192 | !slipDisplacement
|
193 | ) {
|
194 | var value = positionToValue(
|
195 | confined,
|
196 | this.optionsArray,
|
197 | this.props.sliderLength
|
198 | );
|
199 | var snapped = valueToPosition(
|
200 | value,
|
201 | this.optionsArray,
|
202 | this.props.sliderLength
|
203 | );
|
204 | this.setState({
|
205 | positionOne: this.props.snapped ? snapped : confined
|
206 | });
|
207 |
|
208 | if (value !== this.state.valueOne) {
|
209 | this.setState(
|
210 | {
|
211 | valueOne: value
|
212 | },
|
213 | () => {
|
214 | var change = [this.state.valueOne];
|
215 | if (this.state.valueTwo) {
|
216 | change.push(this.state.valueTwo);
|
217 | }
|
218 | this.props.onValuesChange(change);
|
219 | }
|
220 | );
|
221 | }
|
222 | }
|
223 | };
|
224 |
|
225 | moveTwo = gestureState => {
|
226 | if (!this.props.enabledTwo) {
|
227 | return;
|
228 | }
|
229 |
|
230 | const accumDistance = this.props.vertical
|
231 | ? -gestureState.dy
|
232 | : gestureState.dx;
|
233 | const accumDistanceDisplacement = this.props.vertical
|
234 | ? gestureState.dx
|
235 | : gestureState.dy;
|
236 |
|
237 | const unconfined = I18nManager.isRTL
|
238 | ? this.state.pastTwo - accumDistance
|
239 | : accumDistance + this.state.pastTwo;
|
240 | var bottom =
|
241 | this.state.positionOne +
|
242 | (this.props.allowOverlap
|
243 | ? 0
|
244 | : this.props.minMarkerOverlapDistance > 0
|
245 | ? this.props.minMarkerOverlapDistance
|
246 | : this.stepLength);
|
247 | var top = this.props.sliderLength;
|
248 | var confined =
|
249 | unconfined < bottom ? bottom : unconfined > top ? top : unconfined;
|
250 | var slipDisplacement = this.props.touchDimensions.slipDisplacement;
|
251 |
|
252 | if (
|
253 | Math.abs(accumDistanceDisplacement) < slipDisplacement ||
|
254 | !slipDisplacement
|
255 | ) {
|
256 | var value = positionToValue(
|
257 | confined,
|
258 | this.optionsArray,
|
259 | this.props.sliderLength
|
260 | );
|
261 | var snapped = valueToPosition(
|
262 | value,
|
263 | this.optionsArray,
|
264 | this.props.sliderLength
|
265 | );
|
266 |
|
267 | this.setState({
|
268 | positionTwo: this.props.snapped ? snapped : confined
|
269 | });
|
270 |
|
271 | if (value !== this.state.valueTwo) {
|
272 | this.setState(
|
273 | {
|
274 | valueTwo: value
|
275 | },
|
276 | () => {
|
277 | this.props.onValuesChange([
|
278 | this.state.valueOne,
|
279 | this.state.valueTwo
|
280 | ]);
|
281 | }
|
282 | );
|
283 | }
|
284 | }
|
285 | };
|
286 |
|
287 | endOne = gestureState => {
|
288 | if (gestureState.moveX === 0 && this.props.onToggleOne) {
|
289 | this.props.onToggleOne();
|
290 | return;
|
291 | }
|
292 |
|
293 | this.setState(
|
294 | {
|
295 | pastOne: this.state.positionOne,
|
296 | onePressed: !this.state.onePressed
|
297 | },
|
298 | () => {
|
299 | var change = [this.state.valueOne];
|
300 | if (this.state.valueTwo) {
|
301 | change.push(this.state.valueTwo);
|
302 | }
|
303 | this.props.onValuesChangeFinish(change);
|
304 | }
|
305 | );
|
306 | };
|
307 |
|
308 | endTwo = gestureState => {
|
309 | if (gestureState.moveX === 0 && this.props.onToggleTwo) {
|
310 | this.props.onToggleTwo();
|
311 | return;
|
312 | }
|
313 |
|
314 | this.setState(
|
315 | {
|
316 | twoPressed: !this.state.twoPressed,
|
317 | pastTwo: this.state.positionTwo
|
318 | },
|
319 | () => {
|
320 | this.props.onValuesChangeFinish([
|
321 | this.state.valueOne,
|
322 | this.state.valueTwo
|
323 | ]);
|
324 | }
|
325 | );
|
326 | };
|
327 |
|
328 | render() {
|
329 | const { positionOne, positionTwo } = this.state;
|
330 | const {
|
331 | selectedStyle,
|
332 | unselectedStyle,
|
333 | sliderLength,
|
334 | markerOffsetX,
|
335 | markerOffsetY
|
336 | } = this.props;
|
337 | const twoMarkers = this.props.values.length == 2;
|
338 |
|
339 | const trackOneLength = positionOne;
|
340 | const trackOneStyle = twoMarkers
|
341 | ? unselectedStyle
|
342 | : selectedStyle || styles.selectedTrack;
|
343 | const trackThreeLength = twoMarkers ? sliderLength - positionTwo : 0;
|
344 | const trackThreeStyle = unselectedStyle;
|
345 | const trackTwoLength = sliderLength - trackOneLength - trackThreeLength;
|
346 | const trackTwoStyle = twoMarkers
|
347 | ? selectedStyle || styles.selectedTrack
|
348 | : unselectedStyle;
|
349 | const Marker = this.props.customMarker;
|
350 |
|
351 | const MarkerLeft = this.props.customMarkerLeft;
|
352 | const MarkerRight = this.props.customMarkerRight;
|
353 | const isMarkersSeparated = this.props.isMarkersSeparated || false;
|
354 |
|
355 | const {
|
356 | slipDisplacement,
|
357 | height,
|
358 | width,
|
359 | borderRadius
|
360 | } = this.props.touchDimensions;
|
361 | const touchStyle = {
|
362 | borderRadius: borderRadius || 0
|
363 | };
|
364 |
|
365 | const markerContainerOne = {
|
366 | top: markerOffsetY - 24,
|
367 | left: trackOneLength + markerOffsetX - 24
|
368 | };
|
369 |
|
370 | const markerContainerTwo = {
|
371 | top: markerOffsetY - 24,
|
372 | right: trackThreeLength - markerOffsetX - 24
|
373 | };
|
374 |
|
375 | const containerStyle = [styles.container, this.props.containerStyle];
|
376 |
|
377 | if (this.props.vertical) {
|
378 | containerStyle.push({
|
379 | transform: [{ rotate: "-90deg" }]
|
380 | });
|
381 | }
|
382 |
|
383 | return (
|
384 | <View style={containerStyle}>
|
385 | <View style={[styles.fullTrack, { width: sliderLength }]}>
|
386 | <View
|
387 | style={[
|
388 | styles.track,
|
389 | this.props.trackStyle,
|
390 | trackOneStyle,
|
391 | { width: trackOneLength }
|
392 | ]}
|
393 | />
|
394 | <View
|
395 | style={[
|
396 | styles.track,
|
397 | this.props.trackStyle,
|
398 | trackTwoStyle,
|
399 | { width: trackTwoLength }
|
400 | ]}
|
401 | />
|
402 | {twoMarkers && (
|
403 | <View
|
404 | style={[
|
405 | styles.track,
|
406 | this.props.trackStyle,
|
407 | trackThreeStyle,
|
408 | { width: trackThreeLength }
|
409 | ]}
|
410 | />
|
411 | )}
|
412 | <View
|
413 | style={[
|
414 | styles.markerContainer,
|
415 | markerContainerOne,
|
416 | this.props.markerContainerStyle,
|
417 | positionOne > sliderLength / 2 && styles.topMarkerContainer
|
418 | ]}
|
419 | >
|
420 | <View
|
421 | style={[styles.touch, touchStyle]}
|
422 | ref={component => (this._markerOne = component)}
|
423 | {...this._panResponderOne.panHandlers}
|
424 | >
|
425 | {isMarkersSeparated === false ? (
|
426 | <Marker
|
427 | enabled={this.props.enabledOne}
|
428 | pressed={this.state.onePressed}
|
429 | markerStyle={[styles.marker, this.props.markerStyle]}
|
430 | pressedMarkerStyle={this.props.pressedMarkerStyle}
|
431 | currentValue={this.state.valueOne}
|
432 | valuePrefix={this.props.valuePrefix}
|
433 | valueSuffix={this.props.valueSuffix}
|
434 | />
|
435 | ) : (
|
436 | <MarkerLeft
|
437 | enabled={this.props.enabledOne}
|
438 | pressed={this.state.onePressed}
|
439 | markerStyle={[styles.marker, this.props.markerStyle]}
|
440 | pressedMarkerStyle={this.props.pressedMarkerStyle}
|
441 | currentValue={this.state.valueOne}
|
442 | valuePrefix={this.props.valuePrefix}
|
443 | valueSuffix={this.props.valueSuffix}
|
444 | />
|
445 | )}
|
446 | </View>
|
447 | </View>
|
448 | {twoMarkers && positionOne !== this.props.sliderLength && (
|
449 | <View
|
450 | style={[
|
451 | styles.markerContainer,
|
452 | markerContainerTwo,
|
453 | this.props.markerContainerStyle
|
454 | ]}
|
455 | >
|
456 | <View
|
457 | style={[styles.touch, touchStyle]}
|
458 | ref={component => (this._markerTwo = component)}
|
459 | {...this._panResponderTwo.panHandlers}
|
460 | >
|
461 | {isMarkersSeparated === false ? (
|
462 | <Marker
|
463 | pressed={this.state.twoPressed}
|
464 | markerStyle={this.props.markerStyle}
|
465 | pressedMarkerStyle={this.props.pressedMarkerStyle}
|
466 | currentValue={this.state.valueTwo}
|
467 | enabled={this.props.enabledTwo}
|
468 | valuePrefix={this.props.valuePrefix}
|
469 | valueSuffix={this.props.valueSuffix}
|
470 | />
|
471 | ) : (
|
472 | <MarkerRight
|
473 | pressed={this.state.twoPressed}
|
474 | markerStyle={this.props.markerStyle}
|
475 | pressedMarkerStyle={this.props.pressedMarkerStyle}
|
476 | currentValue={this.state.valueTwo}
|
477 | enabled={this.props.enabledTwo}
|
478 | valuePrefix={this.props.valuePrefix}
|
479 | valueSuffix={this.props.valueSuffix}
|
480 | />
|
481 | )}
|
482 | </View>
|
483 | </View>
|
484 | )}
|
485 | </View>
|
486 | </View>
|
487 | );
|
488 | }
|
489 | }
|
490 |
|
491 | const styles = StyleSheet.create({
|
492 | container: {
|
493 | position: "relative",
|
494 | height: 50,
|
495 | justifyContent: "center"
|
496 | },
|
497 | fullTrack: {
|
498 | flexDirection: "row"
|
499 | },
|
500 | track: {
|
501 | ...Platform.select({
|
502 | ios: {
|
503 | height: 2,
|
504 | borderRadius: 2,
|
505 | backgroundColor: "#A7A7A7"
|
506 | },
|
507 | android: {
|
508 | height: 2,
|
509 | backgroundColor: "#CECECE"
|
510 | },
|
511 | web: {
|
512 | height: 2,
|
513 | borderRadius: 2,
|
514 | backgroundColor: "#A7A7A7"
|
515 | }
|
516 | })
|
517 | },
|
518 | selectedTrack: {
|
519 | ...Platform.select({
|
520 | ios: {
|
521 | backgroundColor: "#095FFF"
|
522 | },
|
523 | android: {
|
524 | backgroundColor: "#0D8675"
|
525 | },
|
526 | web: {
|
527 | backgroundColor: "#095FFF"
|
528 | }
|
529 | })
|
530 | },
|
531 | markerContainer: {
|
532 | position: "absolute",
|
533 | width: 48,
|
534 | height: 48,
|
535 | backgroundColor: "transparent",
|
536 | justifyContent: "center",
|
537 | alignItems: "center"
|
538 | },
|
539 | topMarkerContainer: {
|
540 | zIndex: 1
|
541 | },
|
542 | touch: {
|
543 | backgroundColor: "transparent",
|
544 | justifyContent: "center",
|
545 | alignItems: "center",
|
546 | alignSelf: "stretch"
|
547 | }
|
548 | });
|