React Native Collapsible Tab View:
import React, {useState, useEffect, useRef} from 'react';
import {
StyleSheet,
View,
Text,
Dimensions,
Animated,
PanResponder,
Platform,
TouchableOpacity,
Alert,
StatusBar,
} from 'react-native';
import {TabView, TabBar} from 'react-native-tab-view';
const windowHeight = Dimensions.get('window').height;
const windowWidth = Dimensions.get('window').width;
const TabBarHeight = 48;
const HeaderHeight = 300;
const SafeStatusBar = Platform.select({
ios: 44,
android: StatusBar.currentHeight,
});
const tab1ItemSize = (windowWidth - 30) / 2;
const tab2ItemSize = (windowWidth - 40) / 3;
const App = () => {
/**
* stats
*/
const [tabIndex, setIndex] = useState(0);
const [routes] = useState([
{key: 'tab1', title: 'Tab1'},
{key: 'tab2', title: 'Tab2'},
]);
const [canScroll, setCanScroll] = useState(true);
const [tab1Data] = useState(Array(40).fill(0));
const [tab2Data] = useState(Array(30).fill(0));
/**
* ref
*/
const scrollY = useRef(new Animated.Value(0)).current;
const headerScrollY = useRef(new Animated.Value(0)).current;
const listRefArr = useRef([]);
const listOffset = useRef({});
const isListGliding = useRef(false);
const headerScrollStart = useRef(0);
const _tabIndex = useRef(0);
/**
* PanResponder for header
*/
const headerPanResponder = useRef(
PanResponder.create({
onStartShouldSetPanResponderCapture: (evt, gestureState) => false,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => false,
onStartShouldSetPanResponder: (evt, gestureState) => {
headerScrollY.stopAnimation();
syncScrollOffset();
return false;
},
onMoveShouldSetPanResponder: (evt, gestureState) => {
headerScrollY.stopAnimation();
return Math.abs(gestureState.dy) > 5;
},
onPanResponderRelease: (evt, gestureState) => {
syncScrollOffset();
if (Math.abs(gestureState.vy) < 0.2) {
return;
}
headerScrollY.setValue(scrollY._value);
Animated.decay(headerScrollY, {
velocity: -gestureState.vy,
useNativeDriver: true,
}).start(() => {
syncScrollOffset();
});
},
onPanResponderMove: (evt, gestureState) => {
listRefArr.current.forEach(item => {
if (item.key !== routes[_tabIndex.current].key) {
return;
}
if (item.value) {
item.value.scrollToOffset({
offset: -gestureState.dy + headerScrollStart.current,
animated: false,
});
}
});
},
onShouldBlockNativeResponder: () => true,
onPanResponderGrant: (evt, gestureState) => {
headerScrollStart.current = scrollY._value;
},
}),
).current;
/**
* PanResponder for list in tab scene
*/
const listPanResponder = useRef(
PanResponder.create({
onStartShouldSetPanResponderCapture: (evt, gestureState) => false,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => false,
onStartShouldSetPanResponder: (evt, gestureState) => false,
onMoveShouldSetPanResponder: (evt, gestureState) => {
headerScrollY.stopAnimation();
return false;
},
onShouldBlockNativeResponder: () => true,
onPanResponderGrant: (evt, gestureState) => {
headerScrollY.stopAnimation();
},
}),
).current;
/**
* effect
*/
useEffect(() => {
scrollY.addListener(({value}) => {
const curRoute = routes[tabIndex].key;
listOffset.current[curRoute] = value;
});
headerScrollY.addListener(({value}) => {
listRefArr.current.forEach(item => {
if (item.key !== routes[tabIndex].key) {
return;
}
if (value > HeaderHeight || value < 0) {
headerScrollY.stopAnimation();
syncScrollOffset();
}
if (item.value && value <= HeaderHeight) {
item.value.scrollToOffset({
offset: value,
animated: false,
});
}
});
});
return () => {
scrollY.removeAllListeners();
headerScrollY.removeAllListeners();
};
}, [routes, tabIndex]);
/**
* helper functions
*/
const syncScrollOffset = () => {
const curRouteKey = routes[_tabIndex.current].key;
listRefArr.current.forEach(item => {
if (item.key !== curRouteKey) {
if (scrollY._value < HeaderHeight && scrollY._value >= 0) {
if (item.value) {
item.value.scrollToOffset({
offset: scrollY._value,
animated: false,
});
listOffset.current[item.key] = scrollY._value;
}
} else if (scrollY._value >= HeaderHeight) {
if (
listOffset.current[item.key] < HeaderHeight ||
listOffset.current[item.key] == null
) {
if (item.value) {
item.value.scrollToOffset({
offset: HeaderHeight,
animated: false,
});
listOffset.current[item.key] = HeaderHeight;
}
}
}
}
});
};
const onMomentumScrollBegin = () => {
isListGliding.current = true;
};
const onMomentumScrollEnd = () => {
isListGliding.current = false;
syncScrollOffset();
};
const onScrollEndDrag = () => {
syncScrollOffset();
};
/**
* render Helper
*/
const renderHeader = () => {
const y = scrollY.interpolate({
inputRange: [0, HeaderHeight],
outputRange: [0, -HeaderHeight],
extrapolate: 'clamp',
});
return (
<Animated.View
{...headerPanResponder.panHandlers}
style={[styles.header, {transform: [{translateY: y}]}]}>
<TouchableOpacity
style={{flex: 1, justifyContent: 'center'}}
activeOpacity={1}
onPress={() => Alert.alert('header Clicked!')}>
<Text>Scrollable Header</Text>
</TouchableOpacity>
</Animated.View>
);
};
const rednerTab1Item = ({item, index}) => {
return (
<View
style={{
borderRadius: 16,
marginLeft: index % 2 === 0 ? 0 : 10,
width: tab1ItemSize,
height: tab1ItemSize,
backgroundColor: '#aaa',
justifyContent: 'center',
alignItems: 'center',
}}>
<Text>{index}</Text>
</View>
);
};
const rednerTab2Item = ({item, index}) => {
return (
<View
style={{
marginLeft: index % 3 === 0 ? 0 : 10,
borderRadius: 16,
width: tab2ItemSize,
height: tab2ItemSize,
backgroundColor: '#aaa',
justifyContent: 'center',
alignItems: 'center',
}}>
<Text>{index}</Text>
</View>
);
};
const renderLabel = ({route, focused}) => {
return (
<Text style={[styles.label, {opacity: focused ? 1 : 0.5}]}>
{route.title}
</Text>
);
};
const renderScene = ({route}) => {
const focused = route.key === routes[tabIndex].key;
let numCols;
let data;
let renderItem;
switch (route.key) {
case 'tab1':
numCols = 2;
data = tab1Data;
renderItem = rednerTab1Item;
break;
case 'tab2':
numCols = 3;
data = tab2Data;
renderItem = rednerTab2Item;
break;
default:
return null;
}
return (
<Animated.FlatList
// scrollEnabled={canScroll}
{...listPanResponder.panHandlers}
numColumns={numCols}
ref={ref => {
if (ref) {
const found = listRefArr.current.find(e => e.key === route.key);
if (!found) {
listRefArr.current.push({
key: route.key,
value: ref,
});
}
}
}}
scrollEventThrottle={16}
onScroll={
focused
? Animated.event(
[
{
nativeEvent: {contentOffset: {y: scrollY}},
},
],
{useNativeDriver: true},
)
: null
}
onMomentumScrollBegin={onMomentumScrollBegin}
onScrollEndDrag={onScrollEndDrag}
onMomentumScrollEnd={onMomentumScrollEnd}
ItemSeparatorComponent={() => <View style={{height: 10}} />}
ListHeaderComponent={() => <View style={{height: 10}} />}
contentContainerStyle={{
paddingTop: HeaderHeight + TabBarHeight,
paddingHorizontal: 10,
minHeight: windowHeight - SafeStatusBar + HeaderHeight,
}}
showsHorizontalScrollIndicator={false}
data={data}
renderItem={renderItem}
showsVerticalScrollIndicator={false}
keyExtractor={(item, index) => index.toString()}
/>
);
};
const renderTabBar = props => {
const y = scrollY.interpolate({
inputRange: [0, HeaderHeight],
outputRange: [HeaderHeight, 0],
extrapolate: 'clamp',
});
return (
<Animated.View
style={{
top: 0,
zIndex: 1,
position: 'absolute',
transform: [{translateY: y}],
width: '100%',
}}>
<TabBar
{...props}
onTabPress={({route, preventDefault}) => {
if (isListGliding.current) {
preventDefault();
}
}}
style={styles.tab}
renderLabel={renderLabel}
indicatorStyle={styles.indicator}
/>
</Animated.View>
);
};
const renderTabView = () => {
return (
<TabView
onSwipeStart={() => setCanScroll(false)}
onSwipeEnd={() => setCanScroll(true)}
onIndexChange={id => {
_tabIndex.current = id;
setIndex(id);
}}
navigationState={{index: tabIndex, routes}}
renderScene={renderScene}
renderTabBar={renderTabBar}
initialLayout={{
height: 0,
width: windowWidth,
}}
/>
);
};
return (
<View style={styles.container}>
{renderTabView()}
{renderHeader()}
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
header: {
height: HeaderHeight,
width: '100%',
alignItems: 'center',
justifyContent: 'center',
position: 'absolute',
backgroundColor: '#40FFC4',
},
label: {fontSize: 16, color: '#222'},
tab: {
elevation: 0,
shadowOpacity: 0,
backgroundColor: '#FFCC80',
height: TabBarHeight,
},
indicator: {backgroundColor: '#222'},
});
export default App;
No comments:
Post a Comment