From 1fbd546536afe7564387b37d1bb0c361c8b4c57c Mon Sep 17 00:00:00 2001 From: Joe Gornick Date: Wed, 24 Oct 2018 13:07:38 -0500 Subject: [PATCH] Add ability to support multiple thumbs. --- .../java/com/example/MainApplication.java | 4 +- Example/app.js | 48 +++- package.json | 2 +- src/Slider.js | 261 ++++++++++++------ 4 files changed, 214 insertions(+), 101 deletions(-) diff --git a/Example/android/app/src/main/java/com/example/MainApplication.java b/Example/android/app/src/main/java/com/example/MainApplication.java index 3b5c9a2e..d497e8ef 100644 --- a/Example/android/app/src/main/java/com/example/MainApplication.java +++ b/Example/android/app/src/main/java/com/example/MainApplication.java @@ -22,7 +22,7 @@ protected String getJSMainModuleName() { } @Override - protected boolean getUseDeveloperSupport() { + public boolean getUseDeveloperSupport() { return BuildConfig.DEBUG; } @@ -38,4 +38,4 @@ protected List getPackages() { public ReactNativeHost getReactNativeHost() { return mReactNativeHost; } -} \ No newline at end of file +} diff --git a/Example/app.js b/Example/app.js index d45917f0..89766306 100644 --- a/Example/app.js +++ b/Example/app.js @@ -2,7 +2,7 @@ var React = require('react'); var ReactNative = require('react-native'); -var Slider = require('../src/Slider'); +var { default: Slider } = require('../src/Slider'); var { AppRegistry, StyleSheet, @@ -17,18 +17,24 @@ var DEFAULT_VALUE = 0.2; var SliderContainer = React.createClass({ getInitialState() { return { - value: DEFAULT_VALUE, + value: this.props.value != null || Array.isArray(this.props.value) + ? this.props.value + : DEFAULT_VALUE }; }, render() { - var value = this.state.value; + let value = this.state.value; + + if (!Array.isArray(value)) { + value = [value]; + } return ( {this.props.caption} - {value} + {value.map(v => v.toPrecision(2)).join(',')} {this._renderChildren()} @@ -38,11 +44,11 @@ var SliderContainer = React.createClass({ _renderChildren() { return React.Children.map(this.props.children, (child) => { if (child.type === Slider - || child.type === ReactNative.Slider) { - var value = this.state.value; + || child.type === ReactNative.Slider + ) { return React.cloneElement(child, { - value: value, - onValueChange: (val) => this.setState({value: val}), + value: this.state.value, + onValueChange: (value) => this.setState({ value }), }); } else { return child; @@ -67,6 +73,32 @@ var SliderExample = React.createClass({ + + + + + + + + + { + if (value !== oldValues[i]) { + if (this.props.animateTransitions) { + this._setCurrentValueAnimated(value, i); + } else { + this._setCurrentValue(value, i); + } + } + }); } } @@ -234,37 +258,46 @@ export default class Slider extends PureComponent { ...other } = this.props; const { - value, + values, containerSize, - trackSize, thumbSize, allMeasured, } = this.state; + + const mainStyles = styles || defaultStyles; - const thumbLeft = value.interpolate({ + + const interpolatedThumbValues = values.map((v) => v.interpolate({ inputRange: [minimumValue, maximumValue], outputRange: I18nManager.isRTL ? [0, -(containerSize.width - thumbSize.width)] : [0, containerSize.width - thumbSize.width], // extrapolate: 'clamp', - }); - const minimumTrackWidth = value.interpolate({ - inputRange: [minimumValue, maximumValue], - outputRange: [0, containerSize.width - thumbSize.width], - // extrapolate: 'clamp', - }); + })); + + const valueVisibleStyle = {}; if (!allMeasured) { valueVisibleStyle.opacity = 0; } + const interpolatedRawValues = this._getRawValues(interpolatedThumbValues); + const minThumbValue = new Animated.Value(Math.min(...interpolatedRawValues)); + const maxThumbValue = new Animated.Value(Math.max(...interpolatedRawValues)); + const minimumTrackStyle = { position: 'absolute', - width: Animated.add(minimumTrackWidth, thumbSize.width / 2), + left: interpolatedThumbValues.length === 1 + ? new Animated.Value(0) : + Animated.add(minThumbValue, thumbSize.width / 2), + width: interpolatedThumbValues.length === 1 + ? Animated.add(interpolatedThumbValues[0], thumbSize.width / 2) + : Animated.add(Animated.multiply(minThumbValue, -1), maxThumbValue), backgroundColor: minimumTrackTintColor, - ...valueVisibleStyle, + ...valueVisibleStyle }; + const touchOverflowStyle = this._getTouchOverflowStyle(); return ( @@ -282,37 +315,83 @@ export default class Slider extends PureComponent { renderToHardwareTextureAndroid onLayout={this._measureTrack} /> + - - {this._renderThumbImage()} - + + {interpolatedThumbValues.map((value, i) => ( + + {this._renderThumbImage(i)} + + ))} + - {debugTouchArea === true && - this._renderDebugThumbTouchRect(minimumTrackWidth)} - + > ); } + _normalizePropValue(value) { + const getBetweenValue = (value) => Math.max( + Math.min(value, this.props.maximumValue), + this.props.minimumValue + ); + + if (!Array.isArray(value)) { + return [getBetweenValue(value)]; + } + + return value.map(getBetweenValue); + } + + _updateValues(values, newValues = values) { + if (newValues.length !== values.length) { + return this._updateValues(newValues); + } + + return values.map((value, i) => { + if (value instanceof Animated.Value) { + value.setValue( + newValues[i] instanceof Animated.Value + ? newValues[i].__getValue() + : newValues[i] + ); + } + + if (newValues[i] instanceof Animated.Value) { + value = newValues[i]; + } else { + value = new Animated.Value(newValues[i]); + } + + return value; + }); + } + + _getRawValues(values) { + return values.map((value) => value.__getValue()); + } + + + _getPropsForComponentUpdate(props) { const { value, @@ -328,43 +407,49 @@ export default class Slider extends PureComponent { return otherProps; } - _handleStartShouldSetPanResponder = ( - e: Object /* gestureState: Object */, - ): boolean => + _handleStartShouldSetPanResponder = (e: Object): boolean => { // Should we become active when the user presses down on the thumb? - this._thumbHitTest(e); + return this._thumbHitTest(e); + }; - _handleMoveShouldSetPanResponder(/* e: Object, gestureState: Object */): boolean { + _handleMoveShouldSetPanResponder = (): boolean => { // Should we become active when the user moves a touch over the thumb? return false; - } + }; - _handlePanResponderGrant = (/* e: Object, gestureState: Object */) => { - this._previousLeft = this._getThumbLeft(this._getCurrentValue()); + _handlePanResponderGrant = () => { + this._previousLeft = this._getThumbLeft(this._getCurrentValue(this._activeThumbIndex)); this._fireChangeEvent('onSlidingStart'); }; - _handlePanResponderMove = (e: Object, gestureState: Object) => { + _handlePanResponderMove = (_, gestureState: Object) => { if (this.props.disabled) { return; } - this._setCurrentValue(this._getValue(gestureState)); + this._setCurrentValue( + this._getValue(gestureState), + this._activeThumbIndex + ); this._fireChangeEvent('onValueChange'); }; - _handlePanResponderRequestEnd(e: Object, gestureState: Object) { + _handlePanResponderRequestEnd = () => { // Should we allow another component to take over this pan? return false; - } + }; - _handlePanResponderEnd = (e: Object, gestureState: Object) => { + _handlePanResponderEnd = (_, gestureState: Object) => { if (this.props.disabled) { return; } - this._setCurrentValue(this._getValue(gestureState)); + this._setCurrentValue( + this._getValue(gestureState), + this._activeThumbIndex + ); this._fireChangeEvent('onSlidingComplete'); + this._activeThumbIndex = null; }; _measureContainer = (x: Object) => { @@ -448,13 +533,13 @@ export default class Slider extends PureComponent { ); }; - _getCurrentValue = () => this.state.value.__getValue(); + _getCurrentValue = (thumbIndex = 0) => this.state.values[thumbIndex].__getValue(); - _setCurrentValue = (value: number) => { - this.state.value.setValue(value); + _setCurrentValue = (value: number, thumbIndex = 0) => { + this.state.values[thumbIndex].setValue(value); }; - _setCurrentValueAnimated = (value: number) => { + _setCurrentValueAnimated = (value: number, thumbIndex = 0) => { const animationType = this.props.animationType; const animationConfig = Object.assign( {}, @@ -465,12 +550,15 @@ export default class Slider extends PureComponent { }, ); - Animated[animationType](this.state.value, animationConfig).start(); + Animated[animationType]( + this.state.values[thumbIndex], + animationConfig + ).start(); }; _fireChangeEvent = event => { if (this.props[event]) { - this.props[event](this._getCurrentValue()); + this.props[event](this._getRawValues(this.state.values)); } }; @@ -517,21 +605,31 @@ export default class Slider extends PureComponent { _thumbHitTest = (e: Object) => { const nativeEvent = e.nativeEvent; - const thumbTouchRect = this._getThumbTouchRect(); - return thumbTouchRect.containsPoint( - nativeEvent.locationX, - nativeEvent.locationY, - ); + return this.state.values.find((_, i) => { + const thumbTouchRect = this._getThumbTouchRect(i); + + + const containsPoint = thumbTouchRect.containsPoint( + nativeEvent.locationX, + nativeEvent.locationY, + ); + + if (containsPoint) { + this._activeThumbIndex = i; + } + + return containsPoint; + }) != null; }; - _getThumbTouchRect = () => { + _getThumbTouchRect = (thumbIndex = 0) => { const state = this.state; const props = this.props; const touchOverflowSize = this._getTouchOverflowSize(); return new Rect( touchOverflowSize.width / 2 + - this._getThumbLeft(this._getCurrentValue()) + + this._getThumbLeft(this._getCurrentValue(thumbIndex)) + (state.thumbSize.width - props.thumbTouchSize.width) / 2, touchOverflowSize.height / 2 + (state.containerSize.height - props.thumbTouchSize.height) / 2, @@ -540,29 +638,12 @@ export default class Slider extends PureComponent { ); }; - _renderDebugThumbTouchRect = thumbLeft => { - const thumbTouchRect = this._getThumbTouchRect(); - const positionStyle = { - left: thumbLeft, - top: thumbTouchRect.y, - width: thumbTouchRect.width, - height: thumbTouchRect.height, - }; - - return ( - - ); - }; - - _renderThumbImage = () => { + _renderThumbImage = (thumbIndex = 0) => { const { thumbImage } = this.props; - if (!thumbImage) return; + if (thumbImage == null) return; - return ; + return ; }; }