From 56a049dc4fc328531cce35fc23df569b00e6e629 Mon Sep 17 00:00:00 2001 From: awaidmann Date: Thu, 21 Jan 2016 19:25:29 -0800 Subject: [PATCH 1/6] Dual slider implementation. Updated index files and divided the thumb component into its own file. --- lib/index.js | 1 + lib/mdl/DualSlider.js | 367 ++++++++++++++++++++++++++++++++++++++++++ lib/mdl/Thumb.js | 187 +++++++++++++++++++++ lib/mdl/index.js | 1 + 4 files changed, 556 insertions(+) create mode 100644 lib/mdl/DualSlider.js create mode 100644 lib/mdl/Thumb.js diff --git a/lib/index.js b/lib/index.js index 0df4ef09..2c1d86e7 100644 --- a/lib/index.js +++ b/lib/index.js @@ -17,6 +17,7 @@ exports.MKIconToggle = exports.mdl.IconToggle; exports.MKRipple = exports.mdl.Ripple; exports.MKProgress = exports.mdl.Progress; exports.MKSlider = exports.mdl.Slider; +exports.MKDualSlider = exports.mdl.DualSlider; exports.MKSpinner = exports.mdl.Spinner; exports.MKRadioButton = exports.mdl.RadioButton; exports.MKCheckbox = exports.mdl.Checkbox; diff --git a/lib/mdl/DualSlider.js b/lib/mdl/DualSlider.js new file mode 100644 index 00000000..d8ae11a6 --- /dev/null +++ b/lib/mdl/DualSlider.js @@ -0,0 +1,367 @@ +// +// MDL style DualSlider component. +// +// - @see [MDL Slider](http://www.getmdl.io/components/index.html#sliders-section) +// - [Props](#props) +// - [Defaults](#defaults) +// - [Built-in builders](#builders) +// +// Created by ywu on 15/8/23. +// + +const React = require('react-native'); +const MKColor = require('../MKColor'); +const {getTheme} = require('../theme'); +const Thumb = require('./Thumb') + +const { + Component, + Animated, + View, + PanResponder, + PropTypes, +} = React; + + +// Default color of the upper part of the track +const DEFAULT_UPPER_TRACK_COLOR = '#cccccc'; + +// Color of the thumb when lowest value is chosen +const LOWEST_VALUE_THUMB_COLOR = 'white'; + +// The max scale of the thumb +const THUMB_SCALE_RATIO = 1.3; + +// Width of the thumb border +const THUMB_BORDER_WIDTH = 2 + +// extra spacing enlarging the touchable area +const TRACK_EXTRA_MARGIN_V = 5; +const TRACK_EXTRA_MARGIN_H = 5; + + +// ##
Slider
+class DualSlider extends Component { + constructor(props) { + super(props); + this._range = { + minRange: 0, + maxRange: 0, + }; + + // Added to fix a bug where the sliders get stuck on top of each other + this._overriddenThumb + this._trackTotalLength = 0; + this._lowerTrackLength = new Animated.Value(this._range.maxRange - this._range.minRange); + this._lowerTrackMin = new Animated.Value(this._range.minRange) + } + + get sliderMin () { return this._toSliderScale(this._range.minRange) } + get sliderMax () { return this._toSliderScale(this._range.maxRange) } + set sliderMin (sliderMin) { this._setRange() } + set sliderMax (sliderMax) { this._setRange() } + + componentWillMount() { + this._onThumbRadiiUpdate(this.props); + } + + componentWillReceiveProps(nextProps) { + this._onThumbRadiiUpdate(nextProps); + } + + _onTrackLayout({nativeEvent: {layout: {width}}}) { + if (this._trackTotalLength !== width) { + this._trackTotalLength = width; + this._setRange() + this._updateValue(this._range); + } + } + + _setRange() { + var min2Scale = this._toPixelScale(this.props.sliderMin) + var max2Scale = this._toPixelScale(this.props.sliderMax) + + var minBounds = this._toPixelScale(this.props.min) + var maxBounds = this._toPixelScale(this.props.max) + + if(min2Scale > max2Scale) { throw 'Minimum slider value: ' + this.props.sliderMin + ' is greater than max value: ' + this.props.sliderMax } + if(min2Scale < minBounds || min2Scale > maxBounds) { throw 'Minimum slider value: ' + this.props.sliderMin + ' exceeds bounds: ' + this.props.min + "-" + this.props.max } + if(max2Scale < minBounds || max2Scale > maxBounds) { throw 'Maximum slider value: ' + this.props.sliderMax + ' exceeds bounds: ' + this.props.min + "-" + this.props.max } + + this._range = { + minRange: min2Scale ? min2Scale : 0, + maxRange: max2Scale ? max2Scale : 0, + } + } + + _toSliderScale(value) { + var trackToRange = (this.props.max - this.props.min) / this._trackTotalLength + return (value * trackToRange) + this.props.min + } + + _toPixelScale(value) { + var rangeToTrack = this._trackTotalLength / (this.props.max - this.props.min) + return value * rangeToTrack - this.props.min + } + + _internalSetValue(ref, value) { + var target = ref === this.refs.minRange ? 'minRange' : 'maxRange' + this._range[target] = value + this._emitChange(); + } + + _emitChange() { + if (this.props.onChange) { + this.props.onChange({ + min: this._toSliderScale(this._range.minRange), + max: this._toSliderScale(this._range.maxRange), + }); + } + } + + _updateValue(values) { + if (!this._trackTotalLength) { + return; + } + + var lthumb = this.refs.minRange; + var rthumb = this.refs.maxRange; + + this._moveThumb(lthumb, values.minRange); + lthumb.confirmMoveTo(values.minRange); + + this._moveThumb(rthumb, values.maxRange); + rthumb.confirmMoveTo(values.minRange); + } + + _validateMove(dx, trackOriginX, trackWidth, ref) { + var x = dx - trackOriginX + + var onTrack = (x) => { + return x <= 0 ? 0 : (x >= trackWidth ? trackWidth : x) + } + + if(ref) { + var lthumb = this.refs.minRange; + var rthumb = this.refs.maxRange; + + var oRef = ref + if(lthumb.x == rthumb.x) { + if(x > rthumb.x) { + oRef = this._overriddenThumb = rthumb + ref.confirmMoveTo(ref.x) + } + else if(x < lthumb.x) { + oRef = this._overriddenThumb = lthumb + ref.confirmMoveTo(ref.x) + } + } + + var valX + if(oRef === lthumb) { valX = x >= rthumb.x ? rthumb.x : onTrack(x) } + else if(oRef === rthumb) { valX = x <= lthumb.x ? lthumb.x : onTrack(x) } + + return {newRef: oRef, x: valX} + } + } + + _updateValueByTouch(ref, evt) { + var ref = this._overriddenThumb ? this._overriddenThumb : ref + + var dx = evt.nativeEvent.pageX + this.refs.track.measure((fx, fy, width, height, px, py) => { + var {newRef, x} = this._validateMove(dx, px, width, ref) + this._internalSetValue(newRef, x) + this._moveThumb(newRef, x); + }) + } + + _moveThumb(ref, x) { + ref.moveTo(x); + + Animated.parallel([ + Animated.timing(this._lowerTrackMin, { + toValue: this._range.minRange, + duration: 0, + }), + Animated.timing(this._lowerTrackLength, { + toValue: this._range.maxRange - this._range.minRange, + duration: 0, + }), + ]).start() + } + + _endMove(ref, evt) { + var ref = this._overriddenThumb ? this._overriddenThumb : ref + + var dx = evt.nativeEvent.pageX + this.refs.track.measure((fx, fy, width, height, px, py) => { + ref.confirmMoveTo(this._validateMove(dx, px, width)) + this._overriddenThumb = null + }) + } + + // when thumb radii updated, re-calc the dimens + _onThumbRadiiUpdate(props) { + this._thumbRadii = props.thumbRadius; + this._thumbRadiiWithBorder = this._thumbRadii + THUMB_BORDER_WIDTH; + this._trackMarginV = this._thumbRadiiWithBorder * THUMB_SCALE_RATIO + + TRACK_EXTRA_MARGIN_V - this.props.trackSize / 2; + this._trackMarginH = this._thumbRadiiWithBorder * THUMB_SCALE_RATIO + + TRACK_EXTRA_MARGIN_H; + } + + render() { + // making room for the Thumb, cause's Android doesn't support `overflow: visible` + // - @see http://bit.ly/1Fzr5SE + const trackMargin = { + marginLeft: this._trackMarginH, + marginRight: this._trackMarginH, + marginTop: this._trackMarginV, + marginBottom: this._trackMarginV, + }; + + return ( + + + + + this._updateValueByTouch(ref, evt)} + onMove = {(ref, evt) => this._updateValueByTouch(ref, evt)} + onEnd = {(ref, evt) => this._endMove(ref, evt)} + + style={{ + top: this._thumbRadiiWithBorder * (THUMB_SCALE_RATIO - 1) + TRACK_EXTRA_MARGIN_V, + }} + /> + + this._updateValueByTouch(ref, evt, gestureState)} + onMove = {(ref, evt, gestureState) => this._updateValueByTouch(ref, evt, gestureState)} + onEnd = {(ref, evt, gestureState) => this._endMove(ref, evt, gestureState)} + + style={{ + top: this._thumbRadiiWithBorder * (THUMB_SCALE_RATIO - 1) + TRACK_EXTRA_MARGIN_V, + }} + /> + + ); + } +} + +// ##
Props
+DualSlider.propTypes = { + // [RN.View Props](https://facebook.github.io/react-native/docs/view.html#props)... + ...View.propTypes, + + // Minimum value of the range, default is `0` + min: PropTypes.number, + + // Maximum value of the range, default is `100` + max: PropTypes.number, + + // Min preset/user-set slider value + sliderMin: PropTypes.number, + + // Max preset/user-set slider value + sliderMax: PropTypes.number, + + // The thickness of the DualSlider track + trackSize: PropTypes.number, + + // Radius of the thumb of the DualSlider + thumbRadius: PropTypes.number, + + // Color of the lower part of the track, it's also the color of the thumb + lowerTrackColor: PropTypes.string, + + // Color of the upper part of the track + upperTrackColor: PropTypes.string, + + // Callback when value changed + onChange: PropTypes.func, +}; + +// ##
Defaults
+DualSlider.defaultProps = { + thumbRadius: 6, + trackSize: 2, + min: 0, + max: 100, + upperTrackColor: DEFAULT_UPPER_TRACK_COLOR, +}; + + +// -------------------------- +// Builder +// +const { + Builder, +} = require('../builder'); + +// +// ## DualSlider builder +// +class SliderBuilder extends Builder { + build() { + const BuiltSlider = class extends DualSlider {}; + BuiltSlider.defaultProps = Object.assign({}, DualSlider.defaultProps, this.toProps()); + return BuiltSlider; + } +} + +// define builder method for each prop +SliderBuilder.defineProps(DualSlider.propTypes); + +// ---------- +// ##
Built-in builders
+// +function slider() { + return new SliderBuilder().withBackgroundColor(MKColor.Transparent); +} + + +// ## Public interface +module.exports = DualSlider; +DualSlider.Builder = SliderBuilder; +DualSlider.slider = slider; diff --git a/lib/mdl/Thumb.js b/lib/mdl/Thumb.js new file mode 100644 index 00000000..7c0ec70c --- /dev/null +++ b/lib/mdl/Thumb.js @@ -0,0 +1,187 @@ +const React = require('react-native'); +const MKColor = require('../MKColor'); +const {getTheme} = require('../theme'); + +const { + Component, + Animated, + View, + PanResponder, + PropTypes, +} = React; + + +// Default color of the upper part of the track +const DEFAULT_UPPER_TRACK_COLOR = '#cccccc'; + +// Color of the thumb when lowest value is chosen +const LOWEST_VALUE_THUMB_COLOR = 'white'; + +// The max scale of the thumb +const THUMB_SCALE_RATIO = 1.3; + +// Width of the thumb border +const THUMB_BORDER_WIDTH = 2 + +// extra spacing enlarging the touchable area +const TRACK_EXTRA_MARGIN_V = 5; +const TRACK_EXTRA_MARGIN_H = 5; + + +// ##
Thumb
+// `Thumb` component of the [`Slider`](#Slider). +class Thumb extends Component { + constructor(props) { + super(props); + this.x = 0; // current x-axis position + + this._panResponder = {} + this._animatedLeft = new Animated.Value(0); + this._animatedScale = new Animated.Value(1); + this.state = { + color: LOWEST_VALUE_THUMB_COLOR, + borderColor: DEFAULT_UPPER_TRACK_COLOR, + }; + } + + componentWillMount() { + this._panResponder = PanResponder.create({ + onStartShouldSetPanResponder: () => true, + onStartShouldSetPanResponderCapture: () => true, + onMoveShouldSetPanResponder: () => true, + onMoveShouldSetPanResponderCapture: () => true, + onPanResponderTerminationRequest: () => true, + onShouldBlockNativeResponder: () => true, + + onPanResponderGrant: (evt) => { this.props.onGrant(this, evt) }, + onPanResponderMove: (evt) => { this.props.onMove(this, evt) }, + onPanResponderRelease: (evt) => { this.props.onEnd(this, evt) }, + onPanResponderTerminate: (evt) => { this.props.onEnd(this, evt) }, + }); + + this._onRadiiUpdate(this.props); + this.setState({ + borderColor: this.props.disabledColor, + borderWidth: this.props.trackSize, + }); + } + + componentWillReceiveProps(nextProps) { + this._onRadiiUpdate(nextProps); + } + + componentDidMount() { + this._animatedLeft.addListener(this._getOnSliding()); + } + + componentWillUnmount() { + this._animatedLeft.removeAllListeners(); + } + + // when thumb radii updated, re-calc the dimens + _onRadiiUpdate(props) { + this._radii = props.radius; + this._dia = this._radii * 2; + this._containerRadii = this._radii + THUMB_BORDER_WIDTH; + this._containerDia = this._containerRadii * 2; + } + + // return a memoized function to handle sliding animation events + _getOnSliding() { + let prevX = this.x; // memorize the previous x + + // on sliding of the thumb + // `value` - the `left` of the thumb, relative to the container + return ({value}) => { + // convert to value relative to the track + const x = value + this._containerRadii - this.props.trackMarginH; + + if (prevX <= 0 && x > 0) { + // leaving the lowest value, scale up the thumb + this._onExplode(); + } else if (prevX > 0 && x <= 0) { + // at lowest value, scale down the thumb + this._onCollapse(); + } + + prevX = x; + }; + } + + // animate the sliding + // `x` - target position, relative to the track + moveTo(x) { + this.x = x; + const x0 = this.x + this.props.trackMarginH; + + Animated.parallel([ + Animated.timing(this._animatedScale, { + toValue: THUMB_SCALE_RATIO, + duration: 100, + }), + Animated.timing(this._animatedLeft, { + toValue: x0 - this._containerRadii, + duration: 0, + }), + ]).start(); + } + + // stop sliding + confirmMoveTo() { + Animated.timing(this._animatedScale, { + toValue: 1, + duration: 100, + }).start(); + } + + // from 'lowest' to 'non-lowest' + _onExplode() { + this.setState({ + borderColor: this.props.enabledColor, + color: this.props.enabledColor, + }); + } + + // from 'non-lowest' to 'lowest' + _onCollapse() { + this.setState({ + borderColor: this.props.disabledColor, + color: LOWEST_VALUE_THUMB_COLOR, + }); + } + + // Rendering the `Thumb` + render() { + return ( + + + + ); + } +} + +module.exports = Thumb; diff --git a/lib/mdl/index.js b/lib/mdl/index.js index be709fc1..12497672 100644 --- a/lib/mdl/index.js +++ b/lib/mdl/index.js @@ -7,6 +7,7 @@ exports.Textfield = require('./Textfield'); exports.Progress = require('./Progress'); exports.Spinner = require('./Spinner'); exports.Slider = require('./Slider'); +exports.DualSlider = require('./DualSlider'); exports.Button = require('./Button'); exports.Ripple = require('./Ripple'); exports.Card = require('./cards/container'); From 705c55fffe0de1bfda395cef7b92f54d4215ea0c Mon Sep 17 00:00:00 2001 From: awaidmann Date: Fri, 29 Jan 2016 11:49:52 -0800 Subject: [PATCH 2/6] Rename DualSlider to RangeSlider. Airbnb Linter style guide compliant except for .bind(this) in RangeSlider. --- lib/index.js | 2 +- lib/mdl/{DualSlider.js => RangeSlider.js} | 224 ++++++++++++---------- lib/mdl/Thumb.js | 78 ++++---- lib/mdl/index.js | 2 +- 4 files changed, 168 insertions(+), 138 deletions(-) rename lib/mdl/{DualSlider.js => RangeSlider.js} (57%) diff --git a/lib/index.js b/lib/index.js index 2c1d86e7..acfcd465 100644 --- a/lib/index.js +++ b/lib/index.js @@ -14,10 +14,10 @@ exports.MKTextField = exports.mdl.Textfield; exports.MKSwitch = exports.mdl.Switch; exports.MKIconToggle = exports.mdl.IconToggle; +exports.MKRangeSlider = exports.mdl.RangeSlider; exports.MKRipple = exports.mdl.Ripple; exports.MKProgress = exports.mdl.Progress; exports.MKSlider = exports.mdl.Slider; -exports.MKDualSlider = exports.mdl.DualSlider; exports.MKSpinner = exports.mdl.Spinner; exports.MKRadioButton = exports.mdl.RadioButton; exports.MKCheckbox = exports.mdl.Checkbox; diff --git a/lib/mdl/DualSlider.js b/lib/mdl/RangeSlider.js similarity index 57% rename from lib/mdl/DualSlider.js rename to lib/mdl/RangeSlider.js index d8ae11a6..79b92ab1 100644 --- a/lib/mdl/DualSlider.js +++ b/lib/mdl/RangeSlider.js @@ -1,5 +1,5 @@ // -// MDL style DualSlider component. +// MDL style RangeSlider component. // // - @see [MDL Slider](http://www.getmdl.io/components/index.html#sliders-section) // - [Props](#props) @@ -11,14 +11,13 @@ const React = require('react-native'); const MKColor = require('../MKColor'); -const {getTheme} = require('../theme'); -const Thumb = require('./Thumb') +const { getTheme } = require('../theme'); +const Thumb = require('./Thumb'); const { Component, Animated, View, - PanResponder, PropTypes, } = React; @@ -26,14 +25,11 @@ const { // Default color of the upper part of the track const DEFAULT_UPPER_TRACK_COLOR = '#cccccc'; -// Color of the thumb when lowest value is chosen -const LOWEST_VALUE_THUMB_COLOR = 'white'; - // The max scale of the thumb const THUMB_SCALE_RATIO = 1.3; // Width of the thumb border -const THUMB_BORDER_WIDTH = 2 +const THUMB_BORDER_WIDTH = 2; // extra spacing enlarging the touchable area const TRACK_EXTRA_MARGIN_V = 5; @@ -41,7 +37,7 @@ const TRACK_EXTRA_MARGIN_H = 5; // ##
Slider
-class DualSlider extends Component { +class RangeSlider extends Component { constructor(props) { super(props); this._range = { @@ -50,17 +46,12 @@ class DualSlider extends Component { }; // Added to fix a bug where the sliders get stuck on top of each other - this._overriddenThumb + this._overriddenThumb = undefined; this._trackTotalLength = 0; this._lowerTrackLength = new Animated.Value(this._range.maxRange - this._range.minRange); - this._lowerTrackMin = new Animated.Value(this._range.minRange) + this._lowerTrackMin = new Animated.Value(this._range.minRange); } - get sliderMin () { return this._toSliderScale(this._range.minRange) } - get sliderMax () { return this._toSliderScale(this._range.maxRange) } - set sliderMin (sliderMin) { this._setRange() } - set sliderMax (sliderMax) { this._setRange() } - componentWillMount() { this._onThumbRadiiUpdate(this.props); } @@ -69,44 +60,67 @@ class DualSlider extends Component { this._onThumbRadiiUpdate(nextProps); } - _onTrackLayout({nativeEvent: {layout: {width}}}) { + get sliderMin() { return this._toSliderScale(this._range.minRange); } + get sliderMax() { return this._toSliderScale(this._range.maxRange); } + set sliderMin(sliderMin) { + this.props.sliderMin = sliderMin; + this._setRange(); + } + set sliderMax(sliderMax) { + this.props.sliderMax = sliderMax; + this._setRange(); + } + + _onTrackLayout({ nativeEvent: { layout: { width } } }) { if (this._trackTotalLength !== width) { this._trackTotalLength = width; - this._setRange() + this._setRange(); this._updateValue(this._range); } } _setRange() { - var min2Scale = this._toPixelScale(this.props.sliderMin) - var max2Scale = this._toPixelScale(this.props.sliderMax) + const min2Scale = this._toPixelScale(this.props.sliderMin); + const max2Scale = this._toPixelScale(this.props.sliderMax); - var minBounds = this._toPixelScale(this.props.min) - var maxBounds = this._toPixelScale(this.props.max) + const minBounds = this._toPixelScale(this.props.min); + const maxBounds = this._toPixelScale(this.props.max); - if(min2Scale > max2Scale) { throw 'Minimum slider value: ' + this.props.sliderMin + ' is greater than max value: ' + this.props.sliderMax } - if(min2Scale < minBounds || min2Scale > maxBounds) { throw 'Minimum slider value: ' + this.props.sliderMin + ' exceeds bounds: ' + this.props.min + "-" + this.props.max } - if(max2Scale < minBounds || max2Scale > maxBounds) { throw 'Maximum slider value: ' + this.props.sliderMax + ' exceeds bounds: ' + this.props.min + "-" + this.props.max } + if (min2Scale > max2Scale) { + const msg = 'Minimum slider value: ' + this.props.sliderMin + + ' is greater than max value: ' + this.props.sliderMax; + throw msg; + } + if (min2Scale < minBounds || min2Scale > maxBounds) { + const msg = 'Minimum slider value: ' + this.props.sliderMin + + ' exceeds bounds: ' + this.props.min + '-' + this.props.max; + throw msg; + } + if (max2Scale < minBounds || max2Scale > maxBounds) { + const msg = 'Maximum slider value: ' + this.props.sliderMax + + ' exceeds bounds: ' + this.props.min + '-' + this.props.max; + throw msg; + } this._range = { minRange: min2Scale ? min2Scale : 0, maxRange: max2Scale ? max2Scale : 0, - } + }; } _toSliderScale(value) { - var trackToRange = (this.props.max - this.props.min) / this._trackTotalLength - return (value * trackToRange) + this.props.min + const trackToRange = (this.props.max - this.props.min) / this._trackTotalLength; + return (value * trackToRange) + this.props.min; } _toPixelScale(value) { - var rangeToTrack = this._trackTotalLength / (this.props.max - this.props.min) - return value * rangeToTrack - this.props.min + const rangeToTrack = this._trackTotalLength / (this.props.max - this.props.min); + return value * rangeToTrack - this.props.min; } _internalSetValue(ref, value) { - var target = ref === this.refs.minRange ? 'minRange' : 'maxRange' - this._range[target] = value + const target = ref === this.refs.minRange ? 'minRange' : 'maxRange'; + this._range[target] = value; this._emitChange(); } @@ -124,8 +138,8 @@ class DualSlider extends Component { return; } - var lthumb = this.refs.minRange; - var rthumb = this.refs.maxRange; + const lthumb = this.refs.minRange; + const rthumb = this.refs.maxRange; this._moveThumb(lthumb, values.minRange); lthumb.confirmMoveTo(values.minRange); @@ -135,45 +149,48 @@ class DualSlider extends Component { } _validateMove(dx, trackOriginX, trackWidth, ref) { - var x = dx - trackOriginX - - var onTrack = (x) => { - return x <= 0 ? 0 : (x >= trackWidth ? trackWidth : x) - } + const x = dx - trackOriginX; - if(ref) { - var lthumb = this.refs.minRange; - var rthumb = this.refs.maxRange; + const onTrack = (relX) => { + const upperBound = relX >= trackWidth ? trackWidth : relX; + return relX <= 0 ? 0 : upperBound; + }; - var oRef = ref - if(lthumb.x == rthumb.x) { - if(x > rthumb.x) { - oRef = this._overriddenThumb = rthumb - ref.confirmMoveTo(ref.x) - } - else if(x < lthumb.x) { - oRef = this._overriddenThumb = lthumb - ref.confirmMoveTo(ref.x) + if (ref) { + const lthumb = this.refs.minRange; + const rthumb = this.refs.maxRange; + + let oRef = ref; + if (lthumb.x === rthumb.x) { + if (x > rthumb.x) { + oRef = this._overriddenThumb = rthumb; + ref.confirmMoveTo(ref.x); + } else if (x < lthumb.x) { + oRef = this._overriddenThumb = lthumb; + ref.confirmMoveTo(ref.x); } } - var valX - if(oRef === lthumb) { valX = x >= rthumb.x ? rthumb.x : onTrack(x) } - else if(oRef === rthumb) { valX = x <= lthumb.x ? lthumb.x : onTrack(x) } + let valX; + if (oRef === lthumb) { + valX = x >= rthumb.x ? rthumb.x : onTrack(x); + } else if (oRef === rthumb) { + valX = x <= lthumb.x ? lthumb.x : onTrack(x); + } - return {newRef: oRef, x: valX} + return { newRef: oRef, x: valX }; } } _updateValueByTouch(ref, evt) { - var ref = this._overriddenThumb ? this._overriddenThumb : ref + const ovrRef = this._overriddenThumb ? this._overriddenThumb : ref; - var dx = evt.nativeEvent.pageX - this.refs.track.measure((fx, fy, width, height, px, py) => { - var {newRef, x} = this._validateMove(dx, px, width, ref) - this._internalSetValue(newRef, x) + const dx = evt.nativeEvent.pageX; + this.refs.track.measure((fx, fy, width, height, px) => { + const { newRef, x } = this._validateMove(dx, px, width, ovrRef); + this._internalSetValue(newRef, x); this._moveThumb(newRef, x); - }) + }); } _moveThumb(ref, x) { @@ -188,17 +205,17 @@ class DualSlider extends Component { toValue: this._range.maxRange - this._range.minRange, duration: 0, }), - ]).start() + ]).start(); } _endMove(ref, evt) { - var ref = this._overriddenThumb ? this._overriddenThumb : ref + const ovrRef = this._overriddenThumb ? this._overriddenThumb : ref; - var dx = evt.nativeEvent.pageX - this.refs.track.measure((fx, fy, width, height, px, py) => { - ref.confirmMoveTo(this._validateMove(dx, px, width)) - this._overriddenThumb = null - }) + const dx = evt.nativeEvent.pageX; + this.refs.track.measure((fx, fy, width, height, px) => { + ovrRef.confirmMoveTo(this._validateMove(dx, px, width)); + this._overriddenThumb = null; + }); } // when thumb radii updated, re-calc the dimens @@ -223,23 +240,22 @@ class DualSlider extends Component { return ( + style={[this.props.style, { + padding: 0, + paddingTop: 0, + paddingBottom: 0, + paddingLeft: 0, + paddingRight: 0, + }]} + > + style={{ + height: this.props.trackSize, + backgroundColor: this.props.upperTrackColor, + ...trackMargin, + }} + onLayout={ this._onTrackLayout.bind(this) } + > + /> this._updateValueByTouch(ref, evt)} - onMove = {(ref, evt) => this._updateValueByTouch(ref, evt)} - onEnd = {(ref, evt) => this._endMove(ref, evt)} + onGrant = { this._updateValueByTouch.bind(this) } + onMove = { this._updateValueByTouch.bind(this) } + onEnd = { this._endMove.bind(this) } style={{ top: this._thumbRadiiWithBorder * (THUMB_SCALE_RATIO - 1) + TRACK_EXTRA_MARGIN_V, }} - /> + /> this._updateValueByTouch(ref, evt, gestureState)} - onMove = {(ref, evt, gestureState) => this._updateValueByTouch(ref, evt, gestureState)} - onEnd = {(ref, evt, gestureState) => this._endMove(ref, evt, gestureState)} + onGrant = { this._updateValueByTouch.bind(this) } + onMove = { this._updateValueByTouch.bind(this) } + onEnd = { this._endMove.bind(this) } style={{ top: this._thumbRadiiWithBorder * (THUMB_SCALE_RATIO - 1) + TRACK_EXTRA_MARGIN_V, }} - /> + /> ); } } // ##
Props
-DualSlider.propTypes = { +RangeSlider.propTypes = { // [RN.View Props](https://facebook.github.io/react-native/docs/view.html#props)... ...View.propTypes, @@ -306,10 +322,10 @@ DualSlider.propTypes = { // Max preset/user-set slider value sliderMax: PropTypes.number, - // The thickness of the DualSlider track + // The thickness of the RangeSlider track trackSize: PropTypes.number, - // Radius of the thumb of the DualSlider + // Radius of the thumb of the RangeSlider thumbRadius: PropTypes.number, // Color of the lower part of the track, it's also the color of the thumb @@ -323,7 +339,7 @@ DualSlider.propTypes = { }; // ##
Defaults
-DualSlider.defaultProps = { +RangeSlider.defaultProps = { thumbRadius: 6, trackSize: 2, min: 0, @@ -340,18 +356,18 @@ const { } = require('../builder'); // -// ## DualSlider builder +// ## RangeSlider builder // class SliderBuilder extends Builder { build() { - const BuiltSlider = class extends DualSlider {}; - BuiltSlider.defaultProps = Object.assign({}, DualSlider.defaultProps, this.toProps()); + const BuiltSlider = class extends RangeSlider {}; + BuiltSlider.defaultProps = Object.assign({}, RangeSlider.defaultProps, this.toProps()); return BuiltSlider; } } // define builder method for each prop -SliderBuilder.defineProps(DualSlider.propTypes); +SliderBuilder.defineProps(RangeSlider.propTypes); // ---------- // ##
Built-in builders
@@ -362,6 +378,6 @@ function slider() { // ## Public interface -module.exports = DualSlider; -DualSlider.Builder = SliderBuilder; -DualSlider.slider = slider; +module.exports = RangeSlider; +RangeSlider.Builder = SliderBuilder; +RangeSlider.slider = slider; diff --git a/lib/mdl/Thumb.js b/lib/mdl/Thumb.js index 7c0ec70c..e9f8a320 100644 --- a/lib/mdl/Thumb.js +++ b/lib/mdl/Thumb.js @@ -1,6 +1,4 @@ const React = require('react-native'); -const MKColor = require('../MKColor'); -const {getTheme} = require('../theme'); const { Component, @@ -21,12 +19,7 @@ const LOWEST_VALUE_THUMB_COLOR = 'white'; const THUMB_SCALE_RATIO = 1.3; // Width of the thumb border -const THUMB_BORDER_WIDTH = 2 - -// extra spacing enlarging the touchable area -const TRACK_EXTRA_MARGIN_V = 5; -const TRACK_EXTRA_MARGIN_H = 5; - +const THUMB_BORDER_WIDTH = 2; // ##
Thumb
// `Thumb` component of the [`Slider`](#Slider). @@ -35,7 +28,7 @@ class Thumb extends Component { super(props); this.x = 0; // current x-axis position - this._panResponder = {} + this._panResponder = {}; this._animatedLeft = new Animated.Value(0); this._animatedScale = new Animated.Value(1); this.state = { @@ -46,17 +39,17 @@ class Thumb extends Component { componentWillMount() { this._panResponder = PanResponder.create({ - onStartShouldSetPanResponder: () => true, - onStartShouldSetPanResponderCapture: () => true, - onMoveShouldSetPanResponder: () => true, - onMoveShouldSetPanResponderCapture: () => true, - onPanResponderTerminationRequest: () => true, - onShouldBlockNativeResponder: () => true, - - onPanResponderGrant: (evt) => { this.props.onGrant(this, evt) }, - onPanResponderMove: (evt) => { this.props.onMove(this, evt) }, - onPanResponderRelease: (evt) => { this.props.onEnd(this, evt) }, - onPanResponderTerminate: (evt) => { this.props.onEnd(this, evt) }, + onStartShouldSetPanResponder: () => true, + onStartShouldSetPanResponderCapture: () => true, + onMoveShouldSetPanResponder: () => true, + onMoveShouldSetPanResponderCapture: () => true, + onPanResponderTerminationRequest: () => true, + onShouldBlockNativeResponder: () => true, + + onPanResponderGrant: (evt) => { this.props.onGrant(this, evt); }, + onPanResponderMove: (evt) => { this.props.onMove(this, evt); }, + onPanResponderRelease: (evt) => { this.props.onEnd(this, evt); }, + onPanResponderTerminate: (evt) => { this.props.onEnd(this, evt); }, }); this._onRadiiUpdate(this.props); @@ -66,14 +59,14 @@ class Thumb extends Component { }); } - componentWillReceiveProps(nextProps) { - this._onRadiiUpdate(nextProps); - } - componentDidMount() { this._animatedLeft.addListener(this._getOnSliding()); } + componentWillReceiveProps(nextProps) { + this._onRadiiUpdate(nextProps); + } + componentWillUnmount() { this._animatedLeft.removeAllListeners(); } @@ -92,7 +85,7 @@ class Thumb extends Component { // on sliding of the thumb // `value` - the `left` of the thumb, relative to the container - return ({value}) => { + return ({ value }) => { // convert to value relative to the track const x = value + this._containerRadii - this.props.trackMarginH; @@ -153,7 +146,8 @@ class Thumb extends Component { // Rendering the `Thumb` render() { return ( - - + + /> ); } } +Thumb.propTypes = { + // [RN.View Props](https://facebook.github.io/react-native/docs/view.html#props)... + ...View.propTypes, + + onGrant: PropTypes.func, + + onMove: PropTypes.func, + + onEnd: PropTypes.func, + + disabledColor: PropTypes.string, + + enabledColor: PropTypes.string, + + trackSize: PropTypes.number, + + trackMarginH: PropTypes.number, + + style: PropTypes.object, +}; module.exports = Thumb; diff --git a/lib/mdl/index.js b/lib/mdl/index.js index 12497672..5279e365 100644 --- a/lib/mdl/index.js +++ b/lib/mdl/index.js @@ -7,7 +7,7 @@ exports.Textfield = require('./Textfield'); exports.Progress = require('./Progress'); exports.Spinner = require('./Spinner'); exports.Slider = require('./Slider'); -exports.DualSlider = require('./DualSlider'); +exports.RangeSlider = require('./RangeSlider'); exports.Button = require('./Button'); exports.Ripple = require('./Ripple'); exports.Card = require('./cards/container'); From c7257487c4d07b47183b22b2222f9f35e47872d6 Mon Sep 17 00:00:00 2001 From: awaidmann Date: Fri, 29 Jan 2016 14:36:42 -0800 Subject: [PATCH 3/6] Update README to include Range Slider Still need to add demo gif file for Range Slider --- README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/README.md b/README.md index a651c9db..ebed78a1 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,7 @@ Have fun! :metal: - [Progress bar](#progress-bar) - [Spinner](#spinner) - [Sliders](#sliders) + - [Range Slider](#range-slider) - [Textfields](#text-fields) - [Toggles](#toggles) - [Checkbox](#checkbox) @@ -258,6 +259,29 @@ const SliderWithValue = mdl.Slider.slider() 👉 [props reference][slider-props-doc] and [example code][slider-sample] +### Range Slider + +```jsx + +… +const SliderWithRange = mdl.RangeSlider.slider() + .withStyle(styles.slider) + .withMin(10) + .withMax(100) + .withSliderMin(30) + .withSliderMax(50) + .build(); +… + this.setState({ + min: curValue.min, + max: curValue.max, + }) + } + /> +``` + [mdl-slider]: http://www.getmdl.io/components/index.html#sliders-section [slider-demo]: https://cloud.githubusercontent.com/assets/390805/10123318/6c502e6e-6569-11e5-924a-62c8b850511c.gif [slider-sample]: https://github.com/xinthink/rnmk-demo/blob/master/app/sliders.js From ed9acb02434af388705e134d16bbddbb42ffb319 Mon Sep 17 00:00:00 2001 From: awaidmann Date: Mon, 1 Feb 2016 18:48:19 -0800 Subject: [PATCH 4/6] Fix lingering formatting and untested issues. --- lib/mdl/RangeSlider.js | 119 +++++++++++++++++++++++++---------------- lib/mdl/Thumb.js | 69 +++++++++++++++++++++--- 2 files changed, 135 insertions(+), 53 deletions(-) diff --git a/lib/mdl/RangeSlider.js b/lib/mdl/RangeSlider.js index 79b92ab1..fc4cf6cb 100644 --- a/lib/mdl/RangeSlider.js +++ b/lib/mdl/RangeSlider.js @@ -1,12 +1,11 @@ // -// MDL style RangeSlider component. +// RangeSlider component. // -// - @see [MDL Slider](http://www.getmdl.io/components/index.html#sliders-section) // - [Props](#props) // - [Defaults](#defaults) // - [Built-in builders](#builders) // -// Created by ywu on 15/8/23. +// Created by awaidman on 16/1/21. // const React = require('react-native'); @@ -35,21 +34,19 @@ const THUMB_BORDER_WIDTH = 2; const TRACK_EXTRA_MARGIN_V = 5; const TRACK_EXTRA_MARGIN_H = 5; - // ##
Slider
class RangeSlider extends Component { constructor(props) { super(props); this._range = { - minRange: 0, - maxRange: 0, + min: 0, + max: 0, }; - // Added to fix a bug where the sliders get stuck on top of each other this._overriddenThumb = undefined; this._trackTotalLength = 0; - this._lowerTrackLength = new Animated.Value(this._range.maxRange - this._range.minRange); - this._lowerTrackMin = new Animated.Value(this._range.minRange); + this._lowerTrackLength = new Animated.Value(this._range.max - this._range.min); + this._lowerTrackMin = new Animated.Value(this._range.min); } componentWillMount() { @@ -60,79 +57,76 @@ class RangeSlider extends Component { this._onThumbRadiiUpdate(nextProps); } - get sliderMin() { return this._toSliderScale(this._range.minRange); } - get sliderMax() { return this._toSliderScale(this._range.maxRange); } - set sliderMin(sliderMin) { - this.props.sliderMin = sliderMin; - this._setRange(); - } - set sliderMax(sliderMax) { - this.props.sliderMax = sliderMax; - this._setRange(); - } - _onTrackLayout({ nativeEvent: { layout: { width } } }) { if (this._trackTotalLength !== width) { this._trackTotalLength = width; - this._setRange(); + this._setRange({ min: this.props.minValue, max: this.props.maxValue }); this._updateValue(this._range); } } - _setRange() { - const min2Scale = this._toPixelScale(this.props.sliderMin); - const max2Scale = this._toPixelScale(this.props.sliderMax); + // Throw error if preset ranges are invalid + _setRange(range) { + const min2Scale = this._toPixelScale(range.min); + const max2Scale = this._toPixelScale(range.max); const minBounds = this._toPixelScale(this.props.min); const maxBounds = this._toPixelScale(this.props.max); if (min2Scale > max2Scale) { - const msg = 'Minimum slider value: ' + this.props.sliderMin + - ' is greater than max value: ' + this.props.sliderMax; + const msg = 'Minimum slider value: ' + range.min + + ' is greater than max value: ' + range.max; throw msg; } if (min2Scale < minBounds || min2Scale > maxBounds) { - const msg = 'Minimum slider value: ' + this.props.sliderMin + + const msg = 'Minimum slider value: ' + range.min + ' exceeds bounds: ' + this.props.min + '-' + this.props.max; throw msg; } if (max2Scale < minBounds || max2Scale > maxBounds) { - const msg = 'Maximum slider value: ' + this.props.sliderMax + + const msg = 'Maximum slider value: ' + range.max + ' exceeds bounds: ' + this.props.min + '-' + this.props.max; throw msg; } this._range = { - minRange: min2Scale ? min2Scale : 0, - maxRange: max2Scale ? max2Scale : 0, + min: min2Scale ? min2Scale : 0, + max: max2Scale ? max2Scale : 0, }; + + return this._range; } + // Scale global xy coordinate values to track values _toSliderScale(value) { const trackToRange = (this.props.max - this.props.min) / this._trackTotalLength; return (value * trackToRange) + this.props.min; } + // Scale track values to global xy coordinate system _toPixelScale(value) { const rangeToTrack = this._trackTotalLength / (this.props.max - this.props.min); - return value * rangeToTrack - this.props.min; + return (value - this.props.min) * rangeToTrack; } + // Set values for thumb components for user touch events _internalSetValue(ref, value) { - const target = ref === this.refs.minRange ? 'minRange' : 'maxRange'; + const target = ref === this.refs.minRange ? 'min' : 'max'; this._range[target] = value; this._emitChange(); } + // Send changed values to onChange callback _emitChange() { if (this.props.onChange) { this.props.onChange({ - min: this._toSliderScale(this._range.minRange), - max: this._toSliderScale(this._range.maxRange), + min: this._toSliderScale(this._range.min), + max: this._toSliderScale(this._range.max), }); } } + // Internal update of ranges. Values should be to "Pixel Scale" _updateValue(values) { if (!this._trackTotalLength) { return; @@ -141,13 +135,14 @@ class RangeSlider extends Component { const lthumb = this.refs.minRange; const rthumb = this.refs.maxRange; - this._moveThumb(lthumb, values.minRange); - lthumb.confirmMoveTo(values.minRange); + this._moveThumb(lthumb, values.min); + lthumb.confirmMoveTo(values.min); - this._moveThumb(rthumb, values.maxRange); - rthumb.confirmMoveTo(values.minRange); + this._moveThumb(rthumb, values.max); + rthumb.confirmMoveTo(values.max); } + // Ensure thumbs do not cross each other or track boundaries _validateMove(dx, trackOriginX, trackWidth, ref) { const x = dx - trackOriginX; @@ -182,6 +177,7 @@ class RangeSlider extends Component { } } + // Respond to Grant and Move touch gestures _updateValueByTouch(ref, evt) { const ovrRef = this._overriddenThumb ? this._overriddenThumb : ref; @@ -193,21 +189,23 @@ class RangeSlider extends Component { }); } + // Induce smooth animation to move each thumb component _moveThumb(ref, x) { ref.moveTo(x); Animated.parallel([ Animated.timing(this._lowerTrackMin, { - toValue: this._range.minRange, + toValue: this._range.min, duration: 0, }), Animated.timing(this._lowerTrackLength, { - toValue: this._range.maxRange - this._range.minRange, + toValue: this._range.max - this._range.min, duration: 0, }), ]).start(); } + // Respond to both cancelled and finished gestures _endMove(ref, evt) { const ovrRef = this._overriddenThumb ? this._overriddenThumb : ref; @@ -270,7 +268,6 @@ class RangeSlider extends Component { Props RangeSlider.propTypes = { // [RN.View Props](https://facebook.github.io/react-native/docs/view.html#props)... @@ -316,11 +343,11 @@ RangeSlider.propTypes = { // Maximum value of the range, default is `100` max: PropTypes.number, - // Min preset/user-set slider value - sliderMin: PropTypes.number, + // Minimum predefined value for left hand thumb + minValue: PropTypes.number, - // Max preset/user-set slider value - sliderMax: PropTypes.number, + // Maximum predefined value for right hand thumb + maxValue: PropTypes.number, // The thickness of the RangeSlider track trackSize: PropTypes.number, diff --git a/lib/mdl/Thumb.js b/lib/mdl/Thumb.js index e9f8a320..f92c5a1a 100644 --- a/lib/mdl/Thumb.js +++ b/lib/mdl/Thumb.js @@ -1,4 +1,15 @@ +// +// RangeSlider component. +// +// - [Props](#props) +// - [Defaults](#defaults) +// - [Built-in builders](#builders) +// +// Created by awaidman on 16/1/21. +// + const React = require('react-native'); +const MKColor = require('../MKColor'); const { Component, @@ -21,6 +32,9 @@ const THUMB_SCALE_RATIO = 1.3; // Width of the thumb border const THUMB_BORDER_WIDTH = 2; +// extra spacing enlarging the touchable area +const TRACK_EXTRA_MARGIN_H = 5; + // ##
Thumb
// `Thumb` component of the [`Slider`](#Slider). class Thumb extends Component { @@ -28,6 +42,8 @@ class Thumb extends Component { super(props); this.x = 0; // current x-axis position + this._trackMarginH = (props.radius + THUMB_BORDER_WIDTH) * THUMB_SCALE_RATIO + + TRACK_EXTRA_MARGIN_H; this._panResponder = {}; this._animatedLeft = new Animated.Value(0); this._animatedScale = new Animated.Value(1); @@ -55,7 +71,6 @@ class Thumb extends Component { this._onRadiiUpdate(this.props); this.setState({ borderColor: this.props.disabledColor, - borderWidth: this.props.trackSize, }); } @@ -87,7 +102,7 @@ class Thumb extends Component { // `value` - the `left` of the thumb, relative to the container return ({ value }) => { // convert to value relative to the track - const x = value + this._containerRadii - this.props.trackMarginH; + const x = value + this._containerRadii - this._trackMarginH; if (prevX <= 0 && x > 0) { // leaving the lowest value, scale up the thumb @@ -105,7 +120,7 @@ class Thumb extends Component { // `x` - target position, relative to the track moveTo(x) { this.x = x; - const x0 = this.x + this.props.trackMarginH; + const x0 = this.x + this._trackMarginH; Animated.parallel([ Animated.timing(this._animatedScale, { @@ -182,20 +197,60 @@ Thumb.propTypes = { // [RN.View Props](https://facebook.github.io/react-native/docs/view.html#props)... ...View.propTypes, + // Callback to handle onPanResponderGrant gesture onGrant: PropTypes.func, + // Callback to handle onPanResponderMove gesture onMove: PropTypes.func, + // Callback to handle onPanResponderRelease/Terminate gesture onEnd: PropTypes.func, + // Color when thumb has no value disabledColor: PropTypes.string, + // Color when thumb has value enabledColor: PropTypes.string, - trackSize: PropTypes.number, - - trackMarginH: PropTypes.number, + // Radius of thumb component + radius: PropTypes.number, +}; - style: PropTypes.object, +// ##
Defaults
+Thumb.defaultProps = { + radius: 6, + disabledColor: DEFAULT_UPPER_TRACK_COLOR, }; + +// -------------------------- +// Builder +// +const { + Builder, +} = require('../builder'); + +// +// ## Thumb builder +// +class ThumbBuilder extends Builder { + build() { + const BuiltSlider = class extends Thumb {}; + BuiltSlider.defaultProps = Object.assign({}, Thumb.defaultProps, this.toProps()); + return BuiltSlider; + } +} + +// ---------- +// ##
Built-in builders
+// +function thumb() { + return new ThumbBuilder().withBackgroundColor(MKColor.Transparent); +} + +// define builder method for each prop +ThumbBuilder.defineProps(Thumb.propTypes); + +// ## Public interface module.exports = Thumb; +Thumb.thumb = thumb; +Thumb.Builder = ThumbBuilder; From f4186601e3cf2100ca6af23057b35cfca4602b34 Mon Sep 17 00:00:00 2001 From: awaidmann Date: Tue, 2 Feb 2016 12:31:50 -0800 Subject: [PATCH 5/6] Add .gif for Range Slider --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index ebed78a1..62a4087d 100644 --- a/README.md +++ b/README.md @@ -260,6 +260,7 @@ const SliderWithValue = mdl.Slider.slider() 👉 [props reference][slider-props-doc] and [example code][slider-sample] ### Range Slider +![range-slider-demo] ```jsx @@ -284,6 +285,7 @@ const SliderWithRange = mdl.RangeSlider.slider() [mdl-slider]: http://www.getmdl.io/components/index.html#sliders-section [slider-demo]: https://cloud.githubusercontent.com/assets/390805/10123318/6c502e6e-6569-11e5-924a-62c8b850511c.gif +[range-slider-demo]: https://cloud.githubusercontent.com/assets/16245422/12763284/63a2dafc-c9a8-11e5-8fde-37b6f42a60c2.gif [slider-sample]: https://github.com/xinthink/rnmk-demo/blob/master/app/sliders.js [slider-props-doc]: http://www.xinthink.com/react-native-material-kit/docs/lib/mdl/Slider.html#props From c3c5d177c3b61b8863243ed992e3a69d78c08d5a Mon Sep 17 00:00:00 2001 From: awaidmann Date: Tue, 2 Feb 2016 12:34:22 -0800 Subject: [PATCH 6/6] Update code block to reflect renaming of params --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 62a4087d..47a970b7 100644 --- a/README.md +++ b/README.md @@ -269,8 +269,8 @@ const SliderWithRange = mdl.RangeSlider.slider() .withStyle(styles.slider) .withMin(10) .withMax(100) - .withSliderMin(30) - .withSliderMax(50) + .withMinValue(30) + .withMaxValue(50) .build(); …