├── .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 | 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 | Buy Me A Coffee 4 | 5 | ### Youtube tutorial 6 | 7 | [![Advanced React Native Animated FlatList Picker - Synchronise 2 FlatLists](react-native-advanced-flatlist-syncronized-animations-dribbble_.gif)](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 --------------------------------------------------------------------------------