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