├── .expo-shared
└── assets.json
├── .gitignore
├── App.js
├── README.md
├── app.json
├── assets
├── favicon.png
├── icon.png
└── splash.png
├── babel.config.js
├── data.js
├── package.json
├── react-native-advanced-flatlist-syncronized-animations-dribbble_.gif
└── yarn.lock
/.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.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Inspiration: https://dribbble.com/shots/3431451-HUNGRY
3 | */
4 | import * as React from 'react';
5 | import {
6 | TouchableOpacity,
7 | Alert,
8 | StatusBar,
9 | Dimensions,
10 | Animated,
11 | FlatList,
12 | Text,
13 | View,
14 | StyleSheet,
15 | } from 'react-native';
16 | import { SimpleLineIcons } from '@expo/vector-icons';
17 |
18 | import data from './data';
19 | const ICON_SIZE = 42;
20 | const ITEM_HEIGHT = ICON_SIZE * 2;
21 | const colors = {
22 | yellow: '#FFE8A3',
23 | dark: '#2D2D2D',
24 | };
25 | const { width, height } = Dimensions.get('window');
26 |
27 | const Icon = React.memo(({ icon, color }) => {
28 | return ;
29 | });
30 |
31 | const Item = React.memo(({ icon, color, name, showText }) => {
32 | return (
33 |
34 | {showText ? (
35 | {name}
36 | ) : (
37 | // for spacing purposes
38 |
39 | )}
40 |
41 |
42 | );
43 | });
44 |
45 | const ConnectWithText = React.memo(() => {
46 | return (
47 |
55 |
63 | Connect with...
64 |
65 |
66 | );
67 | });
68 |
69 | const ConnectButton = React.memo(({ onPress }) => {
70 | return (
71 |
78 |
85 |
96 |
97 | Done!
98 |
99 |
100 |
101 | );
102 | });
103 |
104 | const List = React.memo(
105 | React.forwardRef(
106 | ({ color, showText, style, onScroll, onItemIndexChange }, ref) => {
107 | return (
108 | `${item.name}-${item.icon}`}
113 | bounces={false}
114 | scrollEnabled={!showText}
115 | scrollEventThrottle={16}
116 | onScroll={onScroll}
117 | decelerationRate='fast'
118 | snapToInterval={ITEM_HEIGHT}
119 | showsVerticalScrollIndicator={false}
120 | renderToHardwareTextureAndroid
121 | contentContainerStyle={{
122 | paddingTop: showText ? 0 : height / 2 - ITEM_HEIGHT / 2,
123 | paddingBottom: showText ? 0 : height / 2 - ITEM_HEIGHT / 2,
124 | paddingHorizontal: 20,
125 | }}
126 | renderItem={({ item }) => {
127 | return ;
128 | }}
129 | onMomentumScrollEnd={(ev) => {
130 | const newIndex = Math.round(
131 | ev.nativeEvent.contentOffset.y / ITEM_HEIGHT
132 | );
133 |
134 | if (onItemIndexChange) {
135 | onItemIndexChange(newIndex);
136 | }
137 | }}
138 | />
139 | );
140 | }
141 | )
142 | );
143 | export default function App() {
144 | const [index, setIndex] = React.useState(0);
145 | const onConnectPress = React.useCallback(() => {
146 | Alert.alert('Connect with:', data[index].name.toUpperCase());
147 | }, [index]);
148 | const yellowRef = React.useRef();
149 | const darkRef = React.useRef();
150 | const scrollY = React.useRef(new Animated.Value(0)).current;
151 | const onScroll = Animated.event(
152 | [{ nativeEvent: { contentOffset: { y: scrollY } } }],
153 | { useNativeDriver: true }
154 | );
155 | const onItemIndexChange = React.useCallback(setIndex, []);
156 | React.useEffect(() => {
157 | scrollY.addListener((v) => {
158 | if (darkRef?.current) {
159 | darkRef.current.scrollToOffset({
160 | offset: v.value,
161 | animated: false,
162 | });
163 | }
164 | });
165 | });
166 |
167 | return (
168 |
169 |
170 |
171 |
178 |
190 |
191 |
192 |
193 | );
194 | }
195 |
196 | const styles = StyleSheet.create({
197 | container: {
198 | flex: 1,
199 | justifyContent: 'center',
200 | paddingTop: StatusBar.currentHeight,
201 | backgroundColor: colors.dark,
202 | },
203 | paragraph: {
204 | margin: 24,
205 | fontSize: 18,
206 | fontWeight: 'bold',
207 | textAlign: 'center',
208 | },
209 | itemWrapper: {
210 | flexDirection: 'row',
211 | justifyContent: 'space-between',
212 | alignItems: 'center',
213 | height: ITEM_HEIGHT,
214 | },
215 | itemText: {
216 | fontSize: 26,
217 | fontWeight: '800',
218 | textTransform: 'capitalize',
219 | },
220 | });
221 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Advanced React Native Animated FlatList Picker - Synchronise 2 FlatLists
2 |
3 |
4 |
5 | ### Youtube tutorial
6 |
7 | [](https://youtu.be/frNf5VMGjsk)
8 |
9 | In this tutorial I'm going to teach you how to synchronise two different FlatList and how to create a picker animation in React Native. This is a performant animation that will work on both iOS and Android at 60fps.
10 | You can use this picker when doing a react native social auth screen.
11 |
12 | I'll walk you through:
13 |
14 | - How to synchronise two FlatLists
15 | - How to get the selected item from a FlatList using onMomentumScrollEnd event
16 | - How to create this Masked effect using two different FlatLists
17 |
18 | Inspiration: https://dribbble.com/shots/3431451-HUNGRY
19 |
20 | ---
21 |
22 | - Expo: https://expo.io/
23 |
24 | You can find me on:
25 |
26 | - Github: http://github.com/catalinmiron
27 | - Twitter: http://twitter.com/mironcatalin
28 |
29 | Wanna give me a coffe?
30 |
31 | - Paypal: mironcatalin@gmail.com
32 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "react-native-social-login-flatlist-animation",
4 | "slug": "react-native-social-login-flatlist-animation",
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 |
--------------------------------------------------------------------------------
/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/catalinmiron/react-native-animated-flatlist-picker/34f387dd64517e534296d9cfd4f245587aea1c2c/assets/favicon.png
--------------------------------------------------------------------------------
/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/catalinmiron/react-native-animated-flatlist-picker/34f387dd64517e534296d9cfd4f245587aea1c2c/assets/icon.png
--------------------------------------------------------------------------------
/assets/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/catalinmiron/react-native-animated-flatlist-picker/34f387dd64517e534296d9cfd4f245587aea1c2c/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 |
--------------------------------------------------------------------------------
/data.js:
--------------------------------------------------------------------------------
1 | export default [
2 | {
3 | icon: 'social-tumblr',
4 | name: 'tumblr',
5 | },
6 | {
7 | icon: 'social-twitter',
8 | name: 'twitter',
9 | },
10 | {
11 | icon: 'social-facebook',
12 | name: 'facebook',
13 | },
14 | {
15 | icon: 'social-instagram',
16 | name: 'instagram',
17 | },
18 | {
19 | icon: 'social-linkedin',
20 | name: 'linkedin',
21 | },
22 | {
23 | icon: 'social-pinterest',
24 | name: 'pinterest',
25 | },
26 | {
27 | icon: 'social-github',
28 | name: 'github',
29 | },
30 | {
31 | icon: 'social-google',
32 | name: 'google',
33 | },
34 | {
35 | icon: 'social-reddit',
36 | name: 'reddit',
37 | },
38 | {
39 | icon: 'social-skype',
40 | name: 'skype',
41 | },
42 | {
43 | icon: 'social-dribbble',
44 | name: 'dribbble',
45 | },
46 | {
47 | icon: 'social-behance',
48 | name: 'behance',
49 | },
50 | {
51 | icon: 'social-foursqare',
52 | name: 'foursqare',
53 | },
54 | {
55 | icon: 'social-soundcloud',
56 | name: 'soundcloud',
57 | },
58 | {
59 | icon: 'social-spotify',
60 | name: 'spotify',
61 | },
62 | {
63 | icon: 'social-stumbleupon',
64 | name: 'stumbleupon',
65 | },
66 | {
67 | icon: 'social-youtube',
68 | name: 'youtube',
69 | },
70 | {
71 | icon: 'social-dropbox',
72 | name: 'dropbox',
73 | },
74 | {
75 | icon: 'social-vkontakte',
76 | name: 'vkontakte',
77 | },
78 | {
79 | icon: 'social-steam',
80 | name: 'steam',
81 | },
82 | ];
83 |
--------------------------------------------------------------------------------
/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/vector-icons": "^10.0.0",
12 | "@react-native-community/masked-view": "0.1.10",
13 | "expo": "~38.0.8",
14 | "expo-status-bar": "^1.0.2",
15 | "react": "~16.11.0",
16 | "react-dom": "~16.11.0",
17 | "react-native": "https://github.com/expo/react-native/archive/sdk-38.0.2.tar.gz",
18 | "react-native-web": "~0.11.7"
19 | },
20 | "devDependencies": {
21 | "@babel/core": "^7.8.6",
22 | "babel-preset-expo": "~8.1.0"
23 | },
24 | "private": true
25 | }
26 |
--------------------------------------------------------------------------------
/react-native-advanced-flatlist-syncronized-animations-dribbble_.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/catalinmiron/react-native-animated-flatlist-picker/34f387dd64517e534296d9cfd4f245587aea1c2c/react-native-advanced-flatlist-syncronized-animations-dribbble_.gif
--------------------------------------------------------------------------------