react-native-tab-view 3.2.1 → 3.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/{LICENSE.md → LICENSE} +0 -0
  2. package/README.md +5 -36
  3. package/lib/commonjs/PagerViewAdapter.js +2 -1
  4. package/lib/commonjs/PagerViewAdapter.js.map +1 -1
  5. package/lib/commonjs/PanResponderAdapter.js +2 -1
  6. package/lib/commonjs/PanResponderAdapter.js.map +1 -1
  7. package/lib/commonjs/SceneMap.js +9 -12
  8. package/lib/commonjs/SceneMap.js.map +1 -1
  9. package/lib/commonjs/SceneView.js +54 -101
  10. package/lib/commonjs/SceneView.js.map +1 -1
  11. package/lib/commonjs/TabBar.js +358 -325
  12. package/lib/commonjs/TabBar.js.map +1 -1
  13. package/lib/commonjs/TabBarIndicator.js +81 -99
  14. package/lib/commonjs/TabBarIndicator.js.map +1 -1
  15. package/lib/commonjs/TabBarItem.js +184 -161
  16. package/lib/commonjs/TabBarItem.js.map +1 -1
  17. package/lib/commonjs/TabView.js +2 -2
  18. package/lib/commonjs/TabView.js.map +1 -1
  19. package/lib/commonjs/index.js +3 -3
  20. package/lib/commonjs/index.js.map +1 -1
  21. package/lib/module/PagerViewAdapter.js +2 -1
  22. package/lib/module/PagerViewAdapter.js.map +1 -1
  23. package/lib/module/PanResponderAdapter.js +3 -2
  24. package/lib/module/PanResponderAdapter.js.map +1 -1
  25. package/lib/module/SceneMap.js +9 -14
  26. package/lib/module/SceneMap.js.map +1 -1
  27. package/lib/module/SceneView.js +54 -99
  28. package/lib/module/SceneView.js.map +1 -1
  29. package/lib/module/TabBar.js +355 -324
  30. package/lib/module/TabBar.js.map +1 -1
  31. package/lib/module/TabBarIndicator.js +75 -93
  32. package/lib/module/TabBarIndicator.js.map +1 -1
  33. package/lib/module/TabBarItem.js +178 -154
  34. package/lib/module/TabBarItem.js.map +1 -1
  35. package/lib/module/TabView.js +2 -2
  36. package/lib/module/TabView.js.map +1 -1
  37. package/lib/module/index.js +2 -2
  38. package/lib/module/index.js.map +1 -1
  39. package/lib/typescript/{Pager.android.d.ts → src/Pager.android.d.ts} +0 -0
  40. package/lib/typescript/{Pager.d.ts → src/Pager.d.ts} +0 -0
  41. package/lib/typescript/{Pager.ios.d.ts → src/Pager.ios.d.ts} +0 -0
  42. package/lib/typescript/{PagerViewAdapter.d.ts → src/PagerViewAdapter.d.ts} +1 -1
  43. package/lib/typescript/{PanResponderAdapter.d.ts → src/PanResponderAdapter.d.ts} +1 -1
  44. package/lib/typescript/{PlatformPressable.d.ts → src/PlatformPressable.d.ts} +0 -0
  45. package/lib/typescript/{SceneMap.d.ts → src/SceneMap.d.ts} +5 -3
  46. package/lib/typescript/src/SceneView.d.ts +15 -0
  47. package/lib/typescript/src/TabBar.d.ts +42 -0
  48. package/lib/typescript/src/TabBarIndicator.d.ts +12 -0
  49. package/lib/typescript/{TabBarItem.d.ts → src/TabBarItem.d.ts} +5 -7
  50. package/lib/typescript/{TabView.d.ts → src/TabView.d.ts} +1 -1
  51. package/lib/typescript/{index.d.ts → src/index.d.ts} +7 -7
  52. package/lib/typescript/{types.d.ts → src/types.d.ts} +0 -0
  53. package/lib/typescript/{useAnimatedValue.d.ts → src/useAnimatedValue.d.ts} +0 -0
  54. package/package.json +28 -58
  55. package/src/PagerViewAdapter.tsx +11 -5
  56. package/src/PanResponderAdapter.tsx +16 -12
  57. package/src/SceneMap.tsx +12 -7
  58. package/src/SceneView.tsx +73 -108
  59. package/src/TabBar.tsx +506 -401
  60. package/src/TabBarIndicator.tsx +114 -117
  61. package/src/TabBarItem.tsx +230 -200
  62. package/src/TabView.tsx +6 -5
  63. package/src/index.tsx +7 -12
  64. package/lib/typescript/SceneView.d.ts +0 -32
  65. package/lib/typescript/TabBar.d.ts +0 -72
  66. package/lib/typescript/TabBarIndicator.d.ts +0 -20
package/src/TabBar.tsx CHANGED
@@ -1,27 +1,31 @@
1
1
  import * as React from 'react';
2
2
  import {
3
3
  Animated,
4
+ FlatList,
5
+ I18nManager,
6
+ LayoutChangeEvent,
7
+ ListRenderItemInfo,
8
+ Platform,
9
+ StyleProp,
4
10
  StyleSheet,
11
+ TextStyle,
5
12
  View,
6
- StyleProp,
7
13
  ViewStyle,
8
- TextStyle,
9
- LayoutChangeEvent,
10
- I18nManager,
11
- Platform,
12
- FlatList,
13
- ListRenderItemInfo,
14
+ ViewToken,
14
15
  } from 'react-native';
15
- import TabBarItem, { Props as TabBarItemProps } from './TabBarItem';
16
+ import useLatestCallback from 'use-latest-callback';
17
+
16
18
  import TabBarIndicator, { Props as IndicatorProps } from './TabBarIndicator';
19
+ import TabBarItem, { Props as TabBarItemProps } from './TabBarItem';
17
20
  import type {
21
+ Event,
22
+ Layout,
23
+ NavigationState,
18
24
  Route,
19
25
  Scene,
20
26
  SceneRendererProps,
21
- NavigationState,
22
- Layout,
23
- Event,
24
27
  } from './types';
28
+ import useAnimatedValue from './useAnimatedValue';
25
29
 
26
30
  export type Props<T extends Route> = SceneRendererProps & {
27
31
  navigationState: NavigationState<T>;
@@ -31,10 +35,10 @@ export type Props<T extends Route> = SceneRendererProps & {
31
35
  inactiveColor?: string;
32
36
  pressColor?: string;
33
37
  pressOpacity?: number;
34
- getLabelText: (scene: Scene<T>) => string | undefined;
35
- getAccessible: (scene: Scene<T>) => boolean | undefined;
36
- getAccessibilityLabel: (scene: Scene<T>) => string | undefined;
37
- getTestID: (scene: Scene<T>) => string | undefined;
38
+ getLabelText?: (scene: Scene<T>) => string | undefined;
39
+ getAccessible?: (scene: Scene<T>) => boolean | undefined;
40
+ getAccessibilityLabel?: (scene: Scene<T>) => string | undefined;
41
+ getTestID?: (scene: Scene<T>) => string | undefined;
38
42
  renderLabel?: (
39
43
  scene: Scene<T> & {
40
44
  focused: boolean;
@@ -48,7 +52,7 @@ export type Props<T extends Route> = SceneRendererProps & {
48
52
  }
49
53
  ) => React.ReactNode;
50
54
  renderBadge?: (scene: Scene<T>) => React.ReactNode;
51
- renderIndicator: (props: IndicatorProps<T>) => React.ReactNode;
55
+ renderIndicator?: (props: IndicatorProps<T>) => React.ReactNode;
52
56
  renderTabBarItem?: (
53
57
  props: TabBarItemProps<T> & { key: string }
54
58
  ) => React.ReactElement;
@@ -61,437 +65,538 @@ export type Props<T extends Route> = SceneRendererProps & {
61
65
  contentContainerStyle?: StyleProp<ViewStyle>;
62
66
  style?: StyleProp<ViewStyle>;
63
67
  gap?: number;
68
+ testID?: string;
64
69
  };
65
70
 
66
- type State = {
67
- layout: Layout;
68
- tabWidths: { [key: string]: number };
69
- };
71
+ type FlattenedTabWidth = string | number | undefined;
70
72
 
71
73
  const Separator = ({ width }: { width: number }) => {
72
74
  return <View style={{ width }} />;
73
75
  };
74
76
 
75
- export default class TabBar<T extends Route> extends React.Component<
76
- Props<T>,
77
- State
78
- > {
79
- static defaultProps = {
80
- getLabelText: ({ route }: Scene<Route>) => route.title,
81
- getAccessible: ({ route }: Scene<Route>) =>
82
- typeof route.accessible !== 'undefined' ? route.accessible : true,
83
- getAccessibilityLabel: ({ route }: Scene<Route>) =>
84
- typeof route.accessibilityLabel === 'string'
85
- ? route.accessibilityLabel
86
- : typeof route.title === 'string'
87
- ? route.title
88
- : undefined,
89
- getTestID: ({ route }: Scene<Route>) => route.testID,
90
- renderIndicator: (props: IndicatorProps<Route>) => (
91
- <TabBarIndicator {...props} />
92
- ),
93
- gap: 0,
94
- };
77
+ const getFlattenedTabWidth = (style: StyleProp<ViewStyle>) => {
78
+ const tabStyle = StyleSheet.flatten(style);
95
79
 
96
- state: State = {
97
- layout: { width: 0, height: 0 },
98
- tabWidths: {},
99
- };
80
+ return tabStyle?.width;
81
+ };
100
82
 
101
- componentDidUpdate(prevProps: Props<T>, prevState: State) {
102
- const { navigationState } = this.props;
103
- const { layout, tabWidths } = this.state;
104
-
105
- if (
106
- prevProps.navigationState.routes.length !==
107
- navigationState.routes.length ||
108
- prevProps.navigationState.index !== navigationState.index ||
109
- prevState.layout.width !== layout.width ||
110
- prevState.tabWidths !== tabWidths
111
- ) {
112
- if (
113
- this.getFlattenedTabWidth(this.props.tabStyle) === 'auto' &&
114
- !(
115
- layout.width &&
116
- navigationState.routes.every(
117
- (r) => typeof tabWidths[r.key] === 'number'
118
- )
119
- )
120
- ) {
121
- // When tab width is dynamic, only adjust the scroll once we have all tab widths and layout
122
- return;
83
+ const getComputedTabWidth = (
84
+ index: number,
85
+ layout: Layout,
86
+ routes: Route[],
87
+ scrollEnabled: boolean | undefined,
88
+ tabWidths: { [key: string]: number },
89
+ flattenedWidth: FlattenedTabWidth
90
+ ) => {
91
+ if (flattenedWidth === 'auto') {
92
+ return tabWidths[routes[index].key] || 0;
93
+ }
94
+
95
+ switch (typeof flattenedWidth) {
96
+ case 'number':
97
+ return flattenedWidth;
98
+ case 'string':
99
+ if (flattenedWidth.endsWith('%')) {
100
+ const width = parseFloat(flattenedWidth);
101
+ if (Number.isFinite(width)) {
102
+ return layout.width * (width / 100);
103
+ }
123
104
  }
105
+ }
124
106
 
125
- this.resetScroll(navigationState.index);
126
- }
107
+ if (scrollEnabled) {
108
+ return (layout.width / 5) * 2;
127
109
  }
110
+ return layout.width / routes.length;
111
+ };
128
112
 
129
- // to store the layout.width of each tab
130
- // when all onLayout's are fired, this would be set in state
131
- private measuredTabWidths: { [key: string]: number } = {};
113
+ const getMaxScrollDistance = (tabBarWidth: number, layoutWidth: number) =>
114
+ tabBarWidth - layoutWidth;
115
+
116
+ const getTranslateX = (
117
+ scrollAmount: Animated.Value,
118
+ maxScrollDistance: number
119
+ ) =>
120
+ Animated.multiply(
121
+ Platform.OS === 'android' && I18nManager.isRTL
122
+ ? Animated.add(maxScrollDistance, Animated.multiply(scrollAmount, -1))
123
+ : scrollAmount,
124
+ I18nManager.isRTL ? 1 : -1
125
+ );
126
+
127
+ const getTabBarWidth = <T extends Route>({
128
+ navigationState,
129
+ layout,
130
+ gap,
131
+ scrollEnabled,
132
+ flattenedTabWidth,
133
+ tabWidths,
134
+ }: Pick<Props<T>, 'navigationState' | 'gap' | 'layout' | 'scrollEnabled'> & {
135
+ tabWidths: Record<string, number>;
136
+ flattenedTabWidth: FlattenedTabWidth;
137
+ }) => {
138
+ const { routes } = navigationState;
139
+
140
+ return routes.reduce<number>(
141
+ (acc, _, i) =>
142
+ acc +
143
+ (i > 0 ? gap ?? 0 : 0) +
144
+ getComputedTabWidth(
145
+ i,
146
+ layout,
147
+ routes,
148
+ scrollEnabled,
149
+ tabWidths,
150
+ flattenedTabWidth
151
+ ),
152
+ 0
153
+ );
154
+ };
132
155
 
133
- private scrollAmount = new Animated.Value(0);
156
+ const normalizeScrollValue = <T extends Route>({
157
+ layout,
158
+ navigationState,
159
+ gap,
160
+ scrollEnabled,
161
+ tabWidths,
162
+ value,
163
+ flattenedTabWidth,
164
+ }: Pick<Props<T>, 'layout' | 'navigationState' | 'gap' | 'scrollEnabled'> & {
165
+ tabWidths: Record<string, number>;
166
+ value: number;
167
+ flattenedTabWidth: FlattenedTabWidth;
168
+ }) => {
169
+ const tabBarWidth = getTabBarWidth({
170
+ layout,
171
+ navigationState,
172
+ tabWidths,
173
+ gap,
174
+ scrollEnabled,
175
+ flattenedTabWidth,
176
+ });
177
+ const maxDistance = getMaxScrollDistance(tabBarWidth, layout.width);
178
+ const scrollValue = Math.max(Math.min(value, maxDistance), 0);
179
+
180
+ if (Platform.OS === 'android' && I18nManager.isRTL) {
181
+ // On Android, scroll value is not applied in reverse in RTL
182
+ // so we need to manually adjust it to apply correct value
183
+ return maxDistance - scrollValue;
184
+ }
134
185
 
135
- private flatListRef = React.createRef<FlatList>();
186
+ return scrollValue;
187
+ };
136
188
 
137
- private getFlattenedTabWidth = (style: StyleProp<ViewStyle>) => {
138
- const tabStyle = StyleSheet.flatten(style);
189
+ const getScrollAmount = <T extends Route>({
190
+ layout,
191
+ navigationState,
192
+ gap,
193
+ scrollEnabled,
194
+ flattenedTabWidth,
195
+ tabWidths,
196
+ }: Pick<Props<T>, 'layout' | 'navigationState' | 'scrollEnabled' | 'gap'> & {
197
+ tabWidths: Record<string, number>;
198
+ flattenedTabWidth: FlattenedTabWidth;
199
+ }) => {
200
+ const centerDistance = Array.from({
201
+ length: navigationState.index + 1,
202
+ }).reduce<number>((total, _, i) => {
203
+ const tabWidth = getComputedTabWidth(
204
+ i,
205
+ layout,
206
+ navigationState.routes,
207
+ scrollEnabled,
208
+ tabWidths,
209
+ flattenedTabWidth
210
+ );
139
211
 
140
- return tabStyle ? tabStyle.width : undefined;
141
- };
212
+ // To get the current index centered we adjust scroll amount by width of indexes
213
+ // 0 through (i - 1) and add half the width of current index i
214
+ return (
215
+ total +
216
+ (navigationState.index === i
217
+ ? (tabWidth + (gap ?? 0)) / 2
218
+ : tabWidth + (gap ?? 0))
219
+ );
220
+ }, 0);
221
+
222
+ const scrollAmount = centerDistance - layout.width / 2;
223
+
224
+ return normalizeScrollValue({
225
+ layout,
226
+ navigationState,
227
+ tabWidths,
228
+ value: scrollAmount,
229
+ gap,
230
+ scrollEnabled,
231
+ flattenedTabWidth,
232
+ });
233
+ };
142
234
 
143
- private getComputedTabWidth = (
144
- index: number,
145
- layout: Layout,
146
- routes: Route[],
147
- scrollEnabled: boolean | undefined,
148
- tabWidths: { [key: string]: number },
149
- flattenedWidth: string | number | undefined
150
- ) => {
151
- if (flattenedWidth === 'auto') {
152
- return tabWidths[routes[index].key] || 0;
235
+ const getLabelTextDefault = ({ route }: Scene<Route>) => route.title;
236
+
237
+ const getAccessibleDefault = ({ route }: Scene<Route>) =>
238
+ typeof route.accessible !== 'undefined' ? route.accessible : true;
239
+
240
+ const getAccessibilityLabelDefault = ({ route }: Scene<Route>) =>
241
+ typeof route.accessibilityLabel === 'string'
242
+ ? route.accessibilityLabel
243
+ : typeof route.title === 'string'
244
+ ? route.title
245
+ : undefined;
246
+
247
+ const renderIndicatorDefault = (props: IndicatorProps<Route>) => (
248
+ <TabBarIndicator {...props} />
249
+ );
250
+
251
+ const getTestIdDefault = ({ route }: Scene<Route>) => route.testID;
252
+
253
+ // How many items measurements should we update per batch.
254
+ // Defaults to 10, since that's whats FlatList is using in initialNumToRender.
255
+ const MEASURE_PER_BATCH = 10;
256
+
257
+ export default function TabBar<T extends Route>({
258
+ getLabelText = getLabelTextDefault,
259
+ getAccessible = getAccessibleDefault,
260
+ getAccessibilityLabel = getAccessibilityLabelDefault,
261
+ getTestID = getTestIdDefault,
262
+ renderIndicator = renderIndicatorDefault,
263
+ gap = 0,
264
+ scrollEnabled,
265
+ jumpTo,
266
+ navigationState,
267
+ position,
268
+ activeColor,
269
+ bounces,
270
+ contentContainerStyle,
271
+ inactiveColor,
272
+ indicatorContainerStyle,
273
+ indicatorStyle,
274
+ labelStyle,
275
+ onTabLongPress,
276
+ onTabPress,
277
+ pressColor,
278
+ pressOpacity,
279
+ renderBadge,
280
+ renderIcon,
281
+ renderLabel,
282
+ renderTabBarItem,
283
+ style,
284
+ tabStyle,
285
+ testID,
286
+ }: Props<T>) {
287
+ const [layout, setLayout] = React.useState<Layout>({ width: 0, height: 0 });
288
+ const [tabWidths, setTabWidths] = React.useState<Record<string, number>>({});
289
+ const flatListRef = React.useRef<FlatList | null>(null);
290
+ const isFirst = React.useRef(true);
291
+ const scrollAmount = useAnimatedValue(0);
292
+ const measuredTabWidths = React.useRef<Record<string, number>>({});
293
+
294
+ const { routes } = navigationState;
295
+ const flattenedTabWidth = getFlattenedTabWidth(tabStyle);
296
+ const isWidthDynamic = flattenedTabWidth === 'auto';
297
+ const scrollOffset = getScrollAmount({
298
+ layout,
299
+ navigationState,
300
+ tabWidths,
301
+ gap,
302
+ scrollEnabled,
303
+ flattenedTabWidth,
304
+ });
305
+
306
+ const hasMeasuredTabWidths =
307
+ Boolean(layout.width) &&
308
+ routes
309
+ .slice(0, navigationState.index)
310
+ .every((r) => typeof tabWidths[r.key] === 'number');
311
+
312
+ React.useEffect(() => {
313
+ if (isFirst.current) {
314
+ isFirst.current = false;
315
+ return;
153
316
  }
154
317
 
155
- switch (typeof flattenedWidth) {
156
- case 'number':
157
- return flattenedWidth;
158
- case 'string':
159
- if (flattenedWidth.endsWith('%')) {
160
- const width = parseFloat(flattenedWidth);
161
- if (Number.isFinite(width)) {
162
- return layout.width * (width / 100);
163
- }
164
- }
318
+ if (isWidthDynamic && !hasMeasuredTabWidths) {
319
+ return;
165
320
  }
166
321
 
167
322
  if (scrollEnabled) {
168
- return (layout.width / 5) * 2;
169
- }
170
- return layout.width / routes.length;
171
- };
172
-
173
- private getMaxScrollDistance = (tabBarWidth: number, layoutWidth: number) =>
174
- tabBarWidth - layoutWidth;
175
-
176
- private getTabBarWidth = (props: Props<T>, state: State) => {
177
- const { layout, tabWidths } = state;
178
- const { scrollEnabled, tabStyle } = props;
179
- const { routes } = props.navigationState;
180
-
181
- return routes.reduce<number>(
182
- (acc, _, i) =>
183
- acc +
184
- (i > 0 ? props.gap ?? 0 : 0) +
185
- this.getComputedTabWidth(
186
- i,
187
- layout,
188
- routes,
189
- scrollEnabled,
190
- tabWidths,
191
- this.getFlattenedTabWidth(tabStyle)
192
- ),
193
- 0
194
- );
195
- };
196
-
197
- private normalizeScrollValue = (
198
- props: Props<T>,
199
- state: State,
200
- value: number
201
- ) => {
202
- const { layout } = state;
203
- const tabBarWidth = this.getTabBarWidth(props, state);
204
- const maxDistance = this.getMaxScrollDistance(tabBarWidth, layout.width);
205
- const scrollValue = Math.max(Math.min(value, maxDistance), 0);
206
-
207
- if (Platform.OS === 'android' && I18nManager.isRTL) {
208
- // On Android, scroll value is not applied in reverse in RTL
209
- // so we need to manually adjust it to apply correct value
210
- return maxDistance - scrollValue;
211
- }
212
-
213
- return scrollValue;
214
- };
215
-
216
- private getScrollAmount = (props: Props<T>, state: State, index: number) => {
217
- const { layout, tabWidths } = state;
218
- const { scrollEnabled, tabStyle } = props;
219
- const { routes } = props.navigationState;
220
-
221
- const centerDistance = Array.from({ length: index + 1 }).reduce<number>(
222
- (total, _, i) => {
223
- const tabWidth = this.getComputedTabWidth(
224
- i,
225
- layout,
226
- routes,
227
- scrollEnabled,
228
- tabWidths,
229
- this.getFlattenedTabWidth(tabStyle)
230
- );
231
-
232
- // To get the current index centered we adjust scroll amount by width of indexes
233
- // 0 through (i - 1) and add half the width of current index i
234
- return (
235
- total +
236
- (index === i
237
- ? (tabWidth + (props.gap ?? 0)) / 2
238
- : tabWidth + (props.gap ?? 0))
239
- );
240
- },
241
- 0
242
- );
243
-
244
- const scrollAmount = centerDistance - layout.width / 2;
245
-
246
- return this.normalizeScrollValue(props, state, scrollAmount);
247
- };
248
-
249
- private resetScroll = (index: number) => {
250
- if (this.props.scrollEnabled) {
251
- this.flatListRef.current?.scrollToOffset({
252
- offset: this.getScrollAmount(this.props, this.state, index),
323
+ flatListRef.current?.scrollToOffset({
324
+ offset: scrollOffset,
253
325
  animated: true,
254
326
  });
255
327
  }
256
- };
328
+ }, [hasMeasuredTabWidths, isWidthDynamic, scrollEnabled, scrollOffset]);
257
329
 
258
- private handleLayout = (e: LayoutChangeEvent) => {
330
+ const handleLayout = (e: LayoutChangeEvent) => {
259
331
  const { height, width } = e.nativeEvent.layout;
260
332
 
261
- if (
262
- this.state.layout.width === width &&
263
- this.state.layout.height === height
264
- ) {
265
- return;
266
- }
267
-
268
- this.setState({
269
- layout: {
270
- height,
271
- width,
272
- },
273
- });
333
+ setLayout((layout) =>
334
+ layout.width === width && layout.height === height
335
+ ? layout
336
+ : { width, height }
337
+ );
274
338
  };
275
339
 
276
- private getTranslateX = (
277
- scrollAmount: Animated.Value,
278
- maxScrollDistance: number
279
- ) =>
280
- Animated.multiply(
281
- Platform.OS === 'android' && I18nManager.isRTL
282
- ? Animated.add(maxScrollDistance, Animated.multiply(scrollAmount, -1))
283
- : scrollAmount,
284
- I18nManager.isRTL ? 1 : -1
285
- );
340
+ const tabBarWidth = getTabBarWidth({
341
+ layout,
342
+ navigationState,
343
+ tabWidths,
344
+ gap,
345
+ scrollEnabled,
346
+ flattenedTabWidth,
347
+ });
348
+
349
+ const separatorsWidth = Math.max(0, routes.length - 1) * gap;
350
+ const separatorPercent = (separatorsWidth / tabBarWidth) * 100;
351
+ const tabBarWidthPercent = `${routes.length * 40}%`;
352
+
353
+ const translateX = React.useMemo(
354
+ () =>
355
+ getTranslateX(
356
+ scrollAmount,
357
+ getMaxScrollDistance(tabBarWidth, layout.width)
358
+ ),
359
+ [layout.width, scrollAmount, tabBarWidth]
360
+ );
361
+
362
+ const renderItem = React.useCallback(
363
+ ({ item: route, index }: ListRenderItemInfo<T>) => {
364
+ const props: TabBarItemProps<T> & { key: string } = {
365
+ key: route.key,
366
+ position: position,
367
+ route: route,
368
+ navigationState: navigationState,
369
+ getAccessibilityLabel: getAccessibilityLabel,
370
+ getAccessible: getAccessible,
371
+ getLabelText: getLabelText,
372
+ getTestID: getTestID,
373
+ renderBadge: renderBadge,
374
+ renderIcon: renderIcon,
375
+ renderLabel: renderLabel,
376
+ activeColor: activeColor,
377
+ inactiveColor: inactiveColor,
378
+ pressColor: pressColor,
379
+ pressOpacity: pressOpacity,
380
+ onLayout: isWidthDynamic
381
+ ? (e: LayoutChangeEvent) => {
382
+ measuredTabWidths.current[route.key] = e.nativeEvent.layout.width;
383
+
384
+ // When we have measured widths for all of the tabs, we should updates the state
385
+ // We avoid doing separate setState for each layout since it triggers multiple renders and slows down app
386
+ // If we have more than 10 routes divide updating tabWidths into multiple batches. Here we update only first batch of 10 items.
387
+ if (
388
+ routes.length > MEASURE_PER_BATCH &&
389
+ index === MEASURE_PER_BATCH &&
390
+ routes
391
+ .slice(0, MEASURE_PER_BATCH)
392
+ .every(
393
+ (r) => typeof measuredTabWidths.current[r.key] === 'number'
394
+ )
395
+ ) {
396
+ setTabWidths({ ...measuredTabWidths.current });
397
+ } else if (
398
+ routes.every(
399
+ (r) => typeof measuredTabWidths.current[r.key] === 'number'
400
+ )
401
+ ) {
402
+ // When we have measured widths for all of the tabs, we should updates the state
403
+ // We avoid doing separate setState for each layout since it triggers multiple renders and slows down app
404
+ setTabWidths({ ...measuredTabWidths.current });
405
+ }
406
+ }
407
+ : undefined,
408
+ onPress: () => {
409
+ const event: Scene<T> & Event = {
410
+ route,
411
+ defaultPrevented: false,
412
+ preventDefault: () => {
413
+ event.defaultPrevented = true;
414
+ },
415
+ };
416
+
417
+ onTabPress?.(event);
418
+
419
+ if (event.defaultPrevented) {
420
+ return;
421
+ }
286
422
 
287
- render() {
288
- const {
289
- position,
290
- navigationState,
291
- jumpTo,
292
- scrollEnabled,
293
- bounces,
423
+ jumpTo(route.key);
424
+ },
425
+ onLongPress: () => onTabLongPress?.({ route }),
426
+ labelStyle: labelStyle,
427
+ style: tabStyle,
428
+ // Calculate the deafult width for tab for FlatList to work
429
+ defaultTabWidth: !isWidthDynamic
430
+ ? getComputedTabWidth(
431
+ index,
432
+ layout,
433
+ routes,
434
+ scrollEnabled,
435
+ tabWidths,
436
+ getFlattenedTabWidth(tabStyle)
437
+ )
438
+ : undefined,
439
+ };
440
+
441
+ return (
442
+ <>
443
+ {gap > 0 && index > 0 ? <Separator width={gap} /> : null}
444
+ {renderTabBarItem ? (
445
+ renderTabBarItem(props)
446
+ ) : (
447
+ <TabBarItem {...props} />
448
+ )}
449
+ </>
450
+ );
451
+ },
452
+ [
453
+ activeColor,
454
+ gap,
294
455
  getAccessibilityLabel,
295
456
  getAccessible,
296
457
  getLabelText,
297
458
  getTestID,
459
+ inactiveColor,
460
+ isWidthDynamic,
461
+ jumpTo,
462
+ labelStyle,
463
+ layout,
464
+ navigationState,
465
+ onTabLongPress,
466
+ onTabPress,
467
+ position,
468
+ pressColor,
469
+ pressOpacity,
298
470
  renderBadge,
299
471
  renderIcon,
300
472
  renderLabel,
301
473
  renderTabBarItem,
302
- activeColor,
303
- inactiveColor,
304
- pressColor,
305
- pressOpacity,
306
- onTabPress,
307
- onTabLongPress,
474
+ routes,
475
+ scrollEnabled,
308
476
  tabStyle,
309
- labelStyle,
310
- indicatorStyle,
477
+ tabWidths,
478
+ ]
479
+ );
480
+
481
+ const keyExtractor = React.useCallback((item: T) => item.key, []);
482
+
483
+ const contentContainerStyleMemoized = React.useMemo(
484
+ () => [
485
+ styles.tabContent,
486
+ scrollEnabled
487
+ ? {
488
+ width:
489
+ tabBarWidth > separatorsWidth ? tabBarWidth : tabBarWidthPercent,
490
+ }
491
+ : styles.container,
311
492
  contentContainerStyle,
312
- style,
313
- indicatorContainerStyle,
314
- gap = 0,
315
- } = this.props;
316
- const { layout, tabWidths } = this.state;
317
- const { routes } = navigationState;
318
-
319
- const isWidthDynamic = this.getFlattenedTabWidth(tabStyle) === 'auto';
320
- const tabBarWidth = this.getTabBarWidth(this.props, this.state);
321
- const separatorsWidth = Math.max(0, routes.length - 1) * gap;
322
- const separatorPercent = (separatorsWidth / tabBarWidth) * 100;
323
-
324
- const tabBarWidthPercent = `${routes.length * 40}%`;
325
- const translateX = this.getTranslateX(
326
- this.scrollAmount,
327
- this.getMaxScrollDistance(tabBarWidth, layout.width)
328
- );
493
+ ],
494
+ [
495
+ contentContainerStyle,
496
+ scrollEnabled,
497
+ separatorsWidth,
498
+ tabBarWidth,
499
+ tabBarWidthPercent,
500
+ ]
501
+ );
502
+
503
+ const handleScroll = React.useMemo(
504
+ () =>
505
+ Animated.event(
506
+ [
507
+ {
508
+ nativeEvent: {
509
+ contentOffset: { x: scrollAmount },
510
+ },
511
+ },
512
+ ],
513
+ { useNativeDriver: true }
514
+ ),
515
+ [scrollAmount]
516
+ );
517
+
518
+ const handleViewableItemsChanged = useLatestCallback(
519
+ ({ changed }: { changed: ViewToken[] }) => {
520
+ if (routes.length <= MEASURE_PER_BATCH) {
521
+ return;
522
+ }
523
+ // Get next vievable item
524
+ const item = changed[changed.length - 1];
525
+ const index = item?.index || 0;
526
+ if (
527
+ item.isViewable &&
528
+ (index % 10 === 0 ||
529
+ index === navigationState.index ||
530
+ index === routes.length - 1)
531
+ ) {
532
+ setTabWidths({ ...measuredTabWidths.current });
533
+ }
534
+ }
535
+ );
329
536
 
330
- return (
537
+ return (
538
+ <Animated.View onLayout={handleLayout} style={[styles.tabBar, style]}>
331
539
  <Animated.View
332
- onLayout={this.handleLayout}
333
- style={[styles.tabBar, style]}
540
+ pointerEvents="none"
541
+ style={[
542
+ styles.indicatorContainer,
543
+ scrollEnabled ? { transform: [{ translateX }] as any } : null,
544
+ tabBarWidth > separatorsWidth
545
+ ? { width: tabBarWidth - separatorsWidth }
546
+ : scrollEnabled
547
+ ? { width: tabBarWidthPercent }
548
+ : null,
549
+ indicatorContainerStyle,
550
+ ]}
334
551
  >
335
- <Animated.View
336
- pointerEvents="none"
337
- style={[
338
- styles.indicatorContainer,
339
- scrollEnabled ? { transform: [{ translateX }] as any } : null,
340
- tabBarWidth > separatorsWidth
341
- ? { width: tabBarWidth - separatorsWidth }
342
- : scrollEnabled
343
- ? { width: tabBarWidthPercent }
344
- : null,
345
- indicatorContainerStyle,
346
- ]}
347
- >
348
- {this.props.renderIndicator({
349
- position,
350
- layout,
351
- navigationState,
352
- jumpTo,
353
- width: isWidthDynamic
354
- ? 'auto'
355
- : `${(100 - separatorPercent) / routes.length}%`,
356
- style: indicatorStyle,
357
- getTabWidth: (i: number) =>
358
- this.getComputedTabWidth(
359
- i,
360
- layout,
361
- routes,
362
- scrollEnabled,
363
- tabWidths,
364
- this.getFlattenedTabWidth(tabStyle)
365
- ),
366
- gap,
367
- })}
368
- </Animated.View>
369
- <View style={styles.scroll}>
370
- <Animated.FlatList
371
- data={routes as Animated.WithAnimatedValue<T>[]}
372
- keyExtractor={(item) => item.key}
373
- horizontal
374
- accessibilityRole="tablist"
375
- keyboardShouldPersistTaps="handled"
376
- scrollEnabled={scrollEnabled}
377
- bounces={bounces}
378
- alwaysBounceHorizontal={false}
379
- scrollsToTop={false}
380
- showsHorizontalScrollIndicator={false}
381
- showsVerticalScrollIndicator={false}
382
- automaticallyAdjustContentInsets={false}
383
- overScrollMode="never"
384
- contentContainerStyle={[
385
- styles.tabContent,
386
- scrollEnabled
387
- ? {
388
- width:
389
- tabBarWidth > separatorsWidth
390
- ? tabBarWidth
391
- : tabBarWidthPercent,
392
- }
393
- : styles.container,
394
- contentContainerStyle,
395
- ]}
396
- scrollEventThrottle={16}
397
- renderItem={({ item: route, index }: ListRenderItemInfo<T>) => {
398
- const props: TabBarItemProps<T> & { key: string } = {
399
- key: route.key,
400
- position: position,
401
- route: route,
402
- navigationState: navigationState,
403
- getAccessibilityLabel: getAccessibilityLabel,
404
- getAccessible: getAccessible,
405
- getLabelText: getLabelText,
406
- getTestID: getTestID,
407
- renderBadge: renderBadge,
408
- renderIcon: renderIcon,
409
- renderLabel: renderLabel,
410
- activeColor: activeColor,
411
- inactiveColor: inactiveColor,
412
- pressColor: pressColor,
413
- pressOpacity: pressOpacity,
414
- onLayout: isWidthDynamic
415
- ? (e) => {
416
- this.measuredTabWidths[route.key] =
417
- e.nativeEvent.layout.width;
418
-
419
- // When we have measured widths for all of the tabs, we should updates the state
420
- // We avoid doing separate setState for each layout since it triggers multiple renders and slows down app
421
- if (
422
- routes.every(
423
- (r) =>
424
- typeof this.measuredTabWidths[r.key] === 'number'
425
- )
426
- ) {
427
- this.setState({
428
- tabWidths: { ...this.measuredTabWidths },
429
- });
430
- }
431
- }
432
- : undefined,
433
- onPress: () => {
434
- const event: Scene<T> & Event = {
435
- route,
436
- defaultPrevented: false,
437
- preventDefault: () => {
438
- event.defaultPrevented = true;
439
- },
440
- };
441
-
442
- onTabPress?.(event);
443
-
444
- if (event.defaultPrevented) {
445
- return;
446
- }
447
-
448
- this.props.jumpTo(route.key);
449
- },
450
- onLongPress: () => onTabLongPress?.({ route }),
451
- labelStyle: labelStyle,
452
- style: [
453
- tabStyle,
454
- // Calculate the deafult width for tab for FlatList to work.
455
- this.getFlattenedTabWidth(tabStyle) === undefined && {
456
- width: this.getComputedTabWidth(
457
- index,
458
- layout,
459
- routes,
460
- scrollEnabled,
461
- tabWidths,
462
- this.getFlattenedTabWidth(tabStyle)
463
- ),
464
- },
465
- ],
466
- };
467
-
468
- return (
469
- <React.Fragment key={route.key}>
470
- {gap > 0 && index > 0 ? <Separator width={gap} /> : null}
471
- {renderTabBarItem ? (
472
- renderTabBarItem(props)
473
- ) : (
474
- <TabBarItem {...props} />
475
- )}
476
- </React.Fragment>
477
- );
478
- }}
479
- onScroll={Animated.event(
480
- [
481
- {
482
- nativeEvent: {
483
- contentOffset: { x: this.scrollAmount },
484
- },
485
- },
486
- ],
487
- { useNativeDriver: true }
488
- )}
489
- ref={this.flatListRef}
490
- />
491
- </View>
552
+ {renderIndicator({
553
+ position,
554
+ layout,
555
+ navigationState,
556
+ jumpTo,
557
+ width: isWidthDynamic
558
+ ? 'auto'
559
+ : `${(100 - separatorPercent) / routes.length}%`,
560
+ style: indicatorStyle,
561
+ getTabWidth: (i: number) =>
562
+ getComputedTabWidth(
563
+ i,
564
+ layout,
565
+ routes,
566
+ scrollEnabled,
567
+ tabWidths,
568
+ flattenedTabWidth
569
+ ),
570
+ gap,
571
+ })}
492
572
  </Animated.View>
493
- );
494
- }
573
+ <View style={styles.scroll}>
574
+ <Animated.FlatList
575
+ data={routes as Animated.WithAnimatedValue<T>[]}
576
+ keyExtractor={keyExtractor}
577
+ horizontal
578
+ accessibilityRole="tablist"
579
+ keyboardShouldPersistTaps="handled"
580
+ scrollEnabled={scrollEnabled}
581
+ bounces={bounces}
582
+ initialNumToRender={MEASURE_PER_BATCH}
583
+ onViewableItemsChanged={handleViewableItemsChanged}
584
+ alwaysBounceHorizontal={false}
585
+ scrollsToTop={false}
586
+ showsHorizontalScrollIndicator={false}
587
+ showsVerticalScrollIndicator={false}
588
+ automaticallyAdjustContentInsets={false}
589
+ overScrollMode="never"
590
+ contentContainerStyle={contentContainerStyleMemoized}
591
+ scrollEventThrottle={16}
592
+ renderItem={renderItem}
593
+ onScroll={handleScroll}
594
+ ref={flatListRef}
595
+ testID={testID}
596
+ />
597
+ </View>
598
+ </Animated.View>
599
+ );
495
600
  }
496
601
 
497
602
  const styles = StyleSheet.create({