๐ Make tabs swipeable
Changes
5 files changed, +118 -87
ADD
app/(tabs)/_layout.swipe.tsx
+0 -0
MODIFY
app/(tabs)/_layout.tsx
+35 -87
@@ -1,93 +1,41 @@
1
-import { Tabs } from 'expo-router';
2
-import React from 'react';
3
-import { Ionicons } from '@expo/vector-icons';
4
-import { useSafeAreaInsets } from 'react-native-safe-area-context';
1
+import React, { useState } from 'react';
2
+import { Dimensions } from 'react-native';
3
+import { TabView, SceneMap } from 'react-native-tab-view';
4
+import InboxScreen from './index';
5
+import TodayScreen from './today';
6
+import CalendarScreen from './calendar';
7
+import ProjectsScreen from './projects';
8
+import SettingsScreen from './settings';
9
+import { SwipeableTabBar } from '@/components/swipeable-tab-bar';
5
10
6
-import { HapticTab } from '@/components/haptic-tab';
7
-import { Colors } from '@/constants/theme';
8
-import { useColorScheme } from '@/hooks/use-color-scheme';
11
+const renderScene = SceneMap({
12
+ inbox: InboxScreen,
13
+ today: TodayScreen,
14
+ calendar: CalendarScreen,
15
+ projects: ProjectsScreen,
16
+ settings: SettingsScreen,
17
+});
9
18
10
19
export default function TabLayout() {
11
- const colorScheme = useColorScheme();
12
- const insets = useSafeAreaInsets();
20
+ const [index, setIndex] = useState(0);
21
+ const [routes] = useState([
22
+ { key: 'inbox', title: 'Inbox' },
23
+ { key: 'today', title: 'Today' },
24
+ { key: 'calendar', title: 'Calendar' },
25
+ { key: 'projects', title: 'Projects' },
26
+ { key: 'settings', title: 'Settings' },
27
+ ]);
28
+
29
+ const initialLayout = { width: Dimensions.get('window').width };
13
30
14
31
return (
15
- <Tabs
16
- screenOptions={{
17
- tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
18
- headerShown: false,
19
- tabBarButton: HapticTab,
20
- tabBarStyle: {
21
- height: 60 + insets.bottom,
22
- paddingBottom: insets.bottom,
23
- paddingTop: 8,
24
- },
25
- }}>
26
- <Tabs.Screen
27
- name="index"
28
- options={{
29
- title: 'Inbox',
30
- tabBarIcon: ({ color, focused }) => (
31
- <Ionicons
32
- name={focused ? "mail" : "mail-outline"}
33
- size={24}
34
- color={color}
35
- />
36
- ),
37
- }}
38
- />
39
- <Tabs.Screen
40
- name="today"
41
- options={{
42
- title: 'Today',
43
- tabBarIcon: ({ color, focused }) => (
44
- <Ionicons
45
- name={focused ? "today" : "today-outline"}
46
- size={24}
47
- color={color}
48
- />
49
- ),
50
- }}
51
- />
52
- <Tabs.Screen
53
- name="calendar"
54
- options={{
55
- title: 'Calendar',
56
- tabBarIcon: ({ color, focused }) => (
57
- <Ionicons
58
- name={focused ? "calendar" : "calendar-outline"}
59
- size={24}
60
- color={color}
61
- />
62
- ),
63
- }}
64
- />
65
- <Tabs.Screen
66
- name="projects"
67
- options={{
68
- title: 'Projects',
69
- tabBarIcon: ({ color, focused }) => (
70
- <Ionicons
71
- name={focused ? "folder" : "folder-outline"}
72
- size={24}
73
- color={color}
74
- />
75
- ),
76
- }}
77
- />
78
- <Tabs.Screen
79
- name="settings"
80
- options={{
81
- title: 'Settings',
82
- tabBarIcon: ({ color, focused }) => (
83
- <Ionicons
84
- name={focused ? "settings" : "settings-outline"}
85
- size={24}
86
- color={color}
87
- />
88
- ),
89
- }}
90
- />
91
- </Tabs>
32
+ <TabView
33
+ navigationState={{ index, routes }}
34
+ renderScene={renderScene}
35
+ onIndexChange={setIndex}
36
+ initialLayout={initialLayout}
37
+ tabBarPosition="bottom"
38
+ renderTabBar={props => <SwipeableTabBar {...props} />}
39
+ />
92
40
);
93
-}
41
+}
ADD
components/swipeable-tab-bar.tsx
+54 -0
@@ -0,0 +1,54 @@
1
+import React from 'react';
2
+import { View, TouchableOpacity, StyleSheet } from 'react-native';
3
+import { Ionicons } from '@expo/vector-icons';
4
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
5
+import { ThemedText } from './themed-text';
6
+import { Colors } from '@/constants/theme';
7
+import { useColorScheme } from '@/hooks/use-color-scheme';
8
+
9
+const iconMap: { [key: string]: { focused: keyof typeof Ionicons.glyphMap; unfocused: keyof typeof Ionicons.glyphMap } } = {
10
+ inbox: { focused: 'mail', unfocused: 'mail-outline' },
11
+ today: { focused: 'today', unfocused: 'today-outline' },
12
+ calendar: { focused: 'calendar', unfocused: 'calendar-outline' },
13
+ projects: { focused: 'folder', unfocused: 'folder-outline' },
14
+ settings: { focused: 'settings', unfocused: 'settings-outline' },
15
+};
16
+
17
+export function SwipeableTabBar({ navigationState, position, jumpTo }) {
18
+ const colorScheme = useColorScheme();
19
+ const colors = Colors[colorScheme];
20
+ const insets = useSafeAreaInsets();
21
+
22
+ return (
23
+ <View style={[styles.tabBar, { height: 60 + insets.bottom, paddingBottom: insets.bottom }]}>
24
+ {navigationState.routes.map((route, i) => {
25
+ const isFocused = navigationState.index === i;
26
+ const iconName = isFocused ? iconMap[route.key].focused : iconMap[route.key].unfocused;
27
+ const color = isFocused ? colors.tint : colors.tabIconDefault;
28
+
29
+ return (
30
+ <TouchableOpacity
31
+ key={route.key}
32
+ onPress={() => jumpTo(route.key)}
33
+ style={styles.tabItem}
34
+ >
35
+ <Ionicons name={iconName} size={24} color={color} />
36
+ <ThemedText style={{ color, fontSize: 10 }}>{route.title}</ThemedText>
37
+ </TouchableOpacity>
38
+ );
39
+ })}
40
+ </View>
41
+ );
42
+}
43
+
44
+const styles = StyleSheet.create({
45
+ tabBar: {
46
+ flexDirection: 'row',
47
+ backgroundColor: 'transparent',
48
+ },
49
+ tabItem: {
50
+ flex: 1,
51
+ alignItems: 'center',
52
+ justifyContent: 'center',
53
+ },
54
+});
MODIFY
package-lock.json
+27 -0
@@ -34,9 +34,11 @@
34
34
"react-dom": "19.1.0",
35
35
"react-native": "0.81.5",
36
36
"react-native-gesture-handler": "~2.28.0",
37
+ "react-native-pager-view": "^6.9.1",
37
38
"react-native-reanimated": "~4.1.1",
38
39
"react-native-safe-area-context": "~5.6.0",
39
40
"react-native-screens": "~4.16.0",
41
+ "react-native-tab-view": "^4.2.0",
40
42
"react-native-web": "~0.21.0",
41
43
"react-native-worklets": "0.5.1",
42
44
"webdav": "^5.8.0"
@@ -10939,6 +10941,17 @@
10939
10941
"react-native": "*"
10940
10942
}
10941
10943
},
10944
+ "node_modules/react-native-pager-view": {
10945
+ "version": "6.9.1",
10946
+ "resolved": "https://registry.npmjs.org/react-native-pager-view/-/react-native-pager-view-6.9.1.tgz",
10947
+ "integrity": "sha512-uUT0MMMbNtoSbxe9pRvdJJKEi9snjuJ3fXlZhG8F2vVMOBJVt/AFtqMPUHu9yMflmqOr08PewKzj9EPl/Yj+Gw==",
10948
+ "license": "MIT",
10949
+ "peer": true,
10950
+ "peerDependencies": {
10951
+ "react": "*",
10952
+ "react-native": "*"
10953
+ }
10954
+ },
10942
10955
"node_modules/react-native-reanimated": {
10943
10956
"version": "4.1.3",
10944
10957
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.3.tgz",
@@ -10995,6 +11008,20 @@
10995
11008
"react-native": "*"
10996
11009
}
10997
11010
},
11011
+ "node_modules/react-native-tab-view": {
11012
+ "version": "4.2.0",
11013
+ "resolved": "https://registry.npmjs.org/react-native-tab-view/-/react-native-tab-view-4.2.0.tgz",
11014
+ "integrity": "sha512-TUbh7Yr0tE/99t1pJQLbQ+4/Px67xkT7/r3AhfV+93Q3WoUira0Lx7yuKUP2C118doqxub8NCLERwcqsHr29nQ==",
11015
+ "license": "MIT",
11016
+ "dependencies": {
11017
+ "use-latest-callback": "^0.2.4"
11018
+ },
11019
+ "peerDependencies": {
11020
+ "react": ">= 18.2.0",
11021
+ "react-native": "*",
11022
+ "react-native-pager-view": ">= 6.0.0"
11023
+ }
11024
+ },
10998
11025
"node_modules/react-native-web": {
10999
11026
"version": "0.21.2",
11000
11027
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz",
MODIFY
package.json
+2 -0
@@ -37,9 +37,11 @@
37
37
"react-dom": "19.1.0",
38
38
"react-native": "0.81.5",
39
39
"react-native-gesture-handler": "~2.28.0",
40
+ "react-native-pager-view": "^6.9.1",
40
41
"react-native-reanimated": "~4.1.1",
41
42
"react-native-safe-area-context": "~5.6.0",
42
43
"react-native-screens": "~4.16.0",
44
+ "react-native-tab-view": "^4.2.0",
43
45
"react-native-web": "~0.21.0",
44
46
"react-native-worklets": "0.5.1",
45
47
"webdav": "^5.8.0"