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