├── .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 |
--------------------------------------------------------------------------------