├── LICENSE ├── README.md ├── demo.gif ├── package-lock.json ├── package.json └── src ├── index.d.ts ├── index.js └── styles.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Vitor Silva 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Native Motion Slider 2 | 3 | A JavaScript slider component for React Native (iOS and Android). 4 | React Native Motion Slider is a high-quality slider with a stunning UI / UX. 5 | 6 | ![demo](./demo.gif) 7 | 8 | ## Installation 9 | 10 | ```bash 11 | npm install --save react-native-motion-slider 12 | ``` 13 | 14 | 15 | ## Usage 16 | 17 | ```javascript 18 | import MotionSlider from 'react-native-motion-slider'; 19 | ``` 20 | 21 | ```javascript 22 | 23 | console.log(value)} 32 | onPressIn={() => console.log('Pressed in')} 33 | onPressOut={() => console.log('Pressed out')} 34 | onDrag={() => console.log('Dragging')} 35 | /> 36 | 37 | ``` 38 | 39 | ## API 40 | ### Properties 41 | 42 | | **Property** | **Description** | **Type** | 43 | |-----------------------|------------------------------------------------------------------|----------| 44 | | width | Slider width. | number | 45 | | height | Slider height. | number | 46 | | borderRadius | Slider border radius. | number | 47 | | backgroundColor | String array containing the slider colors. By default it has only one element. | [string] | 48 | | decimalPlaces | Decimal places to display on min, max and value elements. | number | 49 | | title | Slider title. | string | 50 | | titleColor | Slider title color. | string | 51 | | titleStyle | Slider title custom style. | StyleSheet | 52 | | min | Minimum value of the slider. | number | 53 | | max | Maximum value of the slider. | number | 54 | | value | Current slider value. | number | 55 | | units | Value units (e.g. 'km'). | string | 56 | | minColor | Color of min text element. | string | 57 | | maxColor | Color of max text element. | string | 58 | | valueColor | Color of value text element. | string | 59 | | valueBackgroundColor | Color of value container's background color. By default this color inherits the slider's background color. | string | 60 | | fontSize | Font size for min, max and value text elements. | number | 61 | | fontWeight | Font weight for min, max and value text elements. | string | 62 | | fontFamily | Font family for min, max and value text elements. | string | 63 | 64 | ### Function Properties 65 | 66 | | **Property** | **Input** | Notes | 67 | |-----------------------|--------------|-----------------------------------------------------------| 68 | | onValueChanged | Slider value | Use this to update catch slider value on parent component | 69 | | onPressIn | | | 70 | | onPressOut | | | 71 | | onDrag | | | 72 | 73 | ## Acknowledgement 74 | 75 | * [Virgil Pana](https://dribbble.com/shots/3868232-ios-Fluid-Slider-ui-ux), who designed the concept and inspired me to create this component. I recommend checking his works. 76 | 77 | ## License 78 | 79 | MIT. 80 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VitorCodes/react-native-motion-slider/76d46ee8f0abb4964d63737bc6d665e4cd3fc02f/demo.gif -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-motion-slider", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-motion-slider", 3 | "version": "1.0.3", 4 | "description": "High-quality slider with a stunning UI / UX.", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/VitorCodes/react-native-motion-slider.git" 12 | }, 13 | "keywords": [ 14 | "react native", 15 | "slider", 16 | "fluid", 17 | "awesome", 18 | "animated", 19 | "motion", 20 | "multi color", 21 | "component" 22 | ], 23 | "author": "Vitor Silva ", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/VitorCodes/react-native-motion-slider/issues" 27 | }, 28 | "homepage": "https://github.com/VitorCodes/react-native-motion-slider#readme" 29 | } 30 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Vitor Silva 3 | */ 4 | import * as React from 'react'; 5 | 6 | type BackgroundColor = { 7 | [index: number]: string; 8 | } 9 | 10 | export interface SliderProperties { 11 | /** 12 | * @param {number} width Slider width. 13 | */ 14 | width: number, 15 | 16 | /** 17 | * @param {number} height Slider height. 18 | */ 19 | height: number, 20 | 21 | /** 22 | * @param {number} borderRadius Slider border radius. 23 | */ 24 | borderRadius: number, 25 | 26 | /** 27 | * @param {BackgroundColor} backgroundColor String array containing the slider colors. By default it has only one element. 28 | */ 29 | backgroundColor: BackgroundColor, 30 | 31 | /** 32 | * @param {number} decimalPlaces Decimal places to display on min, max and value elements. 33 | */ 34 | decimalPlaces: number, 35 | 36 | /** 37 | * @param {string} title Slider title. 38 | */ 39 | title: string, 40 | 41 | /** 42 | * @param {string} titleColor Slider title color. 43 | */ 44 | titleColor: string, 45 | 46 | /** 47 | * @param {object} titleStyle Slider title custom style. 48 | */ 49 | titleStyle: object, 50 | 51 | /** 52 | * @param {number} min Minimum value of the slider. 53 | */ 54 | min: number, 55 | 56 | /** 57 | * @param {number} max Maximum value of the slider. 58 | */ 59 | max: number, 60 | 61 | /** 62 | * @param {number} value Current slider value. 63 | */ 64 | value: number, 65 | 66 | /** 67 | * @param {string} units Value units (e.g. 'km'). 68 | */ 69 | units: string, 70 | 71 | /** 72 | * @param {string} minColor Color of min text element. 73 | */ 74 | minColor: string, 75 | 76 | /** 77 | * @param {string} maxColor Color of max text element. 78 | */ 79 | maxColor: string, 80 | 81 | /** 82 | * @param {string} valueColor Color of value text element. 83 | */ 84 | valueColor: string, 85 | 86 | /** 87 | * @param {string} valueBackgroundColor Color of value container's background color. By default this color inherits the slider's background color. 88 | */ 89 | valueBackgroundColor: string, 90 | 91 | /** 92 | * @param {number} fontSize Font size for min, max and value text elements. 93 | */ 94 | fontSize: number, 95 | 96 | /** 97 | * @param {string} fontWeight Font weight for min, max and value text elements. 98 | */ 99 | fontWeight: string, 100 | 101 | /** 102 | * @param {string} fontFamily Font family for min, max and value text elements. 103 | */ 104 | fontFamily: string, 105 | 106 | /** 107 | * @param {Function} onValueChanged Function to execute everytime the slider value changes. 108 | */ 109 | onValueChanged: Function, 110 | 111 | /** 112 | * @param {Function} onPressIn Function to execute everytime the user presses in the slider. 113 | */ 114 | onPressIn: Function, 115 | 116 | /** 117 | * @param {Function} onPressOut Function to execute everytime the presses out the slider. 118 | */ 119 | onPressOut: Function, 120 | 121 | /** 122 | * @param {Function} onDrag Function to execute everytime the user drags the finger inside the slider. 123 | */ 124 | onDrag: Function, 125 | } 126 | 127 | export default class Slider extends React.Component {} -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Vitor Silva - https://github.com/VitorCodes 3 | */ 4 | import React, { Component } from 'react'; 5 | import { View, Text, Dimensions, Animated, Easing, PanResponder } from 'react-native'; 6 | import Style from './styles'; 7 | 8 | const { 9 | width: Width = width, 10 | height: Height = height 11 | } = Dimensions.get('window'); 12 | 13 | class Slider extends Component { 14 | static defaultProps = { 15 | width: Math.floor(Width * 0.85), 16 | height: Math.floor(Height * 0.07), 17 | borderRadius: 10, 18 | backgroundColor: ['#2196F3'], 19 | decimalPlaces: 0, 20 | title: '', 21 | titleColor: null, 22 | titleStyle: {}, 23 | min: 0, 24 | max: 50, 25 | value: 100, 26 | units: '', 27 | minColor: '#fff', 28 | maxColor: '#fff', 29 | valueColor: null, 30 | valueBackgroundColor: '#fff', 31 | fontSize: 10, 32 | fontWeight: 'normal', 33 | fontFamily: null, 34 | onValueChanged: () => null, 35 | onPressIn: () => null, 36 | onPressOut: () => null, 37 | onDrag: () => null, 38 | }; 39 | 40 | constructor(props) { 41 | super(props); 42 | 43 | let { 44 | // Bar 45 | width, height, borderRadius, backgroundColor, 46 | // Min 47 | minColor, 48 | // Max 49 | maxColor, 50 | // Value 51 | value, 52 | valueBackgroundColor, 53 | decimalPlaces, 54 | // Min, Max and Value 55 | fontSize, fontFamily, fontWeight, 56 | } = props; 57 | 58 | this.titleStyle = { 59 | marginHorizontal: Math.floor(width * 0.05), 60 | marginVertical: Math.floor(height * 0.3), 61 | }; 62 | 63 | this.barStyle = { 64 | width, 65 | height, 66 | borderRadius, 67 | backgroundColor 68 | }; 69 | 70 | this.minStyle = { 71 | color: minColor, 72 | fontSize, 73 | fontFamily, 74 | fontWeight, 75 | width: height, 76 | marginHorizontal: borderRadius, 77 | }; 78 | 79 | this.maxStyle = { 80 | color: maxColor, 81 | fontSize, 82 | fontFamily, 83 | fontWeight, 84 | width: height, 85 | marginHorizontal: borderRadius 86 | }; 87 | 88 | this.valueStyle = { 89 | fontSize, 90 | fontFamily, 91 | fontWeight 92 | }; 93 | 94 | this.valueOuterContainerStyle = { 95 | backgroundColor, 96 | width: height, 97 | height, 98 | borderRadius: Math.floor(height / 2), 99 | }; 100 | 101 | this.valueInnerContainerStyle = { 102 | backgroundColor: valueBackgroundColor, 103 | width: height - Math.floor(height * 0.2), 104 | height: height - Math.floor(height * 0.2), 105 | borderRadius: Math.floor((height - Math.floor(height * 0.2)) / 2), 106 | }; 107 | 108 | this.minX = borderRadius + Math.floor(height / 2); 109 | this.maxX = width - borderRadius - Math.floor(height / 2); 110 | this.panResponder = this.setPanResponder(); 111 | this.gradient = this.setGradient(); 112 | 113 | this.state = { 114 | value: value.toFixed(decimalPlaces), 115 | animTranslateY: new Animated.Value(0), 116 | animScale: new Animated.Value(1), 117 | posX: this.xOfValue(value), 118 | pressed: false 119 | }; 120 | } 121 | 122 | xOfValue(y) { 123 | let { max, min } = this.props; 124 | let m = Number(((min - max) / (this.minX - this.maxX)).toFixed(20)); 125 | let x = (y - min) / m; 126 | 127 | return Math.floor(x + this.minX); 128 | } 129 | 130 | calculateValue(posX) { 131 | let { max, min, decimalPlaces, onDrag, onValueChanged } = this.props; 132 | 133 | if(posX < this.minX) posX = this.minX; 134 | if(posX > this.maxX) posX = this.maxX; 135 | 136 | let m = Number(((min - max) / (this.minX - this.maxX)).toFixed(20)); 137 | let x = posX; 138 | let value = ((m * x) - (this.minX * m) + min).toFixed(decimalPlaces); 139 | 140 | this.setState({ value }); 141 | onDrag(); 142 | onValueChanged(value); 143 | } 144 | 145 | setGradient() { 146 | let { backgroundColor, min, max } = this.props; 147 | 148 | // Has only one color -> outputRange will use the same values 149 | if(backgroundColor.length == 1) { 150 | return { 151 | inputRange: [min, max], 152 | outputRange: [backgroundColor[0], backgroundColor[0]] 153 | }; 154 | }; 155 | 156 | // Has two or more colors -> defines the output range with backgroundColor values and calculates middleRange values for inputRange 157 | let gradient = { 158 | inputRange: [min, max], 159 | outputRange: backgroundColor 160 | }; 161 | 162 | let currentRange = min; 163 | let middleRanges = []; 164 | 165 | for(var i = 0; i < backgroundColor.length - 2; i++) { 166 | middleRanges.push(currentRange + Math.floor((1 / backgroundColor.length) * 100)); 167 | currentRange += Math.floor((1 / backgroundColor.length) * 100); 168 | } 169 | 170 | gradient.inputRange.splice(1, 0, ...middleRanges); 171 | return gradient; 172 | } 173 | 174 | setPanResponder() { 175 | return PanResponder.create({ 176 | onStartShouldSetPanResponder: (evt, gestureState) => true, 177 | onStartShouldSetPanResponderCapture: (evt, gestureState) => true, 178 | onMoveShouldSetPanResponder: (evt, gestureState) => true, 179 | onMoveShouldSetPanResponderCapture: (evt, gestureState) => true, 180 | onPanResponderTerminationRequest: (evt, gestureState) => true, 181 | onPanResponderGrant: (evt, gestureState) => { 182 | let posX = Math.floor(evt.nativeEvent.locationX); 183 | this.calculateValue(posX); 184 | this.onPressIn(posX); 185 | }, 186 | onPanResponderMove: (evt, gestureState) => { 187 | let posX = Math.floor(evt.nativeEvent.locationX); 188 | let posY = Math.floor(evt.nativeEvent.locationY); 189 | 190 | if(posY < 0 || posY > this.props.height) return; 191 | 192 | this.calculateValue(posX); 193 | this.setState({ posX }); 194 | }, 195 | onPanResponderRelease: (evt, gestureState) => { 196 | this.onPressOut(); 197 | }, 198 | onPanResponderTerminate: (evt, gestureState) => { 199 | this.onPressOut(); 200 | }, 201 | }); 202 | } 203 | 204 | onPressIn(posX) { 205 | let { height, onPressIn, onValueChanged } = this.props; 206 | let { animScale, animTranslateY, pressed, value } = this.state; 207 | 208 | if(pressed) return; 209 | 210 | this.setState({ 211 | pressed: true, 212 | posX 213 | }, () => { 214 | Animated.parallel([ 215 | Animated.timing(animScale, { 216 | toValue: 1.4, 217 | duration: 200, 218 | }), 219 | Animated.spring(animTranslateY, { 220 | toValue: -(height - Math.floor(height * 0.3)), 221 | bounciness: 12, 222 | }) 223 | ]).start(); 224 | 225 | onPressIn(); 226 | onValueChanged(value); 227 | }); 228 | } 229 | 230 | onPressOut() { 231 | let { animScale, animTranslateY, pressed, value } = this.state; 232 | let { onPressOut, onValueChanged } = this.props; 233 | 234 | if(!pressed) return; 235 | 236 | this.setState({ 237 | pressed: false 238 | }, () => { 239 | Animated.parallel([ 240 | Animated.timing(animScale, { 241 | toValue: 1, 242 | duration: 300, 243 | }), 244 | Animated.timing(animTranslateY, { 245 | toValue: 0, 246 | duration: 300, 247 | easing: Easing.out(Easing.exp), 248 | }), 249 | ]).start(); 250 | 251 | onPressOut(); 252 | onValueChanged(value); 253 | }); 254 | } 255 | 256 | getCurrentColor() { 257 | return new Animated.Value(Number(this.state.value)).interpolate({ 258 | inputRange: this.gradient.inputRange, 259 | outputRange: this.gradient.outputRange 260 | }); 261 | } 262 | 263 | barAnimationStyle() { 264 | let { value, animScale } = this.state; 265 | 266 | return { 267 | transform: [{ 268 | scale: animScale.interpolate({ 269 | inputRange: [1, 1.4], 270 | outputRange: [0.9, 1] 271 | }) 272 | }], 273 | backgroundColor: this.getCurrentColor() 274 | }; 275 | } 276 | 277 | titleAnimationStyle() { 278 | let { animScale } = this.state; 279 | let { titleColor } = this.props; 280 | 281 | return { 282 | opacity: animScale.interpolate({ 283 | inputRange: [1, 1.1, 1.4], 284 | outputRange: [1, 0.2, 0] 285 | }), 286 | color: titleColor || this.getCurrentColor() 287 | }; 288 | } 289 | 290 | renderTitle() { 291 | let { title, titleStyle } = this.props; 292 | 293 | if(!title && title.length === 0) return; 294 | 295 | return ( 296 | 302 | {title} 303 | 304 | ); 305 | } 306 | 307 | renderValue() { 308 | let { value, posX, animTranslateY, animScale } = this.state; 309 | let { width, height, borderRadius, units } = this.props; 310 | let left = posX - Math.floor(height / 2); 311 | 312 | if(left <= borderRadius) { 313 | left = borderRadius; 314 | } 315 | 316 | if(left >= width - borderRadius - height) { 317 | left = width - borderRadius - height; 318 | } 319 | 320 | return ( 321 | 328 | 337 | 338 | {`${value}${units}`} 339 | 340 | 341 | 342 | ); 343 | } 344 | 345 | render() { 346 | let { min, max, decimalPlaces, units } = this.props; 347 | 348 | return ( 349 | 350 | {this.renderTitle()} 351 | 356 | {`${min.toFixed(decimalPlaces)}${units}`} 357 | {`${max.toFixed(decimalPlaces)}${units}`} 358 | {this.renderValue()} 359 | 360 | 361 | 362 | ); 363 | } 364 | } 365 | 366 | export default Slider; -------------------------------------------------------------------------------- /src/styles.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Vitor Silva - https://github.com/VitorCodes 3 | */ 4 | import { StyleSheet } from 'react-native'; 5 | 6 | export default StyleSheet.create({ 7 | bar: { 8 | flexDirection: 'row', 9 | justifyContent: 'space-between', 10 | alignItems: 'center', 11 | }, 12 | 13 | text: { 14 | fontSize: 14, 15 | textAlign: 'center' 16 | }, 17 | 18 | min: { 19 | textAlign: 'center', 20 | fontWeight: 'bold', 21 | }, 22 | 23 | max: { 24 | textAlign: 'center', 25 | fontWeight: 'bold', 26 | }, 27 | 28 | valueContainer: { 29 | position: 'absolute', 30 | alignItems: 'center', 31 | justifyContent: 'center', 32 | }, 33 | 34 | valueOuterContainer: { 35 | alignItems: 'center', 36 | justifyContent: 'center', 37 | }, 38 | 39 | valueInnerContainer: { 40 | alignItems: 'center', 41 | justifyContent: 'center', 42 | }, 43 | 44 | value: { 45 | textAlign: 'center', 46 | fontWeight: 'bold', 47 | }, 48 | 49 | touchableArea: { 50 | position: 'absolute', 51 | width: '100%', 52 | height: '100%', 53 | backgroundColor: 'transparent', 54 | }, 55 | }); --------------------------------------------------------------------------------