├── .watchmanconfig ├── README.md ├── .netlify └── state.json ├── assets ├── images │ ├── icon.png │ ├── splash.png │ ├── robot-dev.png │ └── robot-prod.png └── fonts │ └── SpaceMono-Regular.ttf ├── babel.config.js ├── .gitignore ├── components ├── StyledText.js ├── __tests__ │ ├── StyledText-test.js │ └── __snapshots__ │ │ └── StyledText-test.js.snap ├── TabBarIcon.js └── CustomTabBar │ ├── TabLabel.jsx │ ├── CustomTabBar.jsx │ ├── DynamicHeader.jsx │ └── TabBar.js ├── constants ├── Layout.js └── Colors.js ├── navigation ├── HeaderContainer.native.js ├── AppNavigator.js ├── HeaderContainer.js └── MainTabNavigator.js ├── __tests__ ├── __snapshots__ │ └── App-test.js.snap └── App-test.js ├── screens ├── SettingsScreen.js ├── LinksScreen.js ├── Search │ ├── SearchLayout.js │ ├── SearchBar.js │ ├── Header.js │ └── SearchBar.ios.js ├── SearchScreen.js └── HomeScreen.js ├── app.json ├── package.json ├── App.js └── .expo-shared └── assets.json /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Demo: https://expo-tabs.netlify.com 2 | -------------------------------------------------------------------------------- /.netlify/state.json: -------------------------------------------------------------------------------- 1 | { 2 | "siteId": "ec575236-f58d-4f91-aa26-ff75137b5018" 3 | } -------------------------------------------------------------------------------- /assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvanBacon/react-navigation-web-responsive-tabs-demo/HEAD/assets/images/icon.png -------------------------------------------------------------------------------- /assets/images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvanBacon/react-navigation-web-responsive-tabs-demo/HEAD/assets/images/splash.png -------------------------------------------------------------------------------- /assets/images/robot-dev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvanBacon/react-navigation-web-responsive-tabs-demo/HEAD/assets/images/robot-dev.png -------------------------------------------------------------------------------- /assets/images/robot-prod.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvanBacon/react-navigation-web-responsive-tabs-demo/HEAD/assets/images/robot-prod.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /assets/fonts/SpaceMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvanBacon/react-navigation-web-responsive-tabs-demo/HEAD/assets/fonts/SpaceMono-Regular.ttf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .expo/* 3 | npm-debug.* 4 | *.jks 5 | *.p12 6 | *.key 7 | *.mobileprovision 8 | *.orig.* 9 | web-build/ 10 | web-report/ 11 | -------------------------------------------------------------------------------- /components/StyledText.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text } from 'react-native'; 3 | 4 | export function MonoText(props) { 5 | return ( 6 | 7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /constants/Layout.js: -------------------------------------------------------------------------------- 1 | import { Dimensions } from 'react-native'; 2 | 3 | const width = Dimensions.get('window').width; 4 | const height = Dimensions.get('window').height; 5 | 6 | export default { 7 | window: { 8 | width, 9 | height, 10 | }, 11 | isSmallDevice: width < 375, 12 | }; 13 | -------------------------------------------------------------------------------- /components/__tests__/StyledText-test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | 4 | import { MonoText } from '../StyledText'; 5 | 6 | it(`renders correctly`, () => { 7 | const tree = renderer.create(Snapshot test!).toJSON(); 8 | 9 | expect(tree).toMatchSnapshot(); 10 | }); 11 | -------------------------------------------------------------------------------- /components/__tests__/__snapshots__/StyledText-test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly 1`] = ` 4 | 14 | Snapshot test! 15 | 16 | `; 17 | -------------------------------------------------------------------------------- /constants/Colors.js: -------------------------------------------------------------------------------- 1 | const tintColor = '#2f95dc'; 2 | 3 | export default { 4 | tintColor, 5 | tabIconDefault: '#ccc', 6 | tabIconSelected: tintColor, 7 | tabBar: '#fefefe', 8 | errorBackground: 'red', 9 | errorText: '#fff', 10 | warningBackground: '#EAEB5E', 11 | warningText: '#666804', 12 | noticeBackground: tintColor, 13 | noticeText: '#fff', 14 | header: '#202124', 15 | }; 16 | -------------------------------------------------------------------------------- /components/TabBarIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Ionicons } from '@expo/vector-icons'; 3 | 4 | import Colors from '../constants/Colors'; 5 | 6 | export default function TabBarIcon(props) { 7 | return ( 8 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /navigation/HeaderContainer.native.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export class NavContainer extends React.PureComponent { 4 | render() { 5 | return this.props.children; 6 | } 7 | } 8 | 9 | export class HeaderPortal extends React.PureComponent { 10 | render() { 11 | return this.props.children; 12 | } 13 | } 14 | 15 | export default class HeaderContainer extends React.PureComponent { 16 | render() { 17 | return this.props.children; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/App-test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`App renders the loading screen 1`] = ` 4 | 9 | `; 10 | 11 | exports[`App renders the root without loading screen 1`] = ` 12 | 20 | 21 | 22 | `; 23 | -------------------------------------------------------------------------------- /screens/SettingsScreen.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ExpoConfigView } from '@expo/samples'; 3 | 4 | export default function SettingsScreen({ navigation }) { 5 | React.useEffect(() => { 6 | navigation.setParams({ query: undefined }); 7 | }, []); 8 | /** 9 | * Go ahead and delete ExpoConfigView and replace it with your content; 10 | * we just wanted to give you a quick view of your config. 11 | */ 12 | return ; 13 | } 14 | 15 | SettingsScreen.navigationOptions = { 16 | title: 'app.json', 17 | }; 18 | -------------------------------------------------------------------------------- /screens/LinksScreen.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ScrollView, StyleSheet } from 'react-native'; 3 | import { ExpoLinksView } from '@expo/samples'; 4 | 5 | export default function LinksScreen({ navigation }) { 6 | React.useEffect(() => { 7 | navigation.setParams({ query: undefined }); 8 | }, []); 9 | return ( 10 | 11 | {/** 12 | * Go ahead and delete ExpoLinksView and replace it with your content; 13 | * we just wanted to provide you with some helpful links. 14 | */} 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "tabs-001", 4 | "slug": "tabs-001", 5 | "privacy": "public", 6 | "sdkVersion": "33.0.0", 7 | "platforms": [ 8 | "ios", 9 | "android", 10 | "web" 11 | ], 12 | "version": "1.0.0", 13 | "orientation": "portrait", 14 | "icon": "./assets/images/icon.png", 15 | "splash": { 16 | "image": "./assets/images/splash.png", 17 | "resizeMode": "contain", 18 | "backgroundColor": "#ffffff" 19 | }, 20 | "updates": { 21 | "fallbackToCacheTimeout": 0 22 | }, 23 | "assetBundlePatterns": [ 24 | "**/*" 25 | ], 26 | "ios": { 27 | "supportsTablet": true 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /navigation/AppNavigator.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createAppContainer, createSwitchNavigator } from 'react-navigation'; 3 | import { createBrowserApp } from '@react-navigation/web'; 4 | import { Platform } from 'react-native'; 5 | import MainTabNavigator from './MainTabNavigator'; 6 | MainTabNavigator.path = ''; 7 | const createApp = Platform.select({ 8 | web: createBrowserApp, 9 | default: createAppContainer, 10 | }); 11 | 12 | export default createApp( 13 | createSwitchNavigator({ 14 | // You could add another route here for authentication. 15 | // Read more at https://reactnavigation.org/docs/en/auth-flow.html 16 | Main: MainTabNavigator, 17 | }), 18 | { history: 'hash' }, 19 | ); 20 | -------------------------------------------------------------------------------- /navigation/HeaderContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { View } from 'react-native'; 4 | export class NavContainer extends React.PureComponent { 5 | render() { 6 | return ( 7 |
8 | {this.props.children} 9 |
10 | ); 11 | } 12 | } 13 | 14 | export class HeaderPortal extends React.PureComponent { 15 | render() { 16 | return ReactDOM.createPortal(this.props.children, document.body); 17 | } 18 | } 19 | 20 | export default class HeaderContainer extends React.PureComponent { 21 | render() { 22 | const { children } = this.props; 23 | return ( 24 | 25 | {children} 26 | 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /__tests__/App-test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import NavigationTestUtils from 'react-navigation/NavigationTestUtils'; 3 | import renderer from 'react-test-renderer'; 4 | 5 | import App from '../App'; 6 | 7 | jest.mock('expo', () => ({ 8 | AppLoading: 'AppLoading', 9 | })); 10 | 11 | jest.mock('../navigation/AppNavigator', () => 'AppNavigator'); 12 | 13 | describe('App', () => { 14 | jest.useFakeTimers(); 15 | 16 | beforeEach(() => { 17 | NavigationTestUtils.resetInternalState(); 18 | }); 19 | 20 | it(`renders the loading screen`, () => { 21 | const tree = renderer.create().toJSON(); 22 | expect(tree).toMatchSnapshot(); 23 | }); 24 | 25 | it(`renders the root without loading screen`, () => { 26 | const tree = renderer.create().toJSON(); 27 | expect(tree).toMatchSnapshot(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /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 | "test": "jest --watchAll" 10 | }, 11 | "jest": { 12 | "preset": "jest-expo" 13 | }, 14 | "dependencies": { 15 | "@expo/samples": "3.0.1", 16 | "@expo/style-utils": "^1.0.0-alpha.2", 17 | "@expo/vector-icons": "^10.0.1", 18 | "@react-navigation/web": "^1.0.0-alpha.8", 19 | "expo": "^33.0.0", 20 | "expo-asset": "^5.0.0", 21 | "expo-constants": "^5.0.0", 22 | "expo-font": "^5.0.0", 23 | "expo-web-browser": "^5.0.0", 24 | "react": "16.8.3", 25 | "react-dom": "^16.8.6", 26 | "react-native": "https://github.com/expo/react-native/archive/sdk-33.0.0.tar.gz", 27 | "react-native-hooks": "^0.0.6", 28 | "react-native-web": "^0.11.4", 29 | "react-navigation": "^3.11.0" 30 | }, 31 | "devDependencies": { 32 | "babel-preset-expo": "^5.1.0", 33 | "jest-expo": "^33.0.0" 34 | }, 35 | "private": true 36 | } 37 | -------------------------------------------------------------------------------- /components/CustomTabBar/TabLabel.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Animated, StyleSheet } from 'react-native'; 3 | 4 | export default function TabLabel({ 5 | route, 6 | position, 7 | navigation, 8 | activeTintColor = '#fff', 9 | inactiveTintColor = '#fff', 10 | upperCaseLabel, 11 | labelStyle, 12 | getLabelText, 13 | }) { 14 | const { routes } = navigation.state; 15 | const index = routes.indexOf(route); 16 | const focused = index === navigation.state.index; 17 | 18 | // Prepend '-1', so there are always at least 2 items in inputRange 19 | const inputRange = [-1, ...routes.map((x, i) => i)]; 20 | const outputRange = inputRange.map(inputIndex => 21 | inputIndex === index ? activeTintColor : inactiveTintColor, 22 | ); 23 | const color = position.interpolate({ 24 | inputRange, 25 | outputRange: outputRange, 26 | }); 27 | 28 | const tintColor = focused ? activeTintColor : inactiveTintColor; 29 | const label = getLabelText({ route }); 30 | 31 | if (typeof label === 'string') { 32 | return ( 33 | 37 | {upperCaseLabel ? label.toUpperCase() : label} 38 | 39 | ); 40 | } 41 | if (typeof label === 'function') { 42 | return label({ focused, tintColor }); 43 | } 44 | 45 | return label; 46 | } 47 | 48 | const styles = StyleSheet.create({ 49 | label: { 50 | textAlign: 'center', 51 | fontSize: 20, 52 | margin: 8, 53 | backgroundColor: 'transparent', 54 | }, 55 | }); 56 | -------------------------------------------------------------------------------- /components/CustomTabBar/CustomTabBar.jsx: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { Resizable } from '@expo/style-utils'; 3 | import * as React from 'react'; 4 | import { StyleSheet, View } from 'react-native'; 5 | 6 | import Colors from '../../constants/Colors'; 7 | import DynamicHeader from './DynamicHeader'; 8 | import TabBar from './TabBar'; 9 | import TabLabel from './TabLabel'; 10 | 11 | function TabBarTop(props) { 12 | const { navigation, ...rest } = props; 13 | 14 | const [headerHeight, setHeight] = React.useState(size); 15 | 16 | return ( 17 | 18 | 19 | {({ width }) => ( 20 | setHeight(height)} 23 | isMobileWidth={width < 520} 24 | position={props.position} 25 | > 26 | } 33 | /> 34 | 35 | )} 36 | 37 | 38 | ); 39 | } 40 | 41 | export default TabBarTop; 42 | 43 | const size = 72; 44 | 45 | const styles = StyleSheet.create({ 46 | icon: { 47 | height: 24, 48 | width: 24, 49 | }, 50 | tabBar: { 51 | backgroundColor: Colors.header, 52 | height: 48, 53 | }, 54 | tabStyle: { 55 | paddingHorizontal: 11, 56 | }, 57 | scrollWrapper: { 58 | height: '100%', 59 | }, 60 | }); 61 | -------------------------------------------------------------------------------- /navigation/MainTabNavigator.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | createDrawerNavigator, 4 | createMaterialTopTabNavigator, 5 | createStackNavigator, 6 | createSwitchNavigator, 7 | } from 'react-navigation'; 8 | 9 | import HomeScreen from '../screens/HomeScreen'; 10 | import LinksScreen from '../screens/LinksScreen'; 11 | import SearchScreen from '../screens/SearchScreen'; 12 | import SettingsScreen from '../screens/SettingsScreen'; 13 | import CustomTabBar from '../components/CustomTabBar/CustomTabBar'; 14 | 15 | const stackConfig = { 16 | headerMode: 'none', 17 | }; 18 | 19 | const HomeStack = createStackNavigator( 20 | { 21 | Design: HomeScreen, 22 | }, 23 | stackConfig, 24 | ); 25 | HomeStack.path = ''; 26 | HomeStack.navigationOptions = { 27 | tabBarLabel: 'Design', 28 | }; 29 | 30 | const LinksStack = createStackNavigator( 31 | { 32 | Develop: LinksScreen, 33 | }, 34 | stackConfig, 35 | ); 36 | LinksStack.path = ''; 37 | LinksStack.navigationOptions = { 38 | tabBarLabel: 'Develop', 39 | }; 40 | 41 | const SettingsStack = createStackNavigator( 42 | { 43 | Tools: SettingsScreen, 44 | }, 45 | stackConfig, 46 | ); 47 | SettingsStack.path = ''; 48 | SettingsStack.navigationOptions = { 49 | tabBarLabel: 'Tools', 50 | }; 51 | 52 | const TabNav = createMaterialTopTabNavigator( 53 | { 54 | HomeStack, 55 | LinksStack, 56 | SettingsStack, 57 | }, 58 | { 59 | tabBarComponent: CustomTabBar, 60 | }, 61 | ); 62 | TabNav.path = ''; 63 | 64 | const DrawerNav = createDrawerNavigator({ 65 | TabNav, 66 | }); 67 | DrawerNav.path = ''; 68 | 69 | export default createSwitchNavigator( 70 | { 71 | DrawerNav, 72 | Search: SearchScreen, 73 | }, 74 | { headerMode: 'none' }, 75 | ); 76 | -------------------------------------------------------------------------------- /App.js: -------------------------------------------------------------------------------- 1 | import { Ionicons } from '@expo/vector-icons'; 2 | import { AppLoading } from 'expo'; 3 | import { Asset } from 'expo-asset'; 4 | import * as Font from 'expo-font'; 5 | import React, { useState } from 'react'; 6 | import { Platform, StatusBar, StyleSheet, View } from 'react-native'; 7 | 8 | import AppNavigator from './navigation/AppNavigator'; 9 | 10 | export default function App(props) { 11 | const [isLoadingComplete, setLoadingComplete] = useState(false); 12 | 13 | if (!isLoadingComplete && !props.skipLoadingScreen) { 14 | return ( 15 | handleFinishLoading(setLoadingComplete)} 19 | /> 20 | ); 21 | } else { 22 | return ( 23 | 24 | {Platform.OS === 'ios' && } 25 | 26 | 27 | ); 28 | } 29 | } 30 | 31 | async function loadResourcesAsync() { 32 | await Promise.all([ 33 | Asset.loadAsync([ 34 | require('./assets/images/robot-dev.png'), 35 | require('./assets/images/robot-prod.png'), 36 | ]), 37 | Font.loadAsync({ 38 | // This is the font that we are using for our tab bar 39 | ...Ionicons.font, 40 | // We include SpaceMono because we use it in HomeScreen.js. Feel free to 41 | // remove this if you are not using it in your app 42 | 'space-mono': require('./assets/fonts/SpaceMono-Regular.ttf'), 43 | }), 44 | ]); 45 | } 46 | 47 | function handleLoadingError(error: Error) { 48 | // In this case, you might want to report the error to your error reporting 49 | // service, for example Sentry 50 | console.warn(error); 51 | } 52 | 53 | function handleFinishLoading(setLoadingComplete) { 54 | setLoadingComplete(true); 55 | } 56 | 57 | const styles = StyleSheet.create({ 58 | container: { 59 | flex: 1, 60 | backgroundColor: '#fff', 61 | }, 62 | }); 63 | -------------------------------------------------------------------------------- /screens/Search/SearchLayout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Platform, StyleSheet, Text, View } from 'react-native'; 3 | import SearchBar from './SearchBar'; 4 | import Header from './Header'; 5 | 6 | const DEFAULT_TINT_COLOR = Platform.OS === 'ios' ? '#007AFF' : '#000'; 7 | 8 | export default class SearchLayout extends React.Component { 9 | static SearchBar = SearchBar; 10 | static Header = Header; 11 | static DefaultTintColor = DEFAULT_TINT_COLOR; 12 | 13 | static defaultProps = { 14 | debounce: 500, 15 | headerBackgroundColor: '#fff', 16 | headerTintColor: DEFAULT_TINT_COLOR, 17 | }; 18 | 19 | state = { 20 | q: '', 21 | }; 22 | 23 | _handleSubmit = q => { 24 | this.props.onSubmit && this.props.onSubmit(q); 25 | }; 26 | 27 | // TODO: debounce 28 | _handleChangeQuery = q => { 29 | this.props.onChangeQuery && this.props.onChangeQuery(q); 30 | this.setState({ q }); 31 | }; 32 | 33 | render() { 34 | return ( 35 |
40 | 56 |
57 | ); 58 | } 59 | } 60 | 61 | const styles = StyleSheet.create({ 62 | container: { 63 | flex: 1, 64 | flexBasis: 'auto', 65 | }, 66 | }); 67 | -------------------------------------------------------------------------------- /.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "0cae4d70c6df3e5e96ee8b5c442b59d55c8ab8deb466992ab9abc523822f2a1b": true, 3 | "e997a5256149a4b76e6bfd6cbf519c5e5a0f1d278a3d8fa1253022b03c90473b": true, 4 | "af683c96e0ffd2cf81287651c9433fa44debc1220ca7cb431fe482747f34a505": true, 5 | "e7fc0741cc6562975a990e3d9ef820571588dab20aba97032df9f00caa9cd57a": true, 6 | "51420d8608d23222130d54bf889dda977b78466258199dc75e19c10bbc70c15d": true, 7 | "4cb5f51b5166d2160401033809cb17012b9c39553150254fa242c8d8e33c870e": true, 8 | "a6e814182ee12e2a7d1b9525524bfe508da525ace1ea818597450d1ffeb11018": true, 9 | "802d54a9d813d8ec112479d26c5031896e1f737c021f5d07604ee5a17b21f15e": true, 10 | "610f694e7d1f79ca3e1999cf1930a83c0c745287a279693c6808007bc504365b": true, 11 | "529b9e6d9fde06f9726b8f7c427e3da930e38287a14fc898655a54361b407843": true, 12 | "b528dd66cf54b79289d7ec64fe60cb3144cf3469e56c3abbf4a4060ba514334c": true, 13 | "2a569c563816bd0cc9849bb73466eafb22a0055e80ac7077310c6417c74270b8": true, 14 | "367dc575cfece5d7f6dda472eea7e74bf526a620dcc8a8ac6d752c9fb517b8b9": true, 15 | "afad5c159048acf271e56b96046c48bec78ac73e1c57b5f3b31d17e5a362a5f1": true, 16 | "acb852f79ef92765ca6e56266469293d05b5796edc0c69043f48c0d983f4d038": true, 17 | "e70343bc7066392275a265b333673100c5a5efa887d82c523d403f6adcb4ac00": true, 18 | "60b699953e15a1613269c71667fed8c47b56e9b780ccb1ecf776f2870324cf47": true, 19 | "f1377e47c161d933b3f76875183c75cdb50530810fc368e8c109b09feb75f5c4": true, 20 | "19e27c1e5eee1d19933468afb8b6b497bf05aa204506eddaa24948d4a79ff5e1": true, 21 | "12641c609148ac8e98f010fcf33a57efd3b73667bc6f038542800f03b884845c": true, 22 | "3e598dd701778a0e0e947f42ad96e7fdfa7f810dc439681e454c7f4edeb8772d": true, 23 | "60a9cbf35abd54d76549b678828716ba02d65ad0e6c5589c42a405b70f71471f": true, 24 | "eea1753f1d474d5f443e352b6c9dd06568a20e0ec5976e41295c24f5efeb7c23": true, 25 | "74a706553891447edccd8bb395637fd459797938ecb397320fe6daacd629359e": true, 26 | "a33005222b929c100fbf9405f524c79744dda7d655bae662e495c623ee8b2307": true, 27 | "00cc274ab1bc99ba0918777c3ee3efed84e2a1a3bb075b33a3d42599ab8811ed": true, 28 | "d2ca9928eb35b058793ce7616e9e13eeef31456e713d81b29e018c5fbdc24fa6": true, 29 | "86db67a269bb58f69f9f0e1d806dba0604dc3deb3ac74d772ccff7bebbee812a": true, 30 | "e77bec9fcc64a0fd7313b6bba4d33895615e4150743b5b70dc317db2bf8c6432": true, 31 | "d92dc78c27965b4350df63b4d2db57f34edd22c4ae37e9cce7e9d81d9ccf219d": true 32 | } -------------------------------------------------------------------------------- /screens/SearchScreen.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | Animated, 4 | Button, 5 | Platform, 6 | Text, 7 | StyleSheet, 8 | View, 9 | FlatList, 10 | } from 'react-native'; 11 | import { 12 | createAppContainer, 13 | createStackNavigator, 14 | StackViewTransitionConfigs, 15 | } from 'react-navigation'; 16 | import { RectButton, BorderlessButton } from 'react-native-gesture-handler'; 17 | import SearchLayout from './Search/SearchLayout'; 18 | import { Ionicons } from '@expo/vector-icons'; 19 | 20 | function getData(str) { 21 | if (!str) return []; 22 | return new Array(parseInt(Math.random() * 50 + 8)) 23 | .fill(str) 24 | .map((_, i) => `${str} ${i}`); 25 | } 26 | 27 | class SearchScreen extends React.Component { 28 | constructor(props) { 29 | super(props); 30 | // Get the initial value 31 | const query = props.navigation.getParam('query'); 32 | this.state = { 33 | query, 34 | data: getData(query), 35 | }; 36 | } 37 | 38 | componentDidUpdate(prevProps) { 39 | if ( 40 | this.props.navigation.getParam('query') !== 41 | prevProps.navigation.getParam('query') 42 | ) { 43 | // When the router changes from an external source. 44 | this.setState({ query: this.props.navigation.getParam('query') }); 45 | } 46 | } 47 | 48 | onChangeQuery = query => { 49 | this.setState({ query }); 50 | 51 | clearTimeout(this._id); 52 | this._id = setTimeout(() => { 53 | this.setState({ data: getData(query) }); 54 | }, Math.random() * 600 + 300); 55 | }; 56 | 57 | get query() { 58 | return this.state.query; 59 | } 60 | 61 | onSubmit = () => { 62 | // Update the route 63 | this.props.navigation.setParams({ query: this.query }); 64 | }; 65 | 66 | render() { 67 | return ( 68 | 69 | this.props.navigation.navigate('Design')} 72 | onChangeQuery={this.onChangeQuery} 73 | onSubmit={this.onSubmit} 74 | /> 75 | 76 | 77 | ); 78 | } 79 | } 80 | 81 | class ResultsList extends React.Component { 82 | renderItem = ({ item }) => ( 83 | 90 | {item} 91 | 92 | ); 93 | 94 | render() { 95 | return ( 96 | ( 100 | 106 | 114 | 117 | No Items Found 118 | 119 | 120 | Maybe try looking for something else! 121 | 122 | 123 | 124 | )} 125 | data={this.props.data} 126 | /> 127 | ); 128 | } 129 | } 130 | 131 | export default SearchScreen; 132 | 133 | const styles = StyleSheet.create({ 134 | container: { 135 | flex: 1, 136 | alignItems: 'center', 137 | justifyContent: 'center', 138 | }, 139 | }); 140 | -------------------------------------------------------------------------------- /components/CustomTabBar/DynamicHeader.jsx: -------------------------------------------------------------------------------- 1 | import MaterialIcons from '@expo/vector-icons/MaterialIcons'; 2 | import * as React from 'react'; 3 | import { Animated, StyleSheet, TouchableOpacity, View } from 'react-native'; 4 | 5 | import Colors from '../../constants/Colors'; 6 | 7 | const AnimatedTouchableOpacity = Animated.createAnimatedComponent( 8 | TouchableOpacity, 9 | ); 10 | const size = 72; 11 | 12 | export default function CustomHeader( 13 | { navigation, onHeightChanged, children, isMobileWidth, position }, 14 | ref, 15 | ) { 16 | return ( 17 | onHeightChanged(nativeEvent.layout.height)} 20 | > 21 | 22 | 23 | 27 | 28 | 32 | 33 | 34 | {!isMobileWidth && children} 35 | 36 | 37 | {isMobileWidth && children} 38 | 39 | ); 40 | } 41 | 42 | // Animated styles 43 | function getAnimatedMenuButtonStyle(position) { 44 | const scale = position.interpolate({ 45 | inputRange: [0, 1], 46 | outputRange: [1, 0.001], 47 | extrapolate: 'clamp', 48 | }); 49 | const opacity = position.interpolate({ 50 | inputRange: [0, 1], 51 | outputRange: [1, 0], 52 | extrapolate: 'clamp', 53 | }); 54 | return { 55 | opacity, 56 | transform: [{ scale }], 57 | }; 58 | } 59 | 60 | function getAnimatedIconStyle(position) { 61 | const translateX = position.interpolate({ 62 | inputRange: [0, 1], 63 | outputRange: [0, -36], 64 | extrapolate: 'clamp', 65 | }); 66 | 67 | return { 68 | transform: [ 69 | { translateX }, 70 | { 71 | translateX: position.interpolate({ 72 | inputRange: [0, 1], 73 | outputRange: ['0%', '-50%'], 74 | extrapolate: 'clamp', 75 | }), 76 | }, 77 | ], 78 | }; 79 | } 80 | 81 | // Buttons 82 | const StarButton = ({ navigation, style }) => ( 83 | navigation.navigate('Design')} 86 | > 87 | 88 | 89 | ); 90 | 91 | const MenuButton = ({ navigation, style }) => ( 92 | { 101 | if (navigation.openDrawer) navigation.openDrawer(); 102 | }} 103 | > 104 | 105 | 106 | ); 107 | 108 | const SearchButton = ({ navigation }) => ( 109 | navigation.navigate('Search')} 112 | > 113 | 114 | 115 | ); 116 | 117 | // Icon alias 118 | const Icon = ({ name, size = 32 }) => ( 119 | 120 | ); 121 | 122 | const styles = StyleSheet.create({ 123 | leftHeader: { 124 | flexDirection: 'row', 125 | flex: 1, 126 | minHeight: size, 127 | }, 128 | container: { 129 | position: 'fixed', 130 | top: 0, 131 | left: 0, 132 | right: 0, 133 | }, 134 | row: { 135 | flexDirection: 'row', 136 | justifyContent: 'center', 137 | alignItems: 'center', 138 | }, 139 | header: { 140 | flexDirection: 'row', 141 | backgroundColor: Colors.header, 142 | alignItems: 'center', 143 | }, 144 | buttonTouchable: { 145 | padding: 20, 146 | minWidth: size, 147 | justifyContent: 'center', 148 | alignItems: 'center', 149 | height: '100%', 150 | }, 151 | }); 152 | -------------------------------------------------------------------------------- /screens/Search/SearchBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Platform, StyleSheet, TextInput, View } from 'react-native'; 3 | import { withNavigation } from 'react-navigation'; 4 | import Touchable from 'react-native-platform-touchable'; 5 | import { MaterialIcons } from '@expo/vector-icons'; 6 | 7 | @withNavigation 8 | export default class SearchBar extends React.PureComponent { 9 | constructor(props) { 10 | super(props); 11 | 12 | this.state = { 13 | text: props.text || '', 14 | }; 15 | } 16 | componentDidMount() { 17 | requestAnimationFrame(() => { 18 | this._textInput.focus(); 19 | }); 20 | } 21 | 22 | get text() { 23 | if (this.props.text === 'undefined') { 24 | return this.state.text; 25 | } 26 | return this.props.text; 27 | } 28 | 29 | render() { 30 | let searchInputStyle = {}; 31 | if (this.props.textColor) { 32 | searchInputStyle.color = this.props.textColor; 33 | } 34 | 35 | return ( 36 | 37 | 44 | 50 | 55 | 56 | 57 | 58 | { 60 | this._textInput = view; 61 | }} 62 | placeholder="Search" 63 | placeholderTextColor={this.props.placeholderTextColor || '#ccc'} 64 | value={this.text} 65 | autoCapitalize="none" 66 | autoCorrect={false} 67 | selectionColor={this.props.selectionColor} 68 | underlineColorAndroid={this.props.underlineColorAndroid || '#ccc'} 69 | onSubmitEditing={this._handleSubmit} 70 | onChangeText={this._handleChangeText} 71 | style={[styles.searchInput, searchInputStyle]} 72 | /> 73 | 80 | {this.text ? ( 81 | 87 | 92 | 93 | ) : null} 94 | 95 | 96 | ); 97 | } 98 | 99 | _handleClear = () => { 100 | this._handleChangeText(''); 101 | }; 102 | _handleChangeText = text => { 103 | this.setState({ text }); 104 | this.props.onChangeQuery && this.props.onChangeQuery(text); 105 | }; 106 | 107 | _handleSubmit = () => { 108 | this.props.onSubmit && this.props.onSubmit(this.text); 109 | this._textInput.blur(); 110 | }; 111 | } 112 | 113 | const styles = StyleSheet.create({ 114 | container: { 115 | position: 'absolute', 116 | top: 0, 117 | left: 0, 118 | right: 0, 119 | flex: 1, 120 | flexBasis: 'auto', 121 | flexDirection: 'row', 122 | height: 72, 123 | borderBottomWidth: StyleSheet.hairlineWidth, 124 | borderBottomColor: 'rgba(0,0,0,0.3)', 125 | }, 126 | searchInput: { 127 | flex: 1, 128 | flexBasis: 'auto', 129 | fontSize: 18, 130 | marginBottom: 2, 131 | paddingLeft: 5, 132 | marginRight: 5, 133 | ...Platform.select({ 134 | web: { 135 | outlineWidth: 0, 136 | }, 137 | default: {}, 138 | }), 139 | }, 140 | }); 141 | -------------------------------------------------------------------------------- /screens/Search/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Animated, 4 | Dimensions, 5 | Platform, 6 | StatusBar, 7 | StyleSheet, 8 | View, 9 | } from 'react-native'; 10 | import { withNavigation, HeaderBackButton } from 'react-navigation'; 11 | import { getInset, getStatusBarHeight } from 'react-native-safe-area-view'; 12 | import HeaderContainer from '../../navigation/HeaderContainer'; 13 | // import { isIphoneX } from 'react-native-iphone-x-helper'; 14 | const isIphoneX = () => false; 15 | // @todo: make this work properly when in landscape 16 | const hasNotch = isIphoneX(); 17 | 18 | const APPBAR_HEIGHT = Platform.OS === 'ios' ? 50 : 56; 19 | const TITLE_OFFSET = Platform.OS === 'ios' ? 70 : 56; 20 | 21 | @withNavigation 22 | export default class Header extends React.PureComponent { 23 | constructor(props) { 24 | super(props); 25 | 26 | // @todo: this is static and we don't know if it's visible or not on iOS. 27 | // need to use a more reliable and cross-platform API when one exists, like 28 | // LayoutContext. We also don't know if it's translucent or not on Android 29 | // and depend on react-native-safe-area-view to tell us. 30 | const ANDROID_STATUS_BAR_HEIGHT = getStatusBarHeight 31 | ? getStatusBarHeight() 32 | : StatusBar.currentHeight; 33 | const STATUSBAR_HEIGHT = 34 | Platform.OS === 'ios' ? (hasNotch ? 40 : 25) : ANDROID_STATUS_BAR_HEIGHT; 35 | 36 | let platformContainerStyles; 37 | if (Platform.OS === 'ios') { 38 | platformContainerStyles = { 39 | borderBottomWidth: StyleSheet.hairlineWidth, 40 | borderBottomColor: '#A7A7AA', 41 | }; 42 | } else { 43 | platformContainerStyles = { 44 | shadowColor: 'black', 45 | shadowOpacity: 0.1, 46 | shadowRadius: StyleSheet.hairlineWidth, 47 | shadowOffset: { 48 | height: StyleSheet.hairlineWidth, 49 | }, 50 | elevation: 4, 51 | }; 52 | } 53 | 54 | this.styles = { 55 | container: { 56 | backgroundColor: '#fff', 57 | flexBasis: 'auto', 58 | flex: 1, 59 | paddingTop: STATUSBAR_HEIGHT, 60 | height: STATUSBAR_HEIGHT + APPBAR_HEIGHT, 61 | maxHeight: STATUSBAR_HEIGHT + APPBAR_HEIGHT, 62 | minHeight: STATUSBAR_HEIGHT + APPBAR_HEIGHT, 63 | ...platformContainerStyles, 64 | }, 65 | appBar: { 66 | flex: 1, 67 | flexBasis: 'auto', 68 | }, 69 | header: { 70 | flexDirection: 'row', 71 | }, 72 | item: { 73 | justifyContent: 'center', 74 | alignItems: 'center', 75 | backgroundColor: 'transparent', 76 | }, 77 | title: { 78 | bottom: 0, 79 | left: TITLE_OFFSET, 80 | right: TITLE_OFFSET, 81 | top: 0, 82 | position: 'absolute', 83 | alignItems: Platform.OS === 'ios' ? 'center' : 'flex-start', 84 | }, 85 | left: { 86 | left: 0, 87 | bottom: 0, 88 | top: 0, 89 | position: 'absolute', 90 | }, 91 | right: { 92 | right: 0, 93 | bottom: 0, 94 | top: 0, 95 | position: 'absolute', 96 | }, 97 | }; 98 | } 99 | 100 | _navigateBack = () => { 101 | this.props.navigation.goBack(null); 102 | }; 103 | 104 | _maybeRenderBackButton = () => { 105 | if (!this.props.backButton) { 106 | return; 107 | } 108 | 109 | return ( 110 | 111 | 119 | 120 | ); 121 | }; 122 | 123 | render() { 124 | let headerStyle = {}; 125 | if (this.props.backgroundColor) { 126 | headerStyle.backgroundColor = this.props.backgroundColor; 127 | } 128 | 129 | return ( 130 | 131 | 132 | {this._maybeRenderBackButton()} 133 | {this.props.children} 134 | 135 | 136 | ); 137 | } 138 | } 139 | 140 | const styles = StyleSheet.create({ 141 | container: { 142 | position: 'fixed', 143 | top: 0, 144 | right: 0, 145 | left: 0, 146 | height: 72, 147 | flexDirection: 'row', 148 | }, 149 | }); 150 | -------------------------------------------------------------------------------- /screens/HomeScreen.js: -------------------------------------------------------------------------------- 1 | import * as WebBrowser from 'expo-web-browser'; 2 | import React from 'react'; 3 | import { 4 | Image, 5 | Platform, 6 | ScrollView, 7 | StyleSheet, 8 | Text, 9 | TouchableOpacity, 10 | View, 11 | } from 'react-native'; 12 | 13 | import { MonoText } from '../components/StyledText'; 14 | 15 | export default function HomeScreen({ navigation }) { 16 | React.useEffect(() => { 17 | navigation.setParams({ query: undefined }); 18 | }, []); 19 | 20 | return ( 21 | 22 | 26 | 27 | 35 | 36 | 37 | 38 | 39 | 40 | Get started by opening 41 | 42 | 45 | screens/HomeScreen.js 46 | 47 | 48 | 49 | Change this text and your app will automatically reload. 50 | 51 | 52 | 53 | 54 | 55 | 56 | Help, it didn’t automatically reload! 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | This is a tab bar. You can edit it in: 65 | 66 | 67 | 70 | 71 | navigation/MainTabNavigator.js 72 | 73 | 74 | 75 | 76 | ); 77 | } 78 | 79 | HomeScreen.navigationOptions = { 80 | header: null, 81 | }; 82 | 83 | function DevelopmentModeNotice() { 84 | if (__DEV__) { 85 | const learnMoreButton = ( 86 | 87 | Learn more 88 | 89 | ); 90 | 91 | return ( 92 | 93 | Development mode is enabled: your app will be slower but you can use 94 | useful development tools. {learnMoreButton} 95 | 96 | ); 97 | } else { 98 | return ( 99 | 100 | You are not in development mode: your app will run at full speed. 101 | 102 | ); 103 | } 104 | } 105 | 106 | function handleLearnMorePress() { 107 | WebBrowser.openBrowserAsync( 108 | 'https://docs.expo.io/versions/latest/workflow/development-mode/', 109 | ); 110 | } 111 | 112 | function handleHelpPress() { 113 | WebBrowser.openBrowserAsync( 114 | 'https://docs.expo.io/versions/latest/workflow/up-and-running/#cant-see-your-changes', 115 | ); 116 | } 117 | 118 | const styles = StyleSheet.create({ 119 | container: { 120 | flex: 1, 121 | backgroundColor: '#fff', 122 | }, 123 | developmentModeText: { 124 | marginBottom: 20, 125 | color: 'rgba(0,0,0,0.4)', 126 | fontSize: 14, 127 | lineHeight: 19, 128 | textAlign: 'center', 129 | }, 130 | contentContainer: { 131 | paddingTop: 30, 132 | }, 133 | welcomeContainer: { 134 | alignItems: 'center', 135 | marginTop: 10, 136 | marginBottom: 20, 137 | }, 138 | welcomeImage: { 139 | width: 100, 140 | height: 80, 141 | resizeMode: 'contain', 142 | marginTop: 3, 143 | marginLeft: -10, 144 | }, 145 | getStartedContainer: { 146 | alignItems: 'center', 147 | marginHorizontal: 50, 148 | }, 149 | homeScreenFilename: { 150 | marginVertical: 7, 151 | }, 152 | codeHighlightText: { 153 | color: 'rgba(96,100,109, 0.8)', 154 | }, 155 | codeHighlightContainer: { 156 | backgroundColor: 'rgba(0,0,0,0.05)', 157 | borderRadius: 3, 158 | paddingHorizontal: 4, 159 | }, 160 | getStartedText: { 161 | fontSize: 17, 162 | color: 'rgba(96,100,109, 1)', 163 | lineHeight: 24, 164 | textAlign: 'center', 165 | }, 166 | tabBarInfoContainer: { 167 | position: 'absolute', 168 | bottom: 0, 169 | left: 0, 170 | right: 0, 171 | ...Platform.select({ 172 | ios: { 173 | shadowColor: 'black', 174 | shadowOffset: { width: 0, height: -3 }, 175 | shadowOpacity: 0.1, 176 | shadowRadius: 3, 177 | }, 178 | android: { 179 | elevation: 20, 180 | }, 181 | }), 182 | alignItems: 'center', 183 | backgroundColor: '#fbfbfb', 184 | paddingVertical: 20, 185 | }, 186 | tabBarInfoText: { 187 | fontSize: 17, 188 | color: 'rgba(96,100,109, 1)', 189 | textAlign: 'center', 190 | }, 191 | navigationFilename: { 192 | marginTop: 5, 193 | }, 194 | helpContainer: { 195 | marginTop: 15, 196 | alignItems: 'center', 197 | }, 198 | helpLink: { 199 | paddingVertical: 15, 200 | }, 201 | helpLinkText: { 202 | fontSize: 14, 203 | color: '#2e78b7', 204 | }, 205 | }); 206 | -------------------------------------------------------------------------------- /screens/Search/SearchBar.ios.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Dimensions, 4 | LayoutAnimation, 5 | NativeModules, 6 | StyleSheet, 7 | Text, 8 | TextInput, 9 | TouchableOpacity, 10 | TouchableWithoutFeedback, 11 | View, 12 | } from 'react-native'; 13 | import { withNavigation } from 'react-navigation'; 14 | import Ionicons from 'react-native-vector-icons/Ionicons'; 15 | 16 | const Layout = { 17 | window: { 18 | width: Dimensions.get('window').width, 19 | }, 20 | }; 21 | const SearchContainerHorizontalMargin = 10; 22 | const SearchContainerWidth = 23 | Layout.window.width - SearchContainerHorizontalMargin * 2; 24 | 25 | const SearchIcon = () => ( 26 | 27 | 28 | 29 | ); 30 | 31 | @withNavigation 32 | class PlaceholderButtonSearchBar extends React.PureComponent { 33 | static defaultProps = { 34 | placeholder: 'Search', 35 | placeholderTextColor: '#ccc', 36 | } 37 | 38 | render() { 39 | return ( 40 | 41 | 44 | 45 | 52 | 53 | 54 | 55 | 56 | 57 | ); 58 | } 59 | 60 | _handlePress = () => { 61 | this.props.navigator.push('search'); 62 | }; 63 | } 64 | 65 | @withNavigation 66 | export default class SearchBar extends React.PureComponent { 67 | state = { 68 | text: '', 69 | showCancelButton: false, 70 | inputWidth: SearchContainerWidth, 71 | }; 72 | 73 | _textInput: TextInput; 74 | 75 | componentDidMount() { 76 | requestAnimationFrame(() => { 77 | this._textInput.focus(); 78 | }); 79 | } 80 | 81 | _handleLayoutCancelButton = (e: Object) => { 82 | if (this.state.showCancelButton) { 83 | return; 84 | } 85 | 86 | const cancelButtonWidth = e.nativeEvent.layout.width; 87 | 88 | requestAnimationFrame(() => { 89 | LayoutAnimation.configureNext({ 90 | duration: 200, 91 | create: { 92 | type: LayoutAnimation.Types.linear, 93 | property: LayoutAnimation.Properties.opacity, 94 | }, 95 | update: { 96 | type: LayoutAnimation.Types.spring, 97 | springDamping: 0.9, 98 | initialVelocity: 10, 99 | }, 100 | }); 101 | 102 | this.setState({ 103 | showCancelButton: true, 104 | inputWidth: SearchContainerWidth - cancelButtonWidth, 105 | }); 106 | }); 107 | }; 108 | 109 | render() { 110 | let { inputWidth, showCancelButton } = this.state; 111 | let searchInputStyle = {}; 112 | if (this.props.textColor) { 113 | searchInputStyle.color = this.props.textColor; 114 | } 115 | 116 | return ( 117 | 118 | 119 | { 121 | this._textInput = view; 122 | }} 123 | clearButtonMode="while-editing" 124 | onChangeText={this._handleChangeText} 125 | value={this.state.text} 126 | autoCapitalize="none" 127 | autoCorrect={false} 128 | returnKeyType="search" 129 | placeholder="Search" 130 | placeholderTextColor={this.props.placeholderTextColor || '#ccc'} 131 | onSubmitEditing={this._handleSubmit} 132 | style={[styles.searchInput, searchInputStyle]} 133 | /> 134 | 135 | 136 | 137 | 138 | 148 | 153 | 158 | {this.props.cancelButtonText || 'Cancel'} 159 | 160 | 161 | 162 | 163 | ); 164 | } 165 | 166 | _handleChangeText = text => { 167 | this.setState({ text }); 168 | this.props.onChangeQuery && this.props.onChangeQuery(text); 169 | }; 170 | 171 | _handleSubmit = () => { 172 | let { text } = this.state; 173 | this.props.onSubmit && this.props.onSubmit(text); 174 | this._textInput.blur(); 175 | }; 176 | 177 | _handlePressCancelButton = () => { 178 | if (this.props.onCancelPress) { 179 | this.props.onCancelPress(this.props.navigation.goBack); 180 | } else { 181 | this.props.navigation.goBack(); 182 | } 183 | }; 184 | } 185 | 186 | const styles = StyleSheet.create({ 187 | container: { 188 | flex: 1, 189 | flexDirection: 'row', 190 | }, 191 | buttonContainer: { 192 | position: 'absolute', 193 | right: 0, 194 | top: 0, 195 | paddingTop: 15, 196 | flexDirection: 'row', 197 | alignItems: 'center', 198 | justifyContent: 'center', 199 | }, 200 | button: { 201 | paddingRight: 17, 202 | paddingLeft: 2, 203 | }, 204 | searchContainer: { 205 | height: 30, 206 | width: SearchContainerWidth, 207 | backgroundColor: '#f2f2f2', 208 | borderRadius: 5, 209 | marginHorizontal: SearchContainerHorizontalMargin, 210 | marginTop: 10, 211 | paddingLeft: 27, 212 | }, 213 | searchIconContainer: { 214 | position: 'absolute', 215 | left: 7, 216 | top: 6, 217 | bottom: 0, 218 | }, 219 | searchInput: { 220 | flex: 1, 221 | fontSize: 14, 222 | paddingTop: 1, 223 | }, 224 | }); 225 | -------------------------------------------------------------------------------- /components/CustomTabBar/TabBar.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import * as React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import { 6 | Animated, 7 | NativeModules, 8 | StyleSheet, 9 | View, 10 | ScrollView, 11 | Dimensions, 12 | Platform, 13 | I18nManager, 14 | } from 'react-native'; 15 | import TouchableItem from 'react-native-tab-view/src/TouchableItem'; 16 | import { SceneRendererPropType } from 'react-native-tab-view/src/PropTypes'; 17 | import type { 18 | Scene, 19 | SceneRendererProps, 20 | } from 'react-native-tab-view/src/TypeDefinitions'; 21 | import type { 22 | ViewStyleProp, 23 | TextStyleProp, 24 | } from 'react-native/Libraries/StyleSheet/StyleSheet'; 25 | 26 | type IndicatorProps = SceneRendererProps & { 27 | width: number, 28 | }; 29 | 30 | type Props = SceneRendererProps & { 31 | scrollEnabled?: boolean, 32 | bounces?: boolean, 33 | pressColor?: string, 34 | pressOpacity?: number, 35 | getLabelText: (scene: Scene) => ?string, 36 | getAccessible: (scene: Scene) => ?boolean, 37 | getAccessibilityLabel: (scene: Scene) => ?string, 38 | getTestID: (scene: Scene) => ?string, 39 | renderLabel?: (scene: Scene) => React.Node, 40 | renderIcon?: (scene: Scene) => React.Node, 41 | renderBadge?: (scene: Scene) => React.Node, 42 | renderIndicator?: (props: IndicatorProps) => React.Node, 43 | onTabPress?: (scene: Scene) => mixed, 44 | onTabLongPress?: (scene: Scene) => mixed, 45 | tabStyle?: ViewStyleProp, 46 | indicatorStyle?: ViewStyleProp, 47 | labelStyle?: TextStyleProp, 48 | style?: ViewStyleProp, 49 | }; 50 | 51 | type State = {| 52 | visibility: Animated.Value, 53 | scrollAmount: Animated.Value, 54 | initialOffset: ?{| x: number, y: number |}, 55 | |}; 56 | 57 | const useNativeDriver = Boolean(NativeModules.NativeAnimatedModule); 58 | 59 | export default class TabBar extends React.Component, State> { 60 | static propTypes = { 61 | ...SceneRendererPropType, 62 | scrollEnabled: PropTypes.bool, 63 | bounces: PropTypes.bool, 64 | pressColor: TouchableItem.propTypes.pressColor, 65 | pressOpacity: TouchableItem.propTypes.pressOpacity, 66 | getLabelText: PropTypes.func, 67 | getAccessible: PropTypes.func, 68 | getAccessibilityLabel: PropTypes.func, 69 | getTestID: PropTypes.func, 70 | renderIcon: PropTypes.func, 71 | renderLabel: PropTypes.func, 72 | renderIndicator: PropTypes.func, 73 | onTabPress: PropTypes.func, 74 | onTabLongPress: PropTypes.func, 75 | labelStyle: PropTypes.any, 76 | style: PropTypes.any, 77 | }; 78 | 79 | static defaultProps = { 80 | getLabelText: ({ route }: Scene) => 81 | typeof route.title === 'string' ? route.title.toUpperCase() : route.title, 82 | getAccessible: ({ route }: Scene) => 83 | typeof route.accessible !== 'undefined' ? route.accessible : true, 84 | getAccessibilityLabel: ({ route }: Scene) => route.accessibilityLabel, 85 | getTestID: ({ route }: Scene) => route.testID, 86 | }; 87 | 88 | constructor(props: Props) { 89 | super(props); 90 | 91 | let initialVisibility = 1; 92 | 93 | if (this.props.scrollEnabled) { 94 | const tabWidth = this._getTabWidth(this.props); 95 | if (!tabWidth) { 96 | initialVisibility = 0; 97 | } 98 | } 99 | 100 | const initialOffset = 101 | this.props.scrollEnabled && this.props.layout.width 102 | ? { 103 | x: this._getScrollAmount( 104 | this.props, 105 | this.props.navigationState.index, 106 | ), 107 | y: 0, 108 | } 109 | : undefined; 110 | 111 | this.state = { 112 | visibility: new Animated.Value(initialVisibility), 113 | scrollAmount: new Animated.Value(0), 114 | initialOffset, 115 | }; 116 | } 117 | 118 | componentDidMount() { 119 | this.props.scrollEnabled && this._startTrackingPosition(); 120 | } 121 | 122 | componentDidUpdate(prevProps: Props) { 123 | const prevTabWidth = this._getTabWidth(prevProps); 124 | const currentTabWidth = this._getTabWidth(this.props); 125 | const pendingIndex = 126 | typeof this._pendingIndex === 'number' 127 | ? this._pendingIndex 128 | : this.props.navigationState.index; 129 | 130 | this._pendingIndex = null; 131 | 132 | if (prevTabWidth !== currentTabWidth && currentTabWidth) { 133 | this.state.visibility.setValue(1); 134 | } 135 | 136 | if ( 137 | prevProps.navigationState.routes.length !== 138 | this.props.navigationState.routes.length || 139 | prevProps.layout.width !== this.props.layout.width 140 | ) { 141 | this._resetScroll(this.props.navigationState.index, false); 142 | } else if (prevProps.navigationState.index !== pendingIndex) { 143 | this._resetScroll(this.props.navigationState.index); 144 | } 145 | } 146 | 147 | componentWillUnmount() { 148 | this._stopTrackingPosition(); 149 | } 150 | 151 | _scrollView: ?ScrollView; 152 | _isIntial: boolean = true; 153 | _isManualScroll: boolean = false; 154 | _isMomentumScroll: boolean = false; 155 | _pendingIndex: ?number; 156 | _scrollResetCallback: any; 157 | _lastPanX: ?number; 158 | _lastOffsetX: ?number; 159 | _panXListener: string; 160 | _offsetXListener: string; 161 | 162 | _startTrackingPosition = () => { 163 | this._offsetXListener = this.props.offsetX.addListener(({ value }) => { 164 | this._lastOffsetX = value; 165 | this._handlePosition(); 166 | }); 167 | this._panXListener = this.props.panX.addListener(({ value }) => { 168 | this._lastPanX = value; 169 | this._handlePosition(); 170 | }); 171 | }; 172 | 173 | _stopTrackingPosition = () => { 174 | this.props.offsetX.removeListener(this._offsetXListener); 175 | this.props.panX.removeListener(this._panXListener); 176 | }; 177 | 178 | _handlePosition = () => { 179 | const { navigationState, layout } = this.props; 180 | 181 | if (layout.width === 0) { 182 | // Don't do anything if we don't have layout yet 183 | return; 184 | } 185 | 186 | const panX = typeof this._lastPanX === 'number' ? this._lastPanX : 0; 187 | const offsetX = 188 | typeof this._lastOffsetX === 'number' 189 | ? this._lastOffsetX 190 | : -navigationState.index * layout.width; 191 | 192 | const value = (panX + offsetX) / -(layout.width || 0.001); 193 | 194 | this._adjustScroll(value); 195 | }; 196 | 197 | _renderLabel = (scene: Scene<*>) => { 198 | if (typeof this.props.renderLabel !== 'undefined') { 199 | return this.props.renderLabel(scene); 200 | } 201 | const label = this.props.getLabelText(scene); 202 | if (typeof label !== 'string') { 203 | return null; 204 | } 205 | return ( 206 | 207 | {label} 208 | 209 | ); 210 | }; 211 | 212 | _renderIndicator = (props: IndicatorProps) => { 213 | if (typeof this.props.renderIndicator !== 'undefined') { 214 | return this.props.renderIndicator(props); 215 | } 216 | const { width, position, navigationState } = props; 217 | const translateX = Animated.multiply( 218 | Animated.multiply( 219 | position.interpolate({ 220 | inputRange: [-1, navigationState.routes.length], 221 | outputRange: [-1, navigationState.routes.length], 222 | extrapolate: 'clamp', 223 | }), 224 | width, 225 | ), 226 | I18nManager.isRTL ? -1 : 1, 227 | ); 228 | return ( 229 | 236 | ); 237 | }; 238 | 239 | _getTabWidth = props => { 240 | const { layout, navigationState, tabStyle } = props; 241 | const flattened = StyleSheet.flatten(tabStyle); 242 | 243 | if (flattened) { 244 | switch (typeof flattened.width) { 245 | case 'number': 246 | return flattened.width; 247 | case 'string': 248 | if (flattened.width.endsWith('%')) { 249 | const width = parseFloat(flattened.width); 250 | if (Number.isFinite(width)) { 251 | return layout.width * (width / 100); 252 | } 253 | } 254 | } 255 | } 256 | 257 | if (props.scrollEnabled) { 258 | return (layout.width / 5) * 2; 259 | } 260 | 261 | return layout.width / navigationState.routes.length; 262 | }; 263 | 264 | _handleTabPress = ({ route }: Scene<*>) => { 265 | this._pendingIndex = this.props.navigationState.routes.indexOf(route); 266 | 267 | if (this.props.onTabPress) { 268 | this.props.onTabPress({ route }); 269 | } 270 | 271 | this.props.jumpTo(route.key); 272 | }; 273 | 274 | _handleTabLongPress = ({ route }: Scene<*>) => { 275 | if (this.props.onTabLongPress) { 276 | this.props.onTabLongPress({ route }); 277 | } 278 | }; 279 | 280 | _normalizeScrollValue = (props, value) => { 281 | const { layout, navigationState } = props; 282 | const tabWidth = this._getTabWidth(props); 283 | const tabBarWidth = Math.max( 284 | tabWidth * navigationState.routes.length, 285 | layout.width, 286 | ); 287 | const maxDistance = tabBarWidth - layout.width; 288 | 289 | return Math.max(Math.min(value, maxDistance), 0); 290 | }; 291 | 292 | _getScrollAmount = (props, i) => { 293 | const { layout } = props; 294 | const tabWidth = this._getTabWidth(props); 295 | const centerDistance = tabWidth * (i + 1 / 2); 296 | const scrollAmount = centerDistance - layout.width / 2; 297 | 298 | return this._normalizeScrollValue(props, scrollAmount); 299 | }; 300 | 301 | _adjustScroll = (value: number) => { 302 | if (this.props.scrollEnabled) { 303 | global.cancelAnimationFrame(this._scrollResetCallback); 304 | this._scrollView && 305 | this._scrollView.scrollTo({ 306 | x: this._normalizeScrollValue( 307 | this.props, 308 | this._getScrollAmount(this.props, value), 309 | ), 310 | animated: !this._isIntial, // Disable animation for the initial render 311 | }); 312 | 313 | this._isIntial = false; 314 | } 315 | }; 316 | 317 | _resetScroll = (value: number, animated = true) => { 318 | if (this.props.scrollEnabled) { 319 | global.cancelAnimationFrame(this._scrollResetCallback); 320 | this._scrollResetCallback = global.requestAnimationFrame(() => { 321 | this._scrollView && 322 | this._scrollView.scrollTo({ 323 | x: this._getScrollAmount(this.props, value), 324 | animated, 325 | }); 326 | }); 327 | } 328 | }; 329 | 330 | _handleBeginDrag = () => { 331 | // onScrollBeginDrag fires when user touches the ScrollView 332 | this._isManualScroll = true; 333 | this._isMomentumScroll = false; 334 | }; 335 | 336 | _handleEndDrag = () => { 337 | // onScrollEndDrag fires when user lifts his finger 338 | // onMomentumScrollBegin fires after touch end 339 | // run the logic in next frame so we get onMomentumScrollBegin first 340 | global.requestAnimationFrame(() => { 341 | if (this._isMomentumScroll) { 342 | return; 343 | } 344 | this._isManualScroll = false; 345 | }); 346 | }; 347 | 348 | _handleMomentumScrollBegin = () => { 349 | // onMomentumScrollBegin fires on flick, as well as programmatic scroll 350 | this._isMomentumScroll = true; 351 | }; 352 | 353 | _handleMomentumScrollEnd = () => { 354 | // onMomentumScrollEnd fires when the scroll finishes 355 | this._isMomentumScroll = false; 356 | this._isManualScroll = false; 357 | }; 358 | 359 | render() { 360 | const { position, navigationState, scrollEnabled, bounces } = this.props; 361 | const { routes } = navigationState; 362 | const tabWidth = this._getTabWidth(this.props); 363 | const tabBarWidth = tabWidth * routes.length; 364 | 365 | // Prepend '-1', so there are always at least 2 items in inputRange 366 | const inputRange = [-1, ...routes.map((x, i) => i)]; 367 | const translateX = Animated.multiply(this.state.scrollAmount, -1); 368 | 369 | return ( 370 | 371 | {false && ( 372 | 381 | {this._renderIndicator({ 382 | ...this.props, 383 | width: tabWidth, 384 | })} 385 | 386 | )} 387 | 388 | (this._scrollView = el && el.getNode())} 419 | > 420 | {routes.map((route, i) => { 421 | const outputRange = inputRange.map(inputIndex => 422 | inputIndex === i ? 1 : 0.7, 423 | ); 424 | const opacity = Animated.multiply( 425 | this.state.visibility, 426 | position.interpolate({ 427 | inputRange, 428 | outputRange, 429 | }), 430 | ); 431 | const label = this._renderLabel({ route }); 432 | const icon = this.props.renderIcon 433 | ? this.props.renderIcon({ route }) 434 | : null; 435 | const badge = this.props.renderBadge 436 | ? this.props.renderBadge({ route }) 437 | : null; 438 | 439 | const tabStyle = {}; 440 | 441 | tabStyle.opacity = opacity; 442 | 443 | if (icon) { 444 | if (label) { 445 | tabStyle.paddingTop = 8; 446 | } else { 447 | tabStyle.padding = 12; 448 | } 449 | } 450 | 451 | const passedTabStyle = StyleSheet.flatten(this.props.tabStyle); 452 | const isWidthSet = 453 | (passedTabStyle && 454 | typeof passedTabStyle.width !== 'undefined') || 455 | scrollEnabled === true; 456 | const tabContainerStyle = {}; 457 | 458 | if (isWidthSet) { 459 | tabStyle.width = tabWidth; 460 | } 461 | 462 | if (passedTabStyle && typeof passedTabStyle.flex === 'number') { 463 | tabContainerStyle.flex = passedTabStyle.flex; 464 | } else if (!isWidthSet) { 465 | tabContainerStyle.flex = 1; 466 | } 467 | 468 | let accessibilityLabel = this.props.getAccessibilityLabel({ 469 | route, 470 | }); 471 | 472 | accessibilityLabel = 473 | typeof accessibilityLabel !== 'undefined' 474 | ? accessibilityLabel 475 | : this.props.getLabelText({ route }); 476 | 477 | const isFocused = i === navigationState.index; 478 | 479 | return ( 480 | this._handleTabPress({ route })} 496 | onLongPress={() => this._handleTabLongPress({ route })} 497 | style={tabContainerStyle} 498 | > 499 | 500 | 508 | {icon} 509 | {label} 510 | 511 | {badge ? ( 512 | 518 | {badge} 519 | 520 | ) : null} 521 | 522 | 523 | ); 524 | })} 525 | 526 | 527 | 528 | ); 529 | } 530 | } 531 | 532 | const styles = StyleSheet.create({ 533 | container: { 534 | flex: 1, 535 | }, 536 | scroll: { 537 | overflow: Platform.OS === 'web' ? ('auto': any) : 'scroll', 538 | }, 539 | tabBar: { 540 | backgroundColor: '#2196f3', 541 | elevation: 4, 542 | shadowColor: 'black', 543 | shadowOpacity: 0.1, 544 | shadowRadius: StyleSheet.hairlineWidth, 545 | shadowOffset: { 546 | height: StyleSheet.hairlineWidth, 547 | }, 548 | // We don't need zIndex on Android, disable it since it's buggy 549 | zIndex: Platform.OS === 'android' ? 0 : 1, 550 | }, 551 | tabContent: { 552 | flexDirection: 'row', 553 | flexWrap: 'nowrap', 554 | }, 555 | tabLabel: { 556 | backgroundColor: 'transparent', 557 | color: 'white', 558 | margin: 8, 559 | }, 560 | tabItem: { 561 | flex: 1, 562 | padding: 8, 563 | alignItems: 'center', 564 | justifyContent: 'center', 565 | }, 566 | badge: { 567 | position: 'absolute', 568 | top: 0, 569 | right: 0, 570 | }, 571 | indicatorContainer: { 572 | position: 'absolute', 573 | top: 0, 574 | left: 0, 575 | right: 0, 576 | bottom: 0, 577 | }, 578 | indicator: { 579 | backgroundColor: '#ffeb3b', 580 | position: 'absolute', 581 | left: 0, 582 | bottom: 0, 583 | right: 0, 584 | height: 2, 585 | }, 586 | }); 587 | --------------------------------------------------------------------------------