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.
- package/lib/module/PagerViewAdapter.native.js +29 -13
- package/lib/module/PagerViewAdapter.native.js.map +1 -1
- package/lib/module/PlatformPressable.js +1 -1
- package/lib/module/ScrollViewAdapter.js +46 -18
- package/lib/module/ScrollViewAdapter.js.map +1 -1
- package/lib/module/TabBar.js +260 -148
- package/lib/module/TabBar.js.map +1 -1
- package/lib/module/TabBarIndicator.js +282 -168
- package/lib/module/TabBarIndicator.js.map +1 -1
- package/lib/module/TabBarItem.js +94 -44
- package/lib/module/TabBarItem.js.map +1 -1
- package/lib/module/TabBarItemLabel.js +3 -2
- package/lib/module/TabBarItemLabel.js.map +1 -1
- package/lib/module/constants.js +10 -0
- package/lib/module/constants.js.map +1 -0
- package/lib/module/useLayoutWidths.js +46 -0
- package/lib/module/useLayoutWidths.js.map +1 -0
- package/lib/typescript/src/PagerViewAdapter.native.d.ts +1 -1
- package/lib/typescript/src/PagerViewAdapter.native.d.ts.map +1 -1
- package/lib/typescript/src/ScrollViewAdapter.d.ts +1 -2
- package/lib/typescript/src/ScrollViewAdapter.d.ts.map +1 -1
- package/lib/typescript/src/TabBar.d.ts +2 -1
- package/lib/typescript/src/TabBar.d.ts.map +1 -1
- package/lib/typescript/src/TabBarIndicator.d.ts +4 -7
- package/lib/typescript/src/TabBarIndicator.d.ts.map +1 -1
- package/lib/typescript/src/TabBarItem.d.ts +10 -4
- package/lib/typescript/src/TabBarItem.d.ts.map +1 -1
- package/lib/typescript/src/TabBarItemLabel.d.ts +4 -3
- package/lib/typescript/src/TabBarItemLabel.d.ts.map +1 -1
- package/lib/typescript/src/constants.d.ts +8 -0
- package/lib/typescript/src/constants.d.ts.map +1 -0
- package/lib/typescript/src/useLayoutWidths.d.ts +2 -0
- package/lib/typescript/src/useLayoutWidths.d.ts.map +1 -0
- package/package.json +2 -2
- package/src/PagerViewAdapter.native.tsx +36 -18
- package/src/PlatformPressable.tsx +1 -1
- package/src/ScrollViewAdapter.tsx +81 -21
- package/src/TabBar.tsx +386 -181
- package/src/TabBarIndicator.tsx +323 -248
- package/src/TabBarItem.tsx +102 -41
- package/src/TabBarItemLabel.tsx +5 -4
- package/src/constants.tsx +8 -0
- package/src/useLayoutWidths.tsx +51 -0
package/src/TabBarIndicator.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
118
|
+
const leftPieceWidth = leftRadiusWidth + CAP_FILL_OVERLAP;
|
|
119
|
+
const rightPieceWidth = rightRadiusWidth + CAP_FILL_OVERLAP;
|
|
92
120
|
|
|
93
|
-
|
|
94
|
-
};
|
|
121
|
+
const { routes } = navigationState;
|
|
95
122
|
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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
|
-
|
|
116
|
-
|
|
142
|
+
return position.interpolate({
|
|
143
|
+
inputRange,
|
|
144
|
+
outputRange,
|
|
145
|
+
extrapolate: 'clamp',
|
|
146
|
+
});
|
|
117
147
|
}
|
|
118
148
|
|
|
119
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
outputRange,
|
|
125
|
-
extrapolate: 'clamp',
|
|
126
|
-
});
|
|
157
|
+
const inputRange: number[] = [];
|
|
158
|
+
const outputRange: number[] = [];
|
|
127
159
|
|
|
128
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
170
|
+
inputRange.push(values.length - 1);
|
|
171
|
+
outputRange.push(values[values.length - 1]);
|
|
171
172
|
|
|
172
|
-
return
|
|
173
|
+
return position.interpolate({
|
|
174
|
+
inputRange,
|
|
175
|
+
outputRange,
|
|
176
|
+
extrapolate: 'clamp',
|
|
177
|
+
});
|
|
173
178
|
};
|
|
174
179
|
|
|
175
|
-
|
|
180
|
+
let containerLayout: ViewStyle;
|
|
181
|
+
let leftFillStyle: ViewStyle;
|
|
182
|
+
let centerFillStyle: ViewStyle;
|
|
183
|
+
let rightCapStyle: ViewStyle;
|
|
184
|
+
let rightFillStyle: ViewStyle;
|
|
176
185
|
|
|
177
|
-
|
|
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.
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
194
|
+
if (navigationState.index !== lastIndex) {
|
|
195
|
+
setLastIndex(navigationState.index);
|
|
205
196
|
|
|
206
|
-
|
|
207
|
-
|
|
197
|
+
if (Math.abs(navigationState.index - lastIndex) > 1) {
|
|
198
|
+
setJumpRange({ from: lastIndex, to: navigationState.index });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
208
201
|
|
|
209
|
-
|
|
202
|
+
React.useEffect(() => {
|
|
203
|
+
if (jumpRange == null) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
210
206
|
|
|
211
|
-
|
|
207
|
+
const timer = setTimeout(() => {
|
|
208
|
+
setJumpRange(null);
|
|
209
|
+
}, 500);
|
|
212
210
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
? getTranslateX(position, routes, getTabWidth, direction, gap, (index) =>
|
|
216
|
-
getCenteredIndicatorWidth(getTabWidth(index))
|
|
217
|
-
)
|
|
218
|
-
: 0;
|
|
211
|
+
return () => clearTimeout(timer);
|
|
212
|
+
}, [jumpRange]);
|
|
219
213
|
|
|
220
|
-
|
|
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
|
-
|
|
223
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
getIndicatorWidthWithMargins(tabW, flattenedStyle, direction)
|
|
230
|
-
);
|
|
231
|
-
});
|
|
221
|
+
const offset = easedInterpolate(offsets, trailingSamples);
|
|
222
|
+
const rightEdge = easedInterpolate(rightEdges, leadingSamples);
|
|
232
223
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
247
|
+
leftFillStyle = {
|
|
248
|
+
position: 'absolute',
|
|
249
|
+
start: leftRadiusWidth,
|
|
250
|
+
width: sideFillScale,
|
|
251
|
+
};
|
|
249
252
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
278
|
+
const rightCapTranslateX = Animated.multiply(
|
|
279
|
+
Animated.subtract(containerWidth, rightPieceWidth),
|
|
280
|
+
directionSign
|
|
281
|
+
);
|
|
289
282
|
|
|
290
|
-
|
|
291
|
-
|
|
283
|
+
containerLayout = {
|
|
284
|
+
start: containerStart ?? 0,
|
|
285
|
+
end: containerEnd ?? 0,
|
|
286
|
+
transform: [{ translateX }],
|
|
287
|
+
};
|
|
292
288
|
|
|
293
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
310
|
+
rightCapStyle = {
|
|
311
|
+
position: 'absolute',
|
|
312
|
+
start: 0,
|
|
313
|
+
transform: [{ translateX: rightCapTranslateX }],
|
|
314
|
+
};
|
|
304
315
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
346
|
+
{
|
|
347
|
+
height,
|
|
348
|
+
opacity: indicatorVisible ? 1 : 0,
|
|
349
|
+
},
|
|
350
|
+
containerLayout,
|
|
351
|
+
restStyle,
|
|
317
352
|
]}
|
|
318
353
|
>
|
|
319
|
-
|
|
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
|
-
|
|
395
|
+
flexDirection: 'row',
|
|
396
|
+
justifyContent: 'center',
|
|
327
397
|
position: 'absolute',
|
|
328
|
-
start: 0,
|
|
329
398
|
bottom: 0,
|
|
330
|
-
|
|
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
|
});
|