Skip to content

Instantly share code, notes, and snippets.

@maitham
Last active May 24, 2023 07:57
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save maitham/6e0841800d88bf9c289fc45bbc903b1d to your computer and use it in GitHub Desktop.
Save maitham/6e0841800d88bf9c289fc45bbc903b1d to your computer and use it in GitHub Desktop.
import React, { useState, useRef, useContext, useCallback } from 'react';
import { Dimensions, View, Animated } from 'react-native';
import { TabView } from 'react-native-tab-view';
import { TabScreen } from './Tab';
import { CustomTabBar } from './TabBar';
import { ThemeContext } from 'react-native-elements';
import { useHeaderHeight } from '@react-navigation/stack';
const AnimatedHeader = ({ style, content }) => {
return <Animated.View style={style}>{content}</Animated.View>;
};
const initialLayout = {
width: Dimensions.get('window').width,
height: Dimensions.get('window').height,
};
const HEADER_HEIGHT = 320;
const TABBAR_HEIGHT = 48;
const TOTAL_HEADER_HEIGHT = HEADER_HEIGHT + TABBAR_HEIGHT;
export const Tab: React.FC = ({ routeDefinitions, header, dataMap }) => {
const {
theme: { colors },
} = useContext(ThemeContext);
const tabRef = useRef(null);
const scrollY = useRef(new Animated.Value(0)).current;
const headerTranslateY = useRef(
scrollY.interpolate({
inputRange: [0, HEADER_HEIGHT],
outputRange: [0, -HEADER_HEIGHT],
extrapolate: 'clamp',
}),
).current;
const navHeaderHeight = useHeaderHeight();
// react-native-tab-view
const [index, setIndex] = useState(0);
const routes = routeDefinitions;
const listRefs = useRef([]);
const listRefsOffsets = useRef({});
const getFlatListRefs = (ref, route) => {
if (ref) {
const found = listRefs.current.find((e) => e.key === route.key);
if (!found) {
listRefs.current.push({
key: route.key,
value: ref,
});
}
}
};
const scrollTabToOffset = (item, y) => {
item.value.scrollToOffset({
offset: y,
animated: false,
});
};
const syncHiddenTab = (hiddenTab, currentTabOffsets) => {
const offsets = listRefsOffsets.current[hiddenTab.key];
if (!offsets?.offset && currentTabOffsets.offset > HEADER_HEIGHT) {
scrollTabToOffset(hiddenTab, HEADER_HEIGHT);
} else if (currentTabOffsets.offset >= HEADER_HEIGHT) {
scrollTabToOffset(hiddenTab, HEADER_HEIGHT);
} else if (currentTabOffsets.offset < HEADER_HEIGHT) {
scrollTabToOffset(hiddenTab, currentTabOffsets.offset);
}
};
const syncOffset = () => {
const curRouteKey = routes[index].key;
const currentTabOffsets = listRefsOffsets.current[curRouteKey];
listRefs.current.forEach((item) => {
if (item.key !== curRouteKey) {
syncHiddenTab(item, currentTabOffsets);
}
});
};
const renderScene = ({ route }) => {
const contentInset = Math.max(
initialLayout.height - dataMap[route.key].length * 75 - TABBAR_HEIGHT - navHeaderHeight,
0,
);
return (
<TabScreen
ref={(ref) => getFlatListRefs(ref, route)}
data={dataMap[route.key]}
itemType={route.key}
contentInset={contentInset === 0 ? 0 : contentInset}
scrollContentContainerStyle={{
paddingTop: TOTAL_HEADER_HEIGHT,
}}
onScroll={Animated.event([{ nativeEvent: { contentOffset: { y: scrollY } } }], {
useNativeDriver: true,
listener: (event) => {
listRefsOffsets.current[route.key] = { offset: event.nativeEvent.contentOffset.y };
},
})}
onScrollEndDrag={() => syncOffset()}
onMomentumScrollEnd={() => syncOffset()}
/>
);
};
const AnimatedTabBar = (props) => (
<Animated.View
style={{
position: 'absolute',
top: HEADER_HEIGHT,
left: 0,
right: 0,
zIndex: 1,
paddingLeft: 8,
paddingBottom: 10,
transform: [{ translateY: headerTranslateY }],
backgroundColor: colors.background,
}}
>
<CustomTabBar
tabBarStyle={{ marginTop: 10 }}
currentIndex={index}
onIndexChange={setIndex}
navigationState={props.navigationState}
ref={tabRef}
/>
</Animated.View>
);
return (
<>
<AnimatedHeader
style={{
position: 'absolute',
height: HEADER_HEIGHT,
transform: [{ translateY: headerTranslateY }],
zIndex: 1,
backgroundColor: colors.background,
}}
content={header}
/>
<View style={{ flex: 1 }}>
<TabView
navigationState={{ index, routes }}
renderScene={renderScene}
renderTabBar={(props) => <AnimatedTabBar {...props} />}
initialLayout={initialLayout}
onIndexChange={setIndex}
style={{ overflow: 'visible' }}
/>
</View>
</>
);
};
export const TabScreen: React.FC<TabProps> = React.memo(
React.forwardRef(
(
{
scrollContentContainerStyle,
onScroll,
onScrollEndDrag,
onMomentumScrollEnd,
data,
itemType,
contentInset,
},
ref,
) => {
return (
<AnimatedFlatList
ref={ref}
data={data}
renderItem={({ item }) => <Item item={item} itemType={itemType} />}
keyExtractor={keyExtractor}
scrollEventThrottle={1}
contentInset={{ bottom: contentInset }}
onScroll={onScroll}
showsVerticalScrollIndicator={false}
onScrollEndDrag={onScrollEndDrag}
onMomentumScrollEnd={onMomentumScrollEnd}
contentContainerStyle={[{}, scrollContentContainerStyle]}
/>
);
},
),
);
function keyExtractor(item: any, index: number): string {
return index.toString();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment