├── .expo-shared
└── assets.json
├── .gitignore
├── App.tsx
├── README.md
├── app.json
├── assets
├── adaptive-icon.png
├── favicon.png
├── icon.png
└── splash.png
├── babel.config.js
├── components
├── Header.tsx
├── NavBar.tsx
├── Scene.tsx
└── index.ts
├── hooks
├── index.ts
└── useScrollManager.ts
├── lib
├── CBAnimatedHeader.tsx
├── CBAnimatedNavBar.tsx
├── CBAnimatedTabBar.tsx
├── CBAnimatedTabView.tsx
├── CBTabBar.tsx
├── CBTabView.tsx
├── CBTheme.ts
└── index.ts
├── package.json
├── tsconfig.json
└── yarn.lock
/.expo-shared/assets.json:
--------------------------------------------------------------------------------
1 | {
2 | "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true,
3 | "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true
4 | }
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/**/*
2 | .expo/*
3 | npm-debug.*
4 | *.jks
5 | *.p8
6 | *.p12
7 | *.key
8 | *.mobileprovision
9 | *.orig.*
10 | web-build/
11 |
12 | # macOS
13 | .DS_Store
14 |
--------------------------------------------------------------------------------
/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from "react";
2 | import { StyleSheet, View, Dimensions } from "react-native";
3 | import { SafeAreaProvider } from "react-native-safe-area-context";
4 | import {
5 | CBAnimatedNavBar,
6 | CBAnimatedHeader,
7 | CBAnimatedTabBar,
8 | CBTabRoute,
9 | CBTabView,
10 | CBTabBar,
11 | } from "./lib";
12 | import { useScrollManager } from "./hooks";
13 | import { Scene, NavBar, NavBarTitle, Header } from "./components";
14 |
15 | const initialWidth = Dimensions.get("window").width;
16 | export type tabKeys = "all" | "tradable" | "gainers" | "losers";
17 | export const tabs = [
18 | { key: "all" as tabKeys, title: "All assets" },
19 | { key: "gainers" as tabKeys, title: "Gainers" },
20 | { key: "losers" as tabKeys, title: "Losers" },
21 | ];
22 |
23 | export default function App() {
24 | const {
25 | scrollY,
26 | index,
27 | setIndex,
28 | getRefForKey,
29 | ...sceneProps
30 | } = useScrollManager(tabs);
31 |
32 | const renderScene = useCallback(
33 | ({ route: tab }: { route: CBTabRoute }) => (
34 |
40 | ),
41 | [getRefForKey, index, tabs, scrollY]
42 | );
43 |
44 | return (
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | (
63 |
64 |
65 |
66 | )}
67 | renderScene={renderScene}
68 | />
69 |
70 |
71 | );
72 | }
73 |
74 | const styles = StyleSheet.create({
75 | screen: {
76 | flex: 1,
77 | },
78 | });
79 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Sample Example of Coinbase Animated Tab Header
2 |
3 | ### getting started
4 |
5 | - yarn install
6 | - yarn start (note: app tested in ios and android)
7 |
8 | ### Directories
9 |
10 | - app.tsx - main entry file for app
11 | - components - product level components
12 | - lib - generic tab library components (abstracted from Coinbase's internal Design System library)
13 |
14 | ### Dependencies
15 |
16 | - _expo_ - universal app runner
17 | - _react-native-tab-view_ - Framework enabling tabs + swipe gesture
18 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "CBTabView",
4 | "slug": "CBTabView",
5 | "version": "1.0.0",
6 | "orientation": "portrait",
7 | "icon": "./assets/icon.png",
8 | "splash": {
9 | "image": "./assets/splash.png",
10 | "resizeMode": "contain",
11 | "backgroundColor": "#ffffff"
12 | },
13 | "updates": {
14 | "fallbackToCacheTimeout": 0
15 | },
16 | "assetBundlePatterns": [
17 | "**/*"
18 | ],
19 | "ios": {
20 | "supportsTablet": true
21 | },
22 | "android": {
23 | "adaptiveIcon": {
24 | "foregroundImage": "./assets/adaptive-icon.png",
25 | "backgroundColor": "#FFFFFF"
26 | }
27 | },
28 | "web": {
29 | "favicon": "./assets/favicon.png"
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/assets/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coinbase/CBTabViewExample/9e4a8f99cce4807e4b8065a2eab5d3fff57c4cf1/assets/adaptive-icon.png
--------------------------------------------------------------------------------
/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coinbase/CBTabViewExample/9e4a8f99cce4807e4b8065a2eab5d3fff57c4cf1/assets/favicon.png
--------------------------------------------------------------------------------
/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coinbase/CBTabViewExample/9e4a8f99cce4807e4b8065a2eab5d3fff57c4cf1/assets/icon.png
--------------------------------------------------------------------------------
/assets/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coinbase/CBTabViewExample/9e4a8f99cce4807e4b8065a2eab5d3fff57c4cf1/assets/splash.png
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function(api) {
2 | api.cache(true);
3 | return {
4 | presets: ['babel-preset-expo'],
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/components/Header.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Text, StyleSheet } from "react-native";
3 |
4 | export const Header = () => (
5 | <>
6 | CBTabView
7 | A Scrollable header, tab example.
8 | >
9 | );
10 |
11 | const styles = StyleSheet.create({
12 | title: {
13 | fontSize: 20,
14 | fontWeight: "bold",
15 | paddingBottom: 4,
16 | },
17 | subtitle: {
18 | fontSize: 12,
19 | },
20 | });
21 |
--------------------------------------------------------------------------------
/components/NavBar.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent, ReactNode } from "react";
2 | import { Animated, StyleSheet, View, ViewStyle, Text } from "react-native";
3 | import { useSafeArea } from "react-native-safe-area-context";
4 |
5 | export const NAV_HEADER_HEIGHT = 82;
6 | export const NAV_TITLE_HEIGHT = 56;
7 |
8 | export const NavBar: FunctionComponent = ({ children }) => {
9 | const { top: paddingTop } = useSafeArea();
10 |
11 | return (
12 |
13 | {children}
14 |
15 | );
16 | };
17 |
18 | export const NavBarTitle = () => (
19 | This is the title.
20 | );
21 |
22 | const styles = StyleSheet.create({
23 | container: {
24 | backgroundColor: "#fff",
25 | zIndex: 10,
26 | },
27 | titleContainer: {
28 | alignItems: "center",
29 | justifyContent: "center",
30 | height: NAV_TITLE_HEIGHT,
31 | flexGrow: 1,
32 | },
33 | title: {
34 | fontSize: 16,
35 | textAlign: "center",
36 | fontWeight: "bold",
37 | },
38 | });
39 |
--------------------------------------------------------------------------------
/components/Scene.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent } from "react";
2 | import {
3 | StyleSheet,
4 | RefreshControl,
5 | View,
6 | Text,
7 | NativeSyntheticEvent,
8 | NativeScrollEvent,
9 | Animated,
10 | FlatList,
11 | } from "react-native";
12 | import { CBAnimatedTabView } from "../lib";
13 |
14 | type ScrollEvent = NativeSyntheticEvent;
15 |
16 | const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15].map((i) => ({
17 | title: `Title ${i}`,
18 | key: `key-${i}`,
19 | }));
20 |
21 | interface SceneProps {
22 | isActive: boolean;
23 | routeKey: string;
24 | scrollY: Animated.Value;
25 | trackRef: (key: string, ref: FlatList) => void;
26 | onMomentumScrollBegin: (e: ScrollEvent) => void;
27 | onMomentumScrollEnd: (e: ScrollEvent) => void;
28 | onScrollEndDrag: (e: ScrollEvent) => void;
29 | }
30 |
31 | export const Scene: FunctionComponent = ({
32 | isActive,
33 | routeKey,
34 | scrollY,
35 | trackRef,
36 | onMomentumScrollBegin,
37 | onMomentumScrollEnd,
38 | onScrollEndDrag,
39 | }) => (
40 |
41 | null} />
47 | }
48 | renderItem={({ item }) => (
49 |
50 |
51 | {item.title}
52 |
53 |
54 | )}
55 | onRef={(ref: any) => {
56 | trackRef(routeKey, ref);
57 | }}
58 | scrollY={isActive ? scrollY : undefined}
59 | onScrollEndDrag={onScrollEndDrag}
60 | onMomentumScrollBegin={onMomentumScrollBegin}
61 | onMomentumScrollEnd={onMomentumScrollEnd}
62 | />
63 |
64 | );
65 |
66 | const styles = StyleSheet.create({
67 | container: {
68 | flex: 1,
69 | },
70 | inner: {
71 | paddingHorizontal: 40,
72 | paddingVertical: 20,
73 | },
74 | });
75 |
--------------------------------------------------------------------------------
/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Scene';
2 | export * from './NavBar';
3 | export * from './Header';
--------------------------------------------------------------------------------
/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useScrollManager';
--------------------------------------------------------------------------------
/hooks/useScrollManager.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useMemo, useRef, useState } from 'react';
2 | import { Animated, FlatList } from 'react-native';
3 |
4 | import { CBTabViewOffset } from '../lib/CBAnimatedTabView';
5 | import { Theme } from "../lib/CBTheme";
6 |
7 | export const useScrollManager = (routes: { key: string; title: string }[], sizing = Theme.sizing) => {
8 | const scrollY = useRef(new Animated.Value(-sizing.header)).current;
9 | const [index, setIndex] = useState(0);
10 | const isListGliding = useRef(false);
11 | const tabkeyToScrollPosition = useRef<{ [key: string]: number }>({}).current;
12 | const tabkeyToScrollableChildRef = useRef<{ [key: string]: FlatList }>({})
13 | .current;
14 |
15 | useEffect(() => {
16 | const listener = scrollY.addListener(({ value }) => {
17 | const curRoute = routes[index].key;
18 | tabkeyToScrollPosition[curRoute] = value;
19 | });
20 | return () => {
21 | scrollY.removeListener(listener);
22 | };
23 | }, [index, scrollY, routes, tabkeyToScrollPosition]);
24 |
25 | return useMemo(() => {
26 | const syncScrollOffset = () => {
27 | const curRouteKey = routes[index].key;
28 | const scrollValue = tabkeyToScrollPosition[curRouteKey];
29 |
30 | Object.keys(tabkeyToScrollableChildRef).forEach((key) => {
31 | const scrollRef = tabkeyToScrollableChildRef[key];
32 | if (!scrollRef) {
33 | return;
34 | }
35 |
36 | if (/* header visible */ key !== curRouteKey) {
37 | if (scrollValue <= CBTabViewOffset + sizing.header) {
38 | scrollRef.scrollToOffset({
39 | offset: Math.max(
40 | Math.min(scrollValue, CBTabViewOffset + sizing.header),
41 | CBTabViewOffset,
42 | ),
43 | animated: false,
44 | });
45 | tabkeyToScrollPosition[key] = scrollValue;
46 | } else if (
47 | /* header hidden */
48 | tabkeyToScrollPosition[key] <
49 | CBTabViewOffset + sizing.header ||
50 | tabkeyToScrollPosition[key] == null
51 | ) {
52 | scrollRef.scrollToOffset({
53 | offset: CBTabViewOffset + sizing.header,
54 | animated: false,
55 | });
56 | tabkeyToScrollPosition[key] =
57 | CBTabViewOffset + sizing.header;
58 | }
59 | }
60 | });
61 | };
62 |
63 | const onMomentumScrollBegin = () => {
64 | isListGliding.current = true;
65 | };
66 |
67 | const onMomentumScrollEnd = () => {
68 | isListGliding.current = false;
69 | syncScrollOffset();
70 | };
71 |
72 | const onScrollEndDrag = () => {
73 | syncScrollOffset();
74 | };
75 |
76 | const trackRef = (key: string, ref: FlatList) => {
77 | tabkeyToScrollableChildRef[key] = ref;
78 | };
79 |
80 | const getRefForKey = (key: string) => tabkeyToScrollableChildRef[key];
81 |
82 | return {
83 | scrollY,
84 | onMomentumScrollBegin,
85 | onMomentumScrollEnd,
86 | onScrollEndDrag,
87 | trackRef,
88 | index,
89 | setIndex,
90 | getRefForKey,
91 | };
92 | }, [
93 | index,
94 | routes,
95 | scrollY,
96 | tabkeyToScrollPosition,
97 | tabkeyToScrollableChildRef,
98 | ]);
99 | };
100 |
--------------------------------------------------------------------------------
/lib/CBAnimatedHeader.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent } from "react";
2 | import { Animated, StyleSheet, ViewProps } from "react-native";
3 |
4 | import { CBTabViewOffset } from "./CBAnimatedTabView";
5 | import { Theme } from "./CBTheme";
6 |
7 | export interface CBAnimatedHeaderProps extends Omit {
8 | scrollY: Animated.AnimatedValue;
9 | }
10 |
11 | export const CBAnimatedHeader: FunctionComponent = ({
12 | scrollY,
13 | children,
14 | ...otherProps
15 | }) => {
16 | const translateY = scrollY.interpolate({
17 | inputRange: [CBTabViewOffset, CBTabViewOffset + Theme.sizing.header],
18 | outputRange: [0, -Theme.sizing.header],
19 | extrapolateLeft: "clamp",
20 | });
21 |
22 | return (
23 |
27 | {children}
28 |
29 | );
30 | };
31 |
32 | const styles = StyleSheet.create({
33 | header: {
34 | top: 0,
35 | width: "100%",
36 | backgroundColor: "#fff",
37 | position: "absolute",
38 | zIndex: 2,
39 | justifyContent: "center",
40 | height: Theme.sizing.header,
41 | paddingHorizontal: Theme.spacing.gutter,
42 | },
43 | });
44 |
--------------------------------------------------------------------------------
/lib/CBAnimatedNavBar.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | FunctionComponent,
3 | ReactNode,
4 | useEffect,
5 | useRef,
6 | useState,
7 | } from "react";
8 | import { Animated } from "react-native";
9 |
10 | import { CBTabViewOffset } from "./CBAnimatedTabView";
11 | import { Theme } from "./CBTheme";
12 |
13 | export interface CBAnimatedNavBarProps {
14 | scrollY: Animated.AnimatedValue;
15 | children: ReactNode;
16 | }
17 |
18 | export const CBAnimatedNavBar: FunctionComponent = ({
19 | children,
20 | scrollY,
21 | }) => {
22 | const [showTitle, setShowTitle] = useState(false);
23 | const opacity = useRef(new Animated.Value(0)).current;
24 |
25 | useEffect(() => {
26 | Animated.timing(opacity, {
27 | toValue: showTitle ? 1 : 0,
28 | duration: 300,
29 | useNativeDriver: true,
30 | }).start();
31 | }, [opacity, showTitle]);
32 |
33 | useEffect(() => {
34 | const listener = scrollY?.addListener(({ value }) => {
35 | setShowTitle(value > CBTabViewOffset + Theme.sizing.header * 0.6);
36 | });
37 |
38 | return () => {
39 | scrollY?.removeListener(listener);
40 | };
41 | });
42 |
43 | return {children};
44 | };
45 |
--------------------------------------------------------------------------------
/lib/CBAnimatedTabBar.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent, ReactNode } from "react";
2 | import { Animated, StyleSheet, ViewProps, View } from "react-native";
3 |
4 | import { CBTabViewOffset } from "./CBAnimatedTabView";
5 | import { Theme } from "./CBTheme";
6 |
7 | export interface CBAnimatedTabBarProps extends Omit {
8 | scrollY: Animated.AnimatedValue;
9 | children: ReactNode;
10 | }
11 |
12 | export const CBAnimatedTabBar: FunctionComponent = ({
13 | children,
14 | scrollY,
15 | ...otherProps
16 | }) => {
17 | const translateY = scrollY.interpolate({
18 | inputRange: [CBTabViewOffset, CBTabViewOffset + Theme.sizing.header],
19 | outputRange: [Theme.sizing.header, 0],
20 | extrapolate: "clamp",
21 | });
22 |
23 | const opacity = scrollY.interpolate({
24 | inputRange: [
25 | CBTabViewOffset + Theme.sizing.header,
26 | CBTabViewOffset + Theme.sizing.header + 20,
27 | ],
28 | outputRange: [0, 1],
29 | extrapolateRight: "clamp",
30 | });
31 |
32 | return (
33 |
37 | {children}
38 |
39 |
40 |
41 |
42 | );
43 | };
44 |
45 | const styles = StyleSheet.create({
46 | tabBar: {
47 | width: "100%",
48 | zIndex: 10,
49 | backgroundColor: "#fff",
50 | },
51 | border: {
52 | height: 1,
53 | backgroundColor: "#eee",
54 | },
55 | });
56 |
--------------------------------------------------------------------------------
/lib/CBAnimatedTabView.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, ReactElement } from "react";
2 | import { Animated, FlatListProps, Platform, ViewProps } from "react-native";
3 | import { Theme } from "./CBTheme";
4 |
5 | // we provide this bc ios allows overscrolling but android doesn't
6 | // so on ios because of pull to refresh / rubberbaanding we set scroll pos to negtaive header pos
7 | // on android we set to 0 and makeup header height diff with contentinset padding
8 | export const CBTabViewOffset = Platform.OS === "ios" ? -Theme.sizing.header : 0;
9 |
10 | export interface CBAnimatedTabViewProps
11 | extends ViewProps,
12 | Pick<
13 | FlatListProps,
14 | | "data"
15 | | "getItemLayout"
16 | | "initialNumToRender"
17 | | "maxToRenderPerBatch"
18 | | "onContentSizeChange"
19 | | "onMomentumScrollBegin"
20 | | "onMomentumScrollEnd"
21 | | "onScrollEndDrag"
22 | | "renderItem"
23 | | "updateCellsBatchingPeriod"
24 | | "windowSize"
25 | | "ListEmptyComponent"
26 | > {
27 | onRef: (scrollableChild: Animated.FlatList) => void;
28 | scrollY?: Animated.AnimatedValue;
29 | refreshControl?: ReactElement;
30 | }
31 | const CBAnimatedTabViewWithoutMemo = ({
32 | data,
33 | renderItem,
34 | getItemLayout,
35 | onContentSizeChange,
36 | initialNumToRender,
37 | maxToRenderPerBatch,
38 | onMomentumScrollBegin,
39 | onMomentumScrollEnd,
40 | onScrollEndDrag,
41 | onRef,
42 | scrollY,
43 | refreshControl,
44 | ListEmptyComponent,
45 | }: CBAnimatedTabViewProps) => {
46 | const handleScroll =
47 | scrollY &&
48 | Animated.event([{ nativeEvent: { contentOffset: { y: scrollY } } }], {
49 | useNativeDriver: true,
50 | });
51 |
52 | return (
53 |
54 | style={{ marginBottom: Theme.sizing.tabbar }} //tabbar is absolutely positioned
55 | data={data as readonly Animated.WithAnimatedValue[]}
56 | renderItem={renderItem}
57 | keyboardShouldPersistTaps="always"
58 | ListEmptyComponent={ListEmptyComponent}
59 | getItemLayout={getItemLayout}
60 | initialNumToRender={initialNumToRender}
61 | maxToRenderPerBatch={maxToRenderPerBatch}
62 | ref={onRef}
63 | refreshControl={refreshControl}
64 | onContentSizeChange={onContentSizeChange}
65 | onMomentumScrollBegin={onMomentumScrollBegin}
66 | onMomentumScrollEnd={onMomentumScrollEnd}
67 | onScroll={handleScroll}
68 | onScrollEndDrag={onScrollEndDrag}
69 | // ios has over scrolling and other things which make this look and feel nicer
70 | contentInset={Platform.select({ ios: { top: Theme.sizing.header } })}
71 | contentOffset={Platform.select({
72 | ios: {
73 | x: 0,
74 | y: -Theme.sizing.header,
75 | },
76 | })}
77 | contentContainerStyle={Platform.select({
78 | ios: {
79 | flexGrow: 1,
80 | paddingBottom: Theme.spacing.gutter,
81 | },
82 | android: {
83 | flexGrow: 1,
84 | paddingTop: Theme.sizing.header,
85 | paddingBottom: Theme.spacing.gutter,
86 | },
87 | })}
88 | />
89 | );
90 | };
91 | // Creating an unmemoized component and casting as that type is the only way
92 | // I can get Typescript to respect the generics of the memoized function.
93 | export const CBAnimatedTabView = memo(
94 | CBAnimatedTabViewWithoutMemo
95 | ) as typeof CBAnimatedTabViewWithoutMemo;
96 |
--------------------------------------------------------------------------------
/lib/CBTabBar.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent, useCallback, useMemo, useRef } from "react";
2 | import {
3 | findNodeHandle,
4 | ScrollView,
5 | StyleSheet,
6 | View,
7 | Text,
8 | Pressable,
9 | } from "react-native";
10 | import { NavigationState, SceneRendererProps } from "react-native-tab-view";
11 |
12 | import { CBTabRoute } from "./CBTabView";
13 |
14 | export interface CBTabBarProps extends SceneRendererProps {
15 | navigationState: NavigationState;
16 | setIndex: (index: number) => void;
17 | }
18 |
19 | export const CBTabBar: FunctionComponent = ({
20 | navigationState,
21 | setIndex,
22 | }) => {
23 | const scrollRef = useRef(null);
24 | const tabs = useMemo(() => {
25 | return navigationState.routes.map((route: any, index: number) => {
26 | return (
27 |
35 | );
36 | });
37 | }, [navigationState.index, navigationState.routes, setIndex]);
38 |
39 | return {tabs};
40 | };
41 |
42 | interface TabBarButtonProps {
43 | active: boolean;
44 | index: number;
45 | onPress: (index: number) => void;
46 | title: string;
47 | scrollViewRef: ScrollView | null;
48 | }
49 |
50 | const CBTabBarButton: FunctionComponent = ({
51 | active,
52 | index,
53 | onPress,
54 | title,
55 | scrollViewRef,
56 | }) => {
57 | const xPosition = useRef(null);
58 |
59 | const handleRef = useCallback(
60 | (el: View | null) => {
61 | const scrollNode = findNodeHandle(scrollViewRef);
62 | if (el && scrollNode) {
63 | el.measureLayout(
64 | scrollNode,
65 | (offsetX) => {
66 | xPosition.current = offsetX;
67 | },
68 | () => {}
69 | );
70 | }
71 | },
72 | [scrollViewRef]
73 | );
74 |
75 | const wrappedOnPress = useCallback(() => {
76 | if (xPosition.current) {
77 | scrollViewRef?.scrollTo({
78 | x: index === 0 ? 0 : xPosition.current,
79 | y: 0,
80 | animated: true,
81 | });
82 | }
83 | return onPress(index);
84 | }, [index, onPress, scrollViewRef]);
85 |
86 | return (
87 |
88 |
97 | {title}
98 |
99 |
100 | );
101 | };
102 |
103 | const styles = StyleSheet.create({
104 | scroll: {
105 | flexGrow: 0,
106 | },
107 | contentContainer: {
108 | flexGrow: 1,
109 | },
110 | pill: {
111 | paddingHorizontal: 8,
112 | paddingVertical: 4,
113 | borderRadius: 16,
114 | },
115 | tabBar: {
116 | flexDirection: "row",
117 | paddingBottom: 16,
118 | paddingLeft: 32,
119 | },
120 | });
121 |
--------------------------------------------------------------------------------
/lib/CBTabView.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent, ReactNode } from "react";
2 | import {
3 | NavigationState,
4 | SceneRendererProps,
5 | TabView,
6 | TabViewProps,
7 | } from "react-native-tab-view";
8 |
9 | export interface CBTabRoute {
10 | key: string;
11 | title: string;
12 | }
13 |
14 | export interface CBTabViewProps
15 | extends Pick, "renderScene"> {
16 | routes: CBTabRoute[];
17 | width: number;
18 | index: number;
19 | setIndex: (i: number) => void;
20 | renderTabBar: (
21 | props: SceneRendererProps & {
22 | navigationState: NavigationState;
23 | setIndex: (i: number) => void;
24 | }
25 | ) => ReactNode;
26 | swipeEnabled?: boolean;
27 | }
28 |
29 | export const CBTabView: FunctionComponent = ({
30 | routes,
31 | width,
32 | renderTabBar,
33 | index,
34 | setIndex,
35 | renderScene,
36 | swipeEnabled = true,
37 | }) => {
38 | return (
39 |
43 | renderTabBar({
44 | ...p,
45 | setIndex,
46 | })
47 | }
48 | onIndexChange={setIndex}
49 | initialLayout={{ width }}
50 | swipeEnabled={swipeEnabled}
51 | />
52 | );
53 | };
54 |
--------------------------------------------------------------------------------
/lib/CBTheme.ts:
--------------------------------------------------------------------------------
1 | export const Theme = {
2 | sizing: {
3 | header: 80,
4 | tabbar: 40,
5 | },
6 | spacing: {
7 | gutter: 40,
8 | },
9 | };
--------------------------------------------------------------------------------
/lib/index.ts:
--------------------------------------------------------------------------------
1 | export * from './CBAnimatedTabBar';
2 | export * from './CBAnimatedHeader';
3 | export * from './CBAnimatedNavBar';
4 | export * from './CBAnimatedTabView';
5 | export * from './CBTabBar';
6 | export * from './CBTabView';
7 | export * from './CBTheme';
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "main": "node_modules/expo/AppEntry.js",
3 | "scripts": {
4 | "start": "expo start",
5 | "android": "expo start --android",
6 | "ios": "expo start --ios",
7 | "web": "expo start --web",
8 | "eject": "expo eject"
9 | },
10 | "dependencies": {
11 | "expo": "~40.0.0",
12 | "expo-status-bar": "~1.0.3",
13 | "react": "16.13.1",
14 | "react-dom": "16.13.1",
15 | "react-native": "https://github.com/expo/react-native/archive/sdk-40.0.1.tar.gz",
16 | "react-native-gesture-handler": "1.8.0",
17 | "react-native-reanimated": "1.13.1",
18 | "react-native-tab-view": "2.15.2",
19 | "react-native-web": "~0.13.12"
20 | },
21 | "devDependencies": {
22 | "@babel/core": "~7.9.0",
23 | "@types/react": "~16.9.35",
24 | "@types/react-dom": "~16.9.8",
25 | "@types/react-native": "~0.63.2",
26 | "typescript": "~4.0.0"
27 | },
28 | "resolutions": {
29 | "node-fetch": "^2.6.1",
30 | "xmldom": "0.5.0"
31 | },
32 | "private": true
33 | }
34 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": true,
4 | "jsx": "react-native",
5 | "lib": ["dom", "esnext"],
6 | "moduleResolution": "node",
7 | "noEmit": true,
8 | "skipLibCheck": true,
9 | "resolveJsonModule": true,
10 | "strict": true
11 | }
12 | }
13 |
--------------------------------------------------------------------------------