├── 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 | [![React Native Dot inversion slider Youtube tutorial](react-native-dot-inversion-animation.gif)](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 | 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 | --------------------------------------------------------------------------------