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