react-native-tab-view 5.0.0-alpha.1 → 5.0.0-alpha.11

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 (108) hide show
  1. package/lib/module/DefaultAdapter.android.js +4 -0
  2. package/lib/module/DefaultAdapter.android.js.map +1 -0
  3. package/lib/module/DefaultAdapter.ios.js +4 -0
  4. package/lib/module/DefaultAdapter.ios.js.map +1 -0
  5. package/lib/module/DefaultAdapter.js +5 -0
  6. package/lib/module/DefaultAdapter.js.map +1 -0
  7. package/lib/module/PagerViewAdapter.native.js +29 -13
  8. package/lib/module/PagerViewAdapter.native.js.map +1 -1
  9. package/lib/module/PanResponderAdapter.js +3 -0
  10. package/lib/module/PanResponderAdapter.js.map +1 -1
  11. package/lib/module/PlatformPressable.js +1 -1
  12. package/lib/module/PlatformPressable.js.map +1 -1
  13. package/lib/module/SceneMap.js +12 -6
  14. package/lib/module/SceneMap.js.map +1 -1
  15. package/lib/module/SceneView.js +25 -1
  16. package/lib/module/SceneView.js.map +1 -1
  17. package/lib/module/ScrollViewAdapter.js +49 -18
  18. package/lib/module/ScrollViewAdapter.js.map +1 -1
  19. package/lib/module/TabBar.js +279 -169
  20. package/lib/module/TabBar.js.map +1 -1
  21. package/lib/module/TabBarIndicator.js +314 -90
  22. package/lib/module/TabBarIndicator.js.map +1 -1
  23. package/lib/module/TabBarItem.js +94 -44
  24. package/lib/module/TabBarItem.js.map +1 -1
  25. package/lib/module/TabBarItemLabel.js +3 -2
  26. package/lib/module/TabBarItemLabel.js.map +1 -1
  27. package/lib/module/TabView.js +2 -2
  28. package/lib/module/TabView.js.map +1 -1
  29. package/lib/module/constants.js +10 -0
  30. package/lib/module/constants.js.map +1 -0
  31. package/lib/module/index.js +1 -0
  32. package/lib/module/index.js.map +1 -1
  33. package/lib/module/useLayoutWidths.js +46 -0
  34. package/lib/module/useLayoutWidths.js.map +1 -0
  35. package/lib/typescript/src/DefaultAdapter.android.d.ts +2 -0
  36. package/lib/typescript/src/DefaultAdapter.android.d.ts.map +1 -0
  37. package/lib/typescript/src/DefaultAdapter.d.ts +6 -0
  38. package/lib/typescript/src/DefaultAdapter.d.ts.map +1 -0
  39. package/lib/typescript/src/DefaultAdapter.ios.d.ts +2 -0
  40. package/lib/typescript/src/DefaultAdapter.ios.d.ts.map +1 -0
  41. package/lib/typescript/src/PagerViewAdapter.d.ts +1 -1
  42. package/lib/typescript/src/PagerViewAdapter.d.ts.map +1 -1
  43. package/lib/typescript/src/PagerViewAdapter.native.d.ts +2 -2
  44. package/lib/typescript/src/PagerViewAdapter.native.d.ts.map +1 -1
  45. package/lib/typescript/src/PanResponderAdapter.d.ts +1 -1
  46. package/lib/typescript/src/PanResponderAdapter.d.ts.map +1 -1
  47. package/lib/typescript/src/PlatformPressable.d.ts +5 -5
  48. package/lib/typescript/src/PlatformPressable.d.ts.map +1 -1
  49. package/lib/typescript/src/SceneMap.d.ts +2 -2
  50. package/lib/typescript/src/SceneMap.d.ts.map +1 -1
  51. package/lib/typescript/src/SceneView.d.ts +2 -2
  52. package/lib/typescript/src/SceneView.d.ts.map +1 -1
  53. package/lib/typescript/src/ScrollViewAdapter.d.ts +2 -3
  54. package/lib/typescript/src/ScrollViewAdapter.d.ts.map +1 -1
  55. package/lib/typescript/src/TabBar.d.ts +26 -25
  56. package/lib/typescript/src/TabBar.d.ts.map +1 -1
  57. package/lib/typescript/src/TabBarIndicator.d.ts +5 -7
  58. package/lib/typescript/src/TabBarIndicator.d.ts.map +1 -1
  59. package/lib/typescript/src/TabBarItem.d.ts +17 -10
  60. package/lib/typescript/src/TabBarItem.d.ts.map +1 -1
  61. package/lib/typescript/src/TabBarItemLabel.d.ts +4 -3
  62. package/lib/typescript/src/TabBarItemLabel.d.ts.map +1 -1
  63. package/lib/typescript/src/TabView.d.ts +17 -17
  64. package/lib/typescript/src/TabView.d.ts.map +1 -1
  65. package/lib/typescript/src/constants.d.ts +8 -0
  66. package/lib/typescript/src/constants.d.ts.map +1 -0
  67. package/lib/typescript/src/index.d.ts +13 -12
  68. package/lib/typescript/src/index.d.ts.map +1 -1
  69. package/lib/typescript/src/types.d.ts +31 -31
  70. package/lib/typescript/src/types.d.ts.map +1 -1
  71. package/lib/typescript/src/useLayoutWidths.d.ts +2 -0
  72. package/lib/typescript/src/useLayoutWidths.d.ts.map +1 -0
  73. package/lib/typescript/src/useMeasureLayout.d.ts +1 -1
  74. package/lib/typescript/src/useMeasureLayout.d.ts.map +1 -1
  75. package/package.json +14 -13
  76. package/src/DefaultAdapter.android.tsx +1 -0
  77. package/src/DefaultAdapter.ios.tsx +1 -0
  78. package/src/DefaultAdapter.tsx +13 -0
  79. package/src/PagerViewAdapter.native.tsx +36 -18
  80. package/src/PanResponderAdapter.tsx +4 -0
  81. package/src/PlatformPressable.tsx +11 -7
  82. package/src/SceneMap.tsx +17 -9
  83. package/src/SceneView.tsx +41 -2
  84. package/src/ScrollViewAdapter.tsx +88 -22
  85. package/src/TabBar.tsx +440 -235
  86. package/src/TabBarIndicator.tsx +389 -124
  87. package/src/TabBarItem.tsx +107 -46
  88. package/src/TabBarItemLabel.tsx +5 -4
  89. package/src/TabView.tsx +24 -20
  90. package/src/constants.tsx +8 -0
  91. package/src/index.tsx +5 -1
  92. package/src/types.tsx +40 -36
  93. package/src/useLayoutWidths.tsx +51 -0
  94. package/lib/module/Pager.android.js +0 -4
  95. package/lib/module/Pager.android.js.map +0 -1
  96. package/lib/module/Pager.ios.js +0 -4
  97. package/lib/module/Pager.ios.js.map +0 -1
  98. package/lib/module/Pager.js +0 -4
  99. package/lib/module/Pager.js.map +0 -1
  100. package/lib/typescript/src/Pager.android.d.ts +0 -2
  101. package/lib/typescript/src/Pager.android.d.ts.map +0 -1
  102. package/lib/typescript/src/Pager.d.ts +0 -2
  103. package/lib/typescript/src/Pager.d.ts.map +0 -1
  104. package/lib/typescript/src/Pager.ios.d.ts +0 -2
  105. package/lib/typescript/src/Pager.ios.d.ts.map +0 -1
  106. package/src/Pager.android.tsx +0 -1
  107. package/src/Pager.ios.tsx +0 -1
  108. package/src/Pager.tsx +0 -1
@@ -8,179 +8,444 @@ import {
8
8
  type ViewStyle,
9
9
  } from 'react-native';
10
10
 
11
+ import { TAB_BAR_PRIMARY_ACTIVE_COLOR } from './constants';
11
12
  import type {
12
13
  LocaleDirection,
13
14
  NavigationState,
14
15
  Route,
15
16
  SceneRendererProps,
16
17
  } from './types';
17
- import { useAnimatedValue } from './useAnimatedValue';
18
18
 
19
- export type GetTabWidth = (index: number) => number;
19
+ const CAP_FILL_OVERLAP = 1;
20
+ const EASING_SAMPLE_COUNT = 16;
21
+
22
+ const samplePoints = (easing: (t: number) => number) =>
23
+ Array.from({ length: EASING_SAMPLE_COUNT + 1 }, (_, i) =>
24
+ easing(i / EASING_SAMPLE_COUNT)
25
+ );
26
+
27
+ const TRAILING_EDGE_SAMPLES = samplePoints(Easing.bezier(0.3, 0, 0.8, 0.15));
28
+ const LEADING_EDGE_SAMPLES = samplePoints(Easing.bezier(0.05, 0.7, 0.1, 1));
20
29
 
21
30
  export type Props<T extends Route> = SceneRendererProps & {
31
+ variant?: 'primary' | 'secondary' | undefined;
22
32
  navigationState: NavigationState<T>;
23
- width: 'auto' | `${number}%` | number;
24
- getTabWidth: GetTabWidth;
33
+ widths: number[];
34
+ offsets: number[];
25
35
  direction: LocaleDirection;
26
36
  style?: StyleProp<ViewStyle>;
27
- gap?: number;
28
- children?: React.ReactNode;
29
- };
30
-
31
- const useNativeDriver = Platform.OS !== 'web';
32
-
33
- const getTranslateX = (
34
- position: Animated.AnimatedInterpolation<number>,
35
- routes: Route[],
36
- getTabWidth: GetTabWidth,
37
- direction: LocaleDirection,
38
- gap?: number,
39
- width?: number | string
40
- ) => {
41
- const inputRange = routes.map((_, i) => i);
42
-
43
- // every index contains widths at all previous indices
44
- const outputRange = routes.reduce<number[]>((acc, _, i) => {
45
- if (typeof width === 'number') {
46
- if (i === 0) return [getTabWidth(i) / 2 - width / 2];
47
-
48
- let sumTabWidth = 0;
49
- for (let j = 0; j < i; j++) {
50
- sumTabWidth += getTabWidth(j);
51
- }
52
-
53
- return [
54
- ...acc,
55
- sumTabWidth + getTabWidth(i) / 2 + (gap ? gap * i : 0) - width / 2,
56
- ];
57
- } else {
58
- if (i === 0) return [0];
59
- return [...acc, acc[i - 1] + getTabWidth(i - 1) + (gap ?? 0)];
60
- }
61
- }, []);
62
-
63
- const translateX = position.interpolate({
64
- inputRange,
65
- outputRange,
66
- extrapolate: 'clamp',
67
- });
68
-
69
- return Animated.multiply(translateX, direction === 'rtl' ? -1 : 1);
70
37
  };
71
38
 
72
39
  export function TabBarIndicator<T extends Route>({
73
- getTabWidth,
40
+ variant = 'primary',
41
+ widths,
42
+ offsets,
74
43
  navigationState,
75
44
  position,
76
- width,
77
45
  direction,
78
- gap,
79
46
  style,
80
- children,
81
47
  }: Props<T>) {
82
- const isIndicatorShown = React.useRef(false);
83
- const isWidthDynamic = width === 'auto';
48
+ const isRTL = direction === 'rtl';
84
49
 
85
- const opacity = useAnimatedValue(isWidthDynamic ? 0 : 1);
50
+ const flattenedStyle: ViewStyle =
51
+ StyleSheet.flatten([styles.defaults, style]) || {};
86
52
 
87
- const indicatorVisible = isWidthDynamic
88
- ? navigationState.routes
89
- .slice(0, navigationState.index)
90
- .every((_, r) => getTabWidth(r))
91
- : true;
53
+ const {
54
+ backgroundColor,
55
+ borderRadius,
56
+ borderBottomEndRadius = borderRadius,
57
+ borderBottomLeftRadius = borderRadius,
58
+ borderBottomRightRadius = borderRadius,
59
+ borderBottomStartRadius = borderRadius,
60
+ borderTopEndRadius = borderRadius,
61
+ borderTopLeftRadius = borderRadius,
62
+ borderTopRightRadius = borderRadius,
63
+ borderTopStartRadius = borderRadius,
64
+ height,
65
+ start: indicatorStart,
66
+ end: indicatorEnd,
67
+ left: indicatorLeft,
68
+ right: indicatorRight,
69
+ ...restStyle
70
+ } = flattenedStyle;
92
71
 
93
- React.useEffect(() => {
94
- const fadeInIndicator = () => {
95
- if (
96
- !isIndicatorShown.current &&
97
- isWidthDynamic &&
98
- // We should fade-in the indicator when we have widths for all the tab items
99
- indicatorVisible
100
- ) {
101
- isIndicatorShown.current = true;
102
-
103
- Animated.timing(opacity, {
104
- toValue: 1,
105
- duration: 150,
106
- easing: Easing.in(Easing.linear),
107
- useNativeDriver,
108
- }).start();
109
- }
110
- };
72
+ delete restStyle.width;
73
+ delete restStyle.margin;
74
+ delete restStyle.marginHorizontal;
75
+ delete restStyle.marginStart;
76
+ delete restStyle.marginEnd;
77
+ delete restStyle.marginLeft;
78
+ delete restStyle.marginRight;
79
+
80
+ const containerStart =
81
+ indicatorStart ?? (isRTL ? indicatorRight : indicatorLeft);
82
+ const containerEnd = indicatorEnd ?? (isRTL ? indicatorLeft : indicatorRight);
111
83
 
112
- fadeInIndicator();
84
+ if (
85
+ (borderBottomEndRadius != null &&
86
+ typeof borderBottomEndRadius !== 'number') ||
87
+ (borderBottomStartRadius != null &&
88
+ typeof borderBottomStartRadius !== 'number') ||
89
+ (borderBottomLeftRadius != null &&
90
+ typeof borderBottomLeftRadius !== 'number') ||
91
+ (borderBottomRightRadius != null &&
92
+ typeof borderBottomRightRadius !== 'number') ||
93
+ (borderTopEndRadius != null && typeof borderTopEndRadius !== 'number') ||
94
+ (borderTopStartRadius != null &&
95
+ typeof borderTopStartRadius !== 'number') ||
96
+ (borderTopLeftRadius != null && typeof borderTopLeftRadius !== 'number') ||
97
+ (borderTopRightRadius != null && typeof borderTopRightRadius !== 'number')
98
+ ) {
99
+ throw new Error(
100
+ 'Only numeric border radii are supported in TabBarIndicator.'
101
+ );
102
+ }
113
103
 
114
- return () => opacity.stopAnimation();
115
- }, [indicatorVisible, isWidthDynamic, opacity]);
104
+ const leftRadiusWidth = Math.max(
105
+ (isRTL ? borderTopEndRadius : borderTopStartRadius) ?? 0,
106
+ (isRTL ? borderBottomEndRadius : borderBottomStartRadius) ?? 0,
107
+ borderTopLeftRadius ?? 0,
108
+ borderBottomLeftRadius ?? 0
109
+ );
110
+
111
+ const rightRadiusWidth = Math.max(
112
+ (isRTL ? borderTopStartRadius : borderTopEndRadius) ?? 0,
113
+ (isRTL ? borderBottomStartRadius : borderBottomEndRadius) ?? 0,
114
+ borderTopRightRadius ?? 0,
115
+ borderBottomRightRadius ?? 0
116
+ );
117
+
118
+ const leftPieceWidth = leftRadiusWidth + CAP_FILL_OVERLAP;
119
+ const rightPieceWidth = rightRadiusWidth + CAP_FILL_OVERLAP;
116
120
 
117
121
  const { routes } = navigationState;
118
122
 
119
- const transform = [];
120
-
121
- const translateX =
122
- routes.length > 1
123
- ? getTranslateX(position, routes, getTabWidth, direction, gap, width)
124
- : 0;
125
-
126
- transform.push({ translateX });
127
-
128
- if (width === 'auto') {
129
- const inputRange = routes.map((_, i) => i);
130
- const outputRange = inputRange.map(getTabWidth);
131
-
132
- transform.push(
133
- {
134
- scaleX:
135
- routes.length > 1
136
- ? position.interpolate({
137
- inputRange,
138
- outputRange,
139
- extrapolate: 'clamp',
140
- })
141
- : outputRange[0],
142
- },
143
- { translateX: direction === 'rtl' ? -0.5 : 0.5 }
144
- );
123
+ const easedInterpolate = (values: number[], samples: number[] | null) => {
124
+ if (routes.length <= 1) {
125
+ return values[0] ?? 0;
126
+ }
127
+
128
+ // On multi-tab jumps, slide directly from source to destination and
129
+ // Settle one tab before the pager finishes scrolling
130
+ // This makes the animation feel snappier
131
+ // Especially on Android where the pager animation is slow
132
+ if (jumpRange) {
133
+ const fromValue = values[jumpRange.from];
134
+ const toValue = values[jumpRange.to];
135
+
136
+ if (fromValue == null) {
137
+ throw new Error(`Couldn't find a value at index ${jumpRange.from}.`);
138
+ }
139
+
140
+ if (toValue == null) {
141
+ throw new Error(`Couldn't find a value at index ${jumpRange.to}.`);
142
+ }
143
+
144
+ const inputRange =
145
+ jumpRange.from > jumpRange.to
146
+ ? [jumpRange.to, jumpRange.to + 1, jumpRange.from]
147
+ : [jumpRange.from, jumpRange.to - 1, jumpRange.to];
148
+
149
+ const outputRange = inputRange.map((i) =>
150
+ i === jumpRange.from ? fromValue : toValue
151
+ );
152
+
153
+ return position.interpolate({
154
+ inputRange,
155
+ outputRange,
156
+ extrapolate: 'clamp',
157
+ });
158
+ }
159
+
160
+ if (samples == null) {
161
+ return position.interpolate({
162
+ inputRange: values.map((_, i) => i),
163
+ outputRange: values,
164
+ extrapolate: 'clamp',
165
+ });
166
+ }
167
+
168
+ const inputRange: number[] = [];
169
+ const outputRange: number[] = [];
170
+
171
+ for (let i = 0; i < values.length - 1; i++) {
172
+ const start = values[i];
173
+ const end = values[i + 1];
174
+
175
+ if (start == null) {
176
+ throw new Error(`Couldn't find a value at index ${i}.`);
177
+ }
178
+
179
+ if (end == null) {
180
+ throw new Error(`Couldn't find a value at index ${i + 1}.`);
181
+ }
182
+
183
+ for (let j = 0; j < samples.length - 1; j++) {
184
+ const sample = samples[j];
185
+
186
+ if (sample == null) {
187
+ throw new Error(`Couldn't find an easing sample at index ${j}.`);
188
+ }
189
+
190
+ inputRange.push(i + j / EASING_SAMPLE_COUNT);
191
+ outputRange.push(start + (end - start) * sample);
192
+ }
193
+ }
194
+
195
+ const last = values[values.length - 1];
196
+
197
+ if (last == null) {
198
+ throw new Error(`Couldn't find a value at index ${values.length - 1}.`);
199
+ }
200
+
201
+ inputRange.push(values.length - 1);
202
+ outputRange.push(last);
203
+
204
+ return position.interpolate({
205
+ inputRange,
206
+ outputRange,
207
+ extrapolate: 'clamp',
208
+ });
209
+ };
210
+
211
+ let containerLayout: ViewStyle;
212
+ let leftFillStyle: ViewStyle;
213
+ let centerFillStyle: ViewStyle;
214
+ let rightCapStyle: ViewStyle;
215
+ let rightFillStyle: ViewStyle;
216
+
217
+ const rightEdges = widths.map((w, i) => {
218
+ const offset = offsets[i];
219
+
220
+ if (offset == null) {
221
+ throw new Error(`Couldn't find an offset at index ${i}.`);
222
+ }
223
+
224
+ return offset + w;
225
+ });
226
+
227
+ const [lastIndex, setLastIndex] = React.useState(navigationState.index);
228
+ const [jumpRange, setJumpRange] = React.useState<{
229
+ from: number;
230
+ to: number;
231
+ } | null>(null);
232
+
233
+ if (navigationState.index !== lastIndex) {
234
+ setLastIndex(navigationState.index);
235
+
236
+ if (Math.abs(navigationState.index - lastIndex) > 1) {
237
+ setJumpRange({ from: lastIndex, to: navigationState.index });
238
+ }
145
239
  }
146
240
 
147
- const styleList: StyleProp<ViewStyle> = [];
241
+ React.useEffect(() => {
242
+ if (jumpRange == null) {
243
+ return;
244
+ }
245
+
246
+ const timer = setTimeout(() => {
247
+ setJumpRange(null);
248
+ }, 500);
148
249
 
149
- // scaleX doesn't work properly on chrome and opera for linux and android
150
- if (Platform.OS === 'web' && width === 'auto') {
151
- styleList.push(
152
- { width: transform[1].scaleX },
153
- { left: transform[0].translateX }
154
- );
250
+ return () => clearTimeout(timer);
251
+ }, [jumpRange]);
252
+
253
+ // Primary tabs use stretch animation
254
+ // Secondary tabs and multi-tab jumps slide linearly
255
+ const shouldStretch = variant === 'primary' && jumpRange == null;
256
+
257
+ const trailingSamples = shouldStretch ? TRAILING_EDGE_SAMPLES : null;
258
+ const leadingSamples = shouldStretch ? LEADING_EDGE_SAMPLES : null;
259
+
260
+ const offset = easedInterpolate(offsets, trailingSamples);
261
+ const rightEdge = easedInterpolate(rightEdges, leadingSamples);
262
+
263
+ const containerWidth = Animated.subtract(rightEdge, offset);
264
+ const sideFillWidth = Animated.divide(
265
+ Animated.subtract(containerWidth, leftPieceWidth + rightPieceWidth),
266
+ 2
267
+ );
268
+ const sideFillScale = Animated.add(sideFillWidth, CAP_FILL_OVERLAP);
269
+
270
+ if (Platform.OS === 'web') {
271
+ const centerFillStart = Animated.add(sideFillWidth, leftPieceWidth);
272
+
273
+ const positioned =
274
+ typeof containerStart === 'number'
275
+ ? Animated.add(offset, containerStart)
276
+ : offset;
277
+
278
+ // Web can't reliably scale via transforms
279
+ // So the fills use animated `width` instead of `scaleX`
280
+ // See https://github.com/react-navigation/react-navigation/pull/11440
281
+ containerLayout = {
282
+ width: containerWidth,
283
+ ...(direction === 'rtl' ? { right: positioned } : { left: positioned }),
284
+ };
285
+
286
+ leftFillStyle = {
287
+ position: 'absolute',
288
+ start: leftRadiusWidth,
289
+ width: sideFillScale,
290
+ };
291
+
292
+ centerFillStyle = {
293
+ position: 'absolute',
294
+ start: centerFillStart,
295
+ width: sideFillWidth,
296
+ };
297
+
298
+ rightCapStyle = {
299
+ position: 'absolute',
300
+ end: 0,
301
+ };
302
+
303
+ rightFillStyle = {
304
+ position: 'absolute',
305
+ end: rightRadiusWidth,
306
+ width: sideFillScale,
307
+ };
155
308
  } else {
156
- styleList.push(
157
- { width: width === 'auto' ? 1 : width },
158
- { start: `${(100 / routes.length) * navigationState.index}%` },
159
- { transform }
309
+ const directionSign = direction === 'rtl' ? -1 : 1;
310
+ const translateX = Animated.multiply(offset, directionSign);
311
+
312
+ const centerFillTranslateX = Animated.multiply(
313
+ Animated.divide(sideFillWidth, 2),
314
+ directionSign
160
315
  );
316
+
317
+ const rightCapTranslateX = Animated.multiply(
318
+ Animated.subtract(containerWidth, rightPieceWidth),
319
+ directionSign
320
+ );
321
+
322
+ containerLayout = {
323
+ start: containerStart ?? 0,
324
+ end: containerEnd ?? 0,
325
+ transform: [{ translateX }],
326
+ };
327
+
328
+ leftFillStyle = {
329
+ position: 'absolute',
330
+ start: leftRadiusWidth,
331
+ width: 1,
332
+ transformOrigin: isRTL ? 'right center' : 'left center',
333
+ transform: [{ scaleX: sideFillScale }],
334
+ };
335
+
336
+ centerFillStyle = {
337
+ position: 'absolute',
338
+ start: leftPieceWidth,
339
+ width: 1,
340
+ transformOrigin: isRTL ? 'right center' : 'left center',
341
+ transform: [
342
+ {
343
+ translateX: centerFillTranslateX,
344
+ },
345
+ { scaleX: sideFillWidth },
346
+ ],
347
+ };
348
+
349
+ rightCapStyle = {
350
+ position: 'absolute',
351
+ start: 0,
352
+ transform: [{ translateX: rightCapTranslateX }],
353
+ };
354
+
355
+ rightFillStyle = {
356
+ position: 'absolute',
357
+ end: rightRadiusWidth,
358
+ width: 1,
359
+ transformOrigin: isRTL ? 'left center' : 'right center',
360
+ transform: [{ scaleX: sideFillScale }],
361
+ };
161
362
  }
162
363
 
364
+ // The tab widths may be measured asynchronously
365
+ // So we show the indicator when we have widths till focused tab
366
+ const indicatorVisible = widths
367
+ .slice(0, navigationState.index + 1)
368
+ .every((w, i) => {
369
+ const offset = offsets[i];
370
+
371
+ return w > 0 && offset != null && offset >= 0;
372
+ });
373
+
374
+ /**
375
+ * We render the indicator in multiple pieces
376
+ * So we can preserve border radii when the indicator is scaled with transform
377
+ * We use 3 pieces for the inner fill as the math isn't correct on Android
378
+ * So using 1 or 2 pieces result in misalignment or gaps.
379
+ * Using 3 pieces lets us cover most space from all directions.
380
+ *
381
+ * [left fixed cap [left scaled fill >>>]]
382
+ * [center scaled fill]
383
+ * [[<<< right scaled fill] right fixed cap]
384
+ */
163
385
  return (
164
386
  <Animated.View
165
387
  style={[
166
388
  styles.indicator,
167
- styleList,
168
- width === 'auto' ? { opacity: opacity } : null,
169
- style,
389
+ {
390
+ height,
391
+ opacity: indicatorVisible ? 1 : 0,
392
+ },
393
+ containerLayout,
394
+ restStyle,
170
395
  ]}
171
396
  >
172
- {children}
397
+ <Animated.View
398
+ style={[
399
+ {
400
+ ...(isRTL
401
+ ? { borderTopEndRadius, borderBottomEndRadius }
402
+ : { borderTopStartRadius, borderBottomStartRadius }),
403
+ borderTopLeftRadius,
404
+ borderBottomLeftRadius,
405
+ height,
406
+ width: leftPieceWidth,
407
+ backgroundColor,
408
+ },
409
+ styles.cap,
410
+ ]}
411
+ >
412
+ <Animated.View style={[{ height, backgroundColor }, leftFillStyle]} />
413
+ </Animated.View>
414
+ <Animated.View style={[{ height, backgroundColor }, centerFillStyle]} />
415
+ <Animated.View
416
+ style={[
417
+ {
418
+ ...(isRTL
419
+ ? { borderTopStartRadius, borderBottomStartRadius }
420
+ : { borderTopEndRadius, borderBottomEndRadius }),
421
+ borderTopRightRadius,
422
+ borderBottomRightRadius,
423
+ height,
424
+ width: rightPieceWidth,
425
+ backgroundColor,
426
+ },
427
+ rightCapStyle,
428
+ ]}
429
+ >
430
+ <Animated.View style={[{ height, backgroundColor }, rightFillStyle]} />
431
+ </Animated.View>
173
432
  </Animated.View>
174
433
  );
175
434
  }
176
435
 
177
436
  const styles = StyleSheet.create({
178
437
  indicator: {
179
- backgroundColor: 'rgb(0, 122, 255)',
438
+ flexDirection: 'row',
439
+ justifyContent: 'center',
180
440
  position: 'absolute',
181
- start: 0,
182
441
  bottom: 0,
183
- end: 0,
442
+ },
443
+ defaults: {
444
+ backgroundColor: TAB_BAR_PRIMARY_ACTIVE_COLOR,
184
445
  height: 2,
185
446
  },
447
+ cap: {
448
+ position: 'absolute',
449
+ start: 0,
450
+ },
186
451
  });