├── assets
├── icon.png
├── favicon.png
└── splash.png
├── babel.config.js
├── react-native-dot-inversion-animation.gif
├── .expo-shared
└── assets.json
├── .gitignore
├── app.json
├── package.json
├── README.md
└── App.js
/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/catalinmiron/react-native-dot-inversion/HEAD/assets/icon.png
--------------------------------------------------------------------------------
/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/catalinmiron/react-native-dot-inversion/HEAD/assets/favicon.png
--------------------------------------------------------------------------------
/assets/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/catalinmiron/react-native-dot-inversion/HEAD/assets/splash.png
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function(api) {
2 | api.cache(true);
3 | return {
4 | presets: ['babel-preset-expo'],
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/react-native-dot-inversion-animation.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/catalinmiron/react-native-dot-inversion/HEAD/react-native-dot-inversion-animation.gif
--------------------------------------------------------------------------------
/.expo-shared/assets.json:
--------------------------------------------------------------------------------
1 | {
2 | "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true,
3 | "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true
4 | }
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/**/*
2 | .expo/*
3 | npm-debug.*
4 | *.jks
5 | *.p8
6 | *.p12
7 | *.key
8 | *.mobileprovision
9 | *.orig.*
10 | web-build/
11 |
12 | # macOS
13 | .DS_Store
14 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "react-native-dot-inversion-slider",
4 | "slug": "react-native-dot-inversion-slider",
5 | "version": "1.0.0",
6 | "orientation": "portrait",
7 | "icon": "./assets/icon.png",
8 | "splash": {
9 | "image": "./assets/splash.png",
10 | "resizeMode": "contain",
11 | "backgroundColor": "#ffffff"
12 | },
13 | "updates": {
14 | "fallbackToCacheTimeout": 0
15 | },
16 | "assetBundlePatterns": [
17 | "**/*"
18 | ],
19 | "ios": {
20 | "supportsTablet": true
21 | },
22 | "web": {
23 | "favicon": "./assets/favicon.png"
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "main": "node_modules/expo/AppEntry.js",
3 | "scripts": {
4 | "start": "expo start",
5 | "android": "expo start --android",
6 | "ios": "expo start --ios",
7 | "web": "expo start --web",
8 | "eject": "expo eject"
9 | },
10 | "dependencies": {
11 | "expo": "~38.0.8",
12 | "expo-status-bar": "^1.0.2",
13 | "react": "~16.11.0",
14 | "react-dom": "~16.11.0",
15 | "react-native": "https://github.com/expo/react-native/archive/sdk-38.0.2.tar.gz",
16 | "react-native-web": "~0.11.7",
17 | "@expo/vector-icons": "^10.0.0",
18 | "expo-constants": "~9.1.1"
19 | },
20 | "devDependencies": {
21 | "@babel/core": "^7.8.6",
22 | "babel-preset-expo": "~8.1.0"
23 | },
24 | "private": true
25 | }
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Native Dot inversion slider
2 |
3 | # Run on your device
4 |
5 | Snack: https://snack.expo.io/@catalinmiron/react-native-dot-inversion
6 |
7 | ### Youtube tutorial
8 |
9 |
10 | [](https://youtu.be/vQNg06Hf0MQ)
11 |
12 | In this video tutorial we'll learn how to create this mind blowing animation in React Native using perspective, scale and rotation and vanilla Animated Api from React Native.
13 | This is working cross platform thanks to expo, in other words you can use it either on web or in your React Native projects.
14 |
15 | - Inspiration: https://dribbble.com/shots/6654320-Animated-Onboarding-Screens
16 | - Expo: https://expo.io/
17 |
18 | You can find me on:
19 |
20 | - Github: http://github.com/catalinmiron
21 | - Twitter: http://twitter.com/mironcatalin
22 |
23 | Wanna give me a coffe?
24 |
25 | - Paypal: mironcatalin@gmail.com
26 |
--------------------------------------------------------------------------------
/App.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import {
3 | StatusBar,
4 | Dimensions,
5 | TouchableOpacity,
6 | Animated,
7 | Text,
8 | View,
9 | StyleSheet,
10 | } from 'react-native';
11 | import Constants from 'expo-constants';
12 | import { AntDesign } from '@expo/vector-icons';
13 | const { width } = Dimensions.get('window');
14 |
15 | const AnimatedAntDesign = Animated.createAnimatedComponent(AntDesign);
16 |
17 | const DURATION = 1000;
18 | const TEXT_DURATION = DURATION * 0.8;
19 |
20 | const quotes = [
21 | {
22 | quote:
23 | 'For the things we have to learn before we can do them, we learn by doing them.',
24 | author: 'Aristotle, The Nicomachean Ethics',
25 | },
26 | {
27 | quote: 'The fastest way to build an app.',
28 | author: 'The Expo Team',
29 | },
30 | {
31 | quote:
32 | 'The greatest glory in living lies not in never falling, but in rising every time we fall.',
33 | author: 'Nelson Mandela',
34 | },
35 | {
36 | quote: 'The way to get started is to quit talking and begin doing.',
37 | author: 'Walt Disney',
38 | },
39 | {
40 | quote:
41 | "Your time is limited, so don't waste it living someone else's life. Don't be trapped by dogma – which is living with the results of other people's thinking.",
42 | author: 'Steve Jobs',
43 | },
44 | {
45 | quote:
46 | 'If life were predictable it would cease to be life, and be without flavor.',
47 | author: 'Eleanor Roosevelt',
48 | },
49 | {
50 | quote:
51 | "If you look at what you have in life, you'll always have more. If you look at what you don't have in life, you'll never have enough.",
52 | author: 'Oprah Winfrey',
53 | },
54 | {
55 | quote:
56 | "If you set your goals ridiculously high and it's a failure, you will fail above everyone else's success.",
57 | author: 'James Cameron',
58 | },
59 | {
60 | quote: "Life is what happens when you're busy making other plans.",
61 | author: 'John Lennon',
62 | },
63 | ];
64 |
65 | const Circle = ({ onPress, index, quotes, animatedValue, animatedValue2 }) => {
66 | const { initialBgColor, nextBgColor, bgColor } = colors[index];
67 | const inputRange = [0, 0.001, 0.5, 0.501, 1];
68 | const backgroundColor = animatedValue2.interpolate({
69 | inputRange,
70 | outputRange: [
71 | initialBgColor,
72 | initialBgColor,
73 | initialBgColor,
74 | bgColor,
75 | bgColor,
76 | ],
77 | });
78 | const dotBgColor = animatedValue2.interpolate({
79 | inputRange: [0, 0.001, 0.5, 0.501, 0.9, 1],
80 | outputRange: [
81 | bgColor,
82 | bgColor,
83 | bgColor,
84 | initialBgColor,
85 | initialBgColor,
86 | nextBgColor,
87 | ],
88 | });
89 |
90 | return (
91 |
98 |
129 |
130 |
156 |
157 |
158 |
159 |
160 |
161 | );
162 | };
163 |
164 | /*
165 | initialBgColor -> Big background of the element
166 | bgColor -> initial circle bg color that will be the next slide initial BG Color
167 | nextBgColor -> next circle bg color after we fully transition the circle and this will be small again
168 | prev bgColor === next initialBgColor
169 | prev nextBgColor === next bgColor
170 | */
171 |
172 | const colors = [
173 | {
174 | initialBgColor: 'goldenrod',
175 | bgColor: '#222',
176 | nextBgColor: '#222',
177 | },
178 | {
179 | initialBgColor: 'goldenrod',
180 | bgColor: '#222',
181 | nextBgColor: 'yellowgreen',
182 | },
183 | {
184 | initialBgColor: '#222',
185 | bgColor: 'yellowgreen',
186 | nextBgColor: 'midnightblue',
187 | },
188 | {
189 | initialBgColor: 'yellowgreen',
190 | bgColor: 'midnightblue',
191 | nextBgColor: 'turquoise',
192 | },
193 | {
194 | initialBgColor: 'midnightblue',
195 | bgColor: 'turquoise',
196 | nextBgColor: 'goldenrod',
197 | },
198 | {
199 | initialBgColor: 'turquoise',
200 | bgColor: 'goldenrod',
201 | nextBgColor: '#222',
202 | },
203 | ];
204 |
205 | export default function App() {
206 | const animatedValue = React.useRef(new Animated.Value(0)).current;
207 | const animatedValue2 = React.useRef(new Animated.Value(0)).current;
208 | const sliderAnimatedValue = React.useRef(new Animated.Value(0)).current;
209 | const inputRange = [...Array(quotes.length).keys()];
210 | const [index, setIndex] = React.useState(0);
211 |
212 | const animate = (i) =>
213 | Animated.parallel([
214 | Animated.timing(sliderAnimatedValue, {
215 | toValue: i,
216 | duration: TEXT_DURATION,
217 | useNativeDriver: true,
218 | }),
219 | Animated.timing(animatedValue, {
220 | toValue: 1,
221 | duration: DURATION,
222 | useNativeDriver: true,
223 | }),
224 | Animated.timing(animatedValue2, {
225 | toValue: 1,
226 | duration: DURATION,
227 | useNativeDriver: false,
228 | }),
229 | ]);
230 |
231 | const onPress = () => {
232 | animatedValue.setValue(0);
233 | animatedValue2.setValue(0);
234 | animate((index + 1) % colors.length).start();
235 | setIndex((index + 1) % colors.length);
236 | };
237 |
238 | return (
239 |
240 |
241 |
248 | -i * width * 2),
256 | }),
257 | },
258 | ],
259 | opacity: sliderAnimatedValue.interpolate({
260 | inputRange: [...Array(quotes.length * 2 + 1).keys()].map(
261 | (i) => i / 2
262 | ),
263 | outputRange: [...Array(quotes.length * 2 + 1).keys()].map((i) =>
264 | i % 2 === 0 ? 1 : 0
265 | ),
266 | }),
267 | }}
268 | >
269 | {quotes.slice(0, colors.length).map(({ quote, author }, i) => {
270 | return (
271 |
272 |
275 | {quote}
276 |
277 |
289 | ______ {author}
290 |
291 |
292 | );
293 | })}
294 |
295 |
296 | );
297 | }
298 |
299 | const styles = StyleSheet.create({
300 | container: {
301 | flex: 1,
302 | justifyContent: 'flex-end',
303 | alignItems: 'center',
304 | paddingTop: Constants.statusBarHeight,
305 | padding: 8,
306 | paddingBottom: 50,
307 | },
308 | paragraph: {
309 | margin: 12,
310 | fontSize: 24,
311 | // fontWeight: 'bold',
312 | textAlign: 'center',
313 | fontFamily: 'Menlo',
314 | color: 'white',
315 | },
316 | button: {
317 | height: 100,
318 | width: 100,
319 | borderRadius: 50,
320 | justifyContent: 'center',
321 | alignItems: 'center',
322 | },
323 | circle: {
324 | backgroundColor: 'turquoise',
325 | width: 100,
326 | height: 100,
327 | borderRadius: 50,
328 | },
329 | });
330 |
--------------------------------------------------------------------------------