react-native-tab-view 5.0.0-alpha.8 → 5.0.0-alpha.9

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