├── .gitignore ├── App.jsx ├── README.md ├── app.json ├── assets ├── adaptive-icon.png ├── favicon.png ├── icon.png └── splash.png ├── babel.config.js ├── package.json └── src ├── BottomSheet.jsx ├── GeoBar.jsx ├── Icon.jsx ├── NavBar.jsx ├── Overlay.jsx ├── PicturesCarousel.jsx └── SearchBar.jsx /.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 | 15 | -------------------------------------------------------------------------------- /App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | SafeAreaView, 4 | StatusBar, 5 | StyleSheet, 6 | useWindowDimensions, 7 | View, 8 | } from 'react-native'; 9 | import MapView from 'react-native-maps'; 10 | import { useSharedValue } from 'react-native-reanimated'; 11 | import SearchBar from './src/SearchBar'; 12 | import Overlay from './src/Overlay'; 13 | import NavBar from './src/NavBar'; 14 | import GeoBar from './src/GeoBar'; 15 | import BottomSheet from './src/BottomSheet'; 16 | import PicturesCarousel from './src/PicturesCarousel'; 17 | 18 | export default function App() { 19 | const { width, height } = useWindowDimensions(); 20 | 21 | const y = useSharedValue(0); 22 | 23 | return ( 24 | 25 | 26 | 27 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | ); 56 | } 57 | 58 | const styles = StyleSheet.create({ 59 | container: { 60 | flex: 1, 61 | }, 62 | }); 63 | 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Google Maps Animations in React Native and Reanimated 2 | 3 | Code used for [the following livestream](https://youtu.be/Z_dC5Mv99bI). 4 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "gmaps", 4 | "slug": "gmaps", 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 | "android": { 23 | "adaptiveIcon": { 24 | "foregroundImage": "./assets/adaptive-icon.png", 25 | "backgroundColor": "#FFFFFF" 26 | } 27 | }, 28 | "web": { 29 | "favicon": "./assets/favicon.png" 30 | } 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eveningkid/react-native-google-maps/f244eeaa103fa3db33f2e19ae6a2b9ea6473dbba/assets/adaptive-icon.png -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eveningkid/react-native-google-maps/f244eeaa103fa3db33f2e19ae6a2b9ea6473dbba/assets/favicon.png -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eveningkid/react-native-google-maps/f244eeaa103fa3db33f2e19ae6a2b9ea6473dbba/assets/icon.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eveningkid/react-native-google-maps/f244eeaa103fa3db33f2e19ae6a2b9ea6473dbba/assets/splash.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | plugins: ['react-native-reanimated/plugin'], 6 | }; 7 | }; 8 | 9 | -------------------------------------------------------------------------------- /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": "~41.0.1", 12 | "expo-status-bar": "~1.0.4", 13 | "react": "16.13.1", 14 | "react-dom": "16.13.1", 15 | "react-native": "https://github.com/expo/react-native/archive/sdk-41.0.0.tar.gz", 16 | "react-native-gesture-handler": "~1.10.2", 17 | "react-native-maps": "0.27.1", 18 | "react-native-reanimated": "~2.1.0", 19 | "react-native-web": "~0.13.12" 20 | }, 21 | "devDependencies": { 22 | "@babel/core": "^7.9.0", 23 | "@types/react": "~16.9.35", 24 | "@types/react-native": "~0.63.2", 25 | "typescript": "~4.0.0" 26 | }, 27 | "private": true 28 | } 29 | 30 | -------------------------------------------------------------------------------- /src/BottomSheet.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | SafeAreaView, 4 | StyleSheet, 5 | Text, 6 | useWindowDimensions, 7 | View, 8 | } from 'react-native'; 9 | import Animated, { 10 | Extrapolate, 11 | interpolate, 12 | useAnimatedGestureHandler, 13 | useAnimatedStyle, 14 | withTiming, 15 | } from 'react-native-reanimated'; 16 | import { PanGestureHandler } from 'react-native-gesture-handler'; 17 | 18 | export default function BottomSheet({ panY }) { 19 | const { height } = useWindowDimensions(); 20 | 21 | const gestureHandler = useAnimatedGestureHandler( 22 | { 23 | onStart(_, context) { 24 | context.startY = panY.value; 25 | }, 26 | onActive(event, context) { 27 | panY.value = context.startY + event.translationY; 28 | }, 29 | onEnd() { 30 | if (panY.value < -height * 0.4) { 31 | panY.value = withTiming(-(height * 0.6)); 32 | } else { 33 | panY.value = withTiming(0); 34 | } 35 | }, 36 | }, 37 | [height] 38 | ); 39 | 40 | const animatedStyle = useAnimatedStyle(() => { 41 | return { 42 | transform: [ 43 | { 44 | translateY: interpolate(panY.value, [-1, 0], [-1, 0], { 45 | extrapolateLeft: Extrapolate.EXTEND, 46 | extrapolateRight: Extrapolate.CLAMP, 47 | }), 48 | }, 49 | ], 50 | }; 51 | }); 52 | 53 | return ( 54 | 55 | 62 | 63 | 64 | Maison Paul Bocuse 65 | 66 | 67 | 68 | 69 | 70 | ); 71 | } 72 | 73 | const styles = StyleSheet.create({ 74 | container: { 75 | position: 'absolute', 76 | top: 0, 77 | left: 0, 78 | right: 0, 79 | backgroundColor: 'white', 80 | shadowColor: 'black', 81 | shadowOffset: { 82 | height: -6, 83 | width: 0, 84 | }, 85 | shadowOpacity: 0.1, 86 | shadowRadius: 5, 87 | }, 88 | wrapper: { 89 | flex: 1, 90 | }, 91 | content: { 92 | flex: 1, 93 | padding: 20, 94 | }, 95 | title: { 96 | fontWeight: '400', 97 | fontSize: 22, 98 | }, 99 | fakeContent: { 100 | flex: 1, 101 | height: 1000, 102 | }, 103 | }); 104 | 105 | -------------------------------------------------------------------------------- /src/GeoBar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | SafeAreaView, 4 | StyleSheet, 5 | useWindowDimensions, 6 | View, 7 | } from 'react-native'; 8 | import Animated, { 9 | Extrapolate, 10 | interpolate, 11 | useAnimatedStyle, 12 | } from 'react-native-reanimated'; 13 | import Icon from './Icon'; 14 | 15 | export default function GeoBar({ panY }) { 16 | const { height } = useWindowDimensions(); 17 | 18 | const animatedStyle = useAnimatedStyle(() => { 19 | return { 20 | transform: [ 21 | { 22 | translateY: interpolate( 23 | panY.value, 24 | [-100, 0], 25 | [-100, 0], 26 | Extrapolate.CLAMP 27 | ), 28 | }, 29 | ], 30 | }; 31 | }); 32 | 33 | return ( 34 | 35 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | ); 52 | } 53 | 54 | const styles = StyleSheet.create({ 55 | container: { 56 | position: 'absolute', 57 | left: 0, 58 | right: 0, 59 | bottom: 0, 60 | marginHorizontal: '5%', 61 | alignItems: 'flex-end', 62 | }, 63 | icon: { 64 | height: 50, 65 | width: 50, 66 | justifyContent: 'center', 67 | alignItems: 'center', 68 | borderRadius: 25, 69 | backgroundColor: 'white', 70 | shadowColor: 'black', 71 | shadowOffset: { 72 | height: 6, 73 | width: 0, 74 | }, 75 | shadowOpacity: 0.1, 76 | shadowRadius: 5, 77 | }, 78 | iconMargin: { 79 | marginBottom: 15, 80 | }, 81 | }); 82 | 83 | -------------------------------------------------------------------------------- /src/Icon.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Ionicons } from '@expo/vector-icons'; 3 | 4 | export default function Icon(props) { 5 | return ; 6 | } 7 | 8 | -------------------------------------------------------------------------------- /src/NavBar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, useWindowDimensions, View } from 'react-native'; 3 | import Animated, { 4 | useAnimatedStyle, 5 | withTiming, 6 | } from 'react-native-reanimated'; 7 | import Icon from './Icon'; 8 | 9 | export default function NavBar({ panY }) { 10 | const { height } = useWindowDimensions(); 11 | 12 | const animatedStyle = useAnimatedStyle(() => { 13 | const hidden = panY.value > -(height / 3); 14 | 15 | return { 16 | opacity: withTiming(hidden ? 0 : 1), 17 | }; 18 | }); 19 | 20 | return ( 21 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ); 43 | } 44 | 45 | const styles = StyleSheet.create({ 46 | container: { 47 | marginTop: 10, 48 | marginHorizontal: '5%', 49 | flexDirection: 'row', 50 | justifyContent: 'space-between', 51 | }, 52 | rightIcons: { 53 | flexDirection: 'row', 54 | }, 55 | icon: { 56 | paddingLeft: 2, 57 | height: 40, 58 | width: 40, 59 | justifyContent: 'center', 60 | alignItems: 'center', 61 | borderRadius: 20, 62 | backgroundColor: 'rgba(0, 0, 0, 0.4)', 63 | }, 64 | iconMargin: { 65 | marginRight: 10, 66 | }, 67 | }); 68 | 69 | -------------------------------------------------------------------------------- /src/Overlay.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, useWindowDimensions } from 'react-native'; 3 | import Animated, { 4 | interpolate, 5 | Extrapolate, 6 | useAnimatedStyle, 7 | } from 'react-native-reanimated'; 8 | 9 | export default function Overlay({ panY }) { 10 | const { height } = useWindowDimensions(); 11 | 12 | const animatedStyle = useAnimatedStyle(() => { 13 | return { 14 | opacity: interpolate( 15 | panY.value, 16 | [0, -height], 17 | [0, 1], 18 | Extrapolate.CLAMP 19 | ), 20 | }; 21 | }); 22 | 23 | return ( 24 | 32 | ); 33 | } 34 | 35 | const styles = StyleSheet.create({ 36 | container: { 37 | backgroundColor: 'black', 38 | }, 39 | }); 40 | 41 | -------------------------------------------------------------------------------- /src/PicturesCarousel.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Image, StyleSheet, useWindowDimensions } from 'react-native'; 3 | import Animated, { 4 | Extrapolate, 5 | interpolate, 6 | useAnimatedStyle, 7 | } from 'react-native-reanimated'; 8 | 9 | const IMAGES = [ 10 | 'https://lh5.googleusercontent.com/p/AF1QipPVL19xwWdTqGRq0OJaijq28AxKP_34ww8fOXOa=s1013-k-no', 11 | 'https://lh5.googleusercontent.com/p/AF1QipMOSlEF_obIrLiP6Q7xM8UHyn4jnDhLezCrjvR7=s773-k-no', 12 | 'https://lh5.googleusercontent.com/p/AF1QipOA7zoRS0zXE6ntoOMPKJZFGcJKmAUKM-NOEWHg=s773-k-no', 13 | 'https://lh5.googleusercontent.com/p/AF1QipPfag6TLyhgDdDGWxXBPkgEgmmdBeZFD2lIEfBO=s872-k-no', 14 | ]; 15 | 16 | export default function PicturesCarousel({ panY }) { 17 | const { height, width } = useWindowDimensions(); 18 | 19 | const animatedStyle = useAnimatedStyle(() => { 20 | return { 21 | transform: [ 22 | { 23 | translateY: interpolate( 24 | panY.value, 25 | [0, -(height * 0.6)], 26 | [0, -height], 27 | Extrapolate.CLAMP 28 | ), 29 | }, 30 | ], 31 | }; 32 | }); 33 | 34 | return ( 35 | 40 | {IMAGES.map((image, index) => ( 41 | 47 | ))} 48 | 49 | ); 50 | } 51 | 52 | const styles = StyleSheet.create({ 53 | container: { 54 | position: 'absolute', 55 | top: 0, 56 | left: 0, 57 | right: 0, 58 | backgroundColor: 'white', 59 | }, 60 | }); 61 | 62 | -------------------------------------------------------------------------------- /src/SearchBar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, useWindowDimensions } from 'react-native'; 3 | import Animated, { 4 | useAnimatedStyle, 5 | withTiming, 6 | } from 'react-native-reanimated'; 7 | 8 | export default function SearchBar({ panY }) { 9 | const { height } = useWindowDimensions(); 10 | 11 | const animatedStyle = useAnimatedStyle(() => { 12 | const hidden = panY.value < -(height / 3); 13 | 14 | return { 15 | transform: [ 16 | { 17 | translateY: withTiming(hidden ? -150 : 0), 18 | }, 19 | ], 20 | }; 21 | }); 22 | 23 | return ( 24 | 31 | ); 32 | } 33 | 34 | const styles = StyleSheet.create({ 35 | container: { 36 | marginTop: 10, 37 | marginHorizontal: '5%', 38 | height: 50, 39 | borderRadius: 25, 40 | backgroundColor: 'white', 41 | shadowColor: 'black', 42 | shadowOffset: { 43 | height: 6, 44 | width: 0, 45 | }, 46 | shadowOpacity: 0.1, 47 | shadowRadius: 5, 48 | }, 49 | }); 50 | 51 | --------------------------------------------------------------------------------