├── 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 | 
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 | });
--------------------------------------------------------------------------------