@zoverdoser/react-native-tab-view 1.0.1 → 1.3.1
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.js +1 -1
- package/lib/module/PagerViewAdapter.js.map +1 -1
- package/lib/module/PanResponderAdapter.js +1 -1
- package/lib/module/PanResponderAdapter.js.map +1 -1
- package/lib/module/TabBar.js +7 -30
- package/lib/module/TabBar.js.map +1 -1
- package/lib/module/TabBarIndicator.js +110 -35
- package/lib/module/TabBarIndicator.js.map +1 -1
- package/lib/module/TabBarItem.js +1 -1
- package/lib/module/TabBarItem.js.map +1 -1
- package/lib/module/TabBarItemLabel.js.map +1 -1
- package/lib/module/useAnimatedValue.js.map +1 -1
- package/lib/typescript/src/Pager.android.d.ts +1 -1
- package/lib/typescript/src/Pager.android.d.ts.map +1 -1
- package/lib/typescript/src/Pager.d.ts +1 -1
- package/lib/typescript/src/Pager.d.ts.map +1 -1
- package/lib/typescript/src/Pager.ios.d.ts +1 -1
- package/lib/typescript/src/Pager.ios.d.ts.map +1 -1
- package/lib/typescript/src/PagerViewAdapter.d.ts +1 -1
- package/lib/typescript/src/PagerViewAdapter.d.ts.map +1 -1
- package/lib/typescript/src/PanResponderAdapter.d.ts +1 -1
- package/lib/typescript/src/PanResponderAdapter.d.ts.map +1 -1
- package/lib/typescript/src/SceneMap.d.ts +1 -1
- package/lib/typescript/src/SceneMap.d.ts.map +1 -1
- package/lib/typescript/src/SceneView.d.ts +1 -1
- package/lib/typescript/src/SceneView.d.ts.map +1 -1
- package/lib/typescript/src/TabBar.d.ts +3 -3
- package/lib/typescript/src/TabBar.d.ts.map +1 -1
- package/lib/typescript/src/TabBarIndicator.d.ts +1 -1
- package/lib/typescript/src/TabBarIndicator.d.ts.map +1 -1
- package/lib/typescript/src/TabBarItem.d.ts +1 -1
- package/lib/typescript/src/TabBarItem.d.ts.map +1 -1
- package/lib/typescript/src/TabBarItemLabel.d.ts.map +1 -1
- package/lib/typescript/src/TabView.d.ts +1 -1
- package/lib/typescript/src/TabView.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +10 -10
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/useAnimatedValue.d.ts.map +1 -1
- package/package.json +8 -8
- package/src/PagerViewAdapter.tsx +1 -1
- package/src/PanResponderAdapter.tsx +1 -1
- package/src/TabBar.tsx +11 -45
- package/src/TabBarIndicator.tsx +182 -41
- package/src/TabBarItem.tsx +1 -1
- package/src/TabBarItemLabel.tsx +1 -0
- package/src/useAnimatedValue.tsx +2 -2
package/src/TabBarIndicator.tsx
CHANGED
|
@@ -30,35 +30,94 @@ export type Props<T extends Route> = SceneRendererProps & {
|
|
|
30
30
|
|
|
31
31
|
const useNativeDriver = Platform.OS !== 'web';
|
|
32
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);
|
|
43
|
+
|
|
44
|
+
if (Number.isFinite(parsed)) {
|
|
45
|
+
return referenceWidth * (parsed / 100);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
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)
|
|
74
|
+
);
|
|
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
|
+
|
|
85
|
+
if (customWidth !== undefined) {
|
|
86
|
+
return customWidth;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (typeof width === 'number') {
|
|
90
|
+
return getIndicatorWidthWithMargins(width, style, direction);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return width;
|
|
94
|
+
};
|
|
95
|
+
|
|
33
96
|
const getTranslateX = (
|
|
34
97
|
position: Animated.AnimatedInterpolation<number>,
|
|
35
98
|
routes: Route[],
|
|
36
99
|
getTabWidth: GetTabWidth,
|
|
37
100
|
direction: LocaleDirection,
|
|
38
101
|
gap?: number,
|
|
39
|
-
|
|
102
|
+
getWidth?: (index: number) => number | undefined
|
|
40
103
|
) => {
|
|
41
104
|
const inputRange = routes.map((_, i) => i);
|
|
105
|
+
const outputRange = routes.map((_, i) => {
|
|
106
|
+
let sumTabWidth = 0;
|
|
42
107
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (i === 0) return [getTabWidth(i) / 2 - width / 2];
|
|
108
|
+
for (let j = 0; j < i; j++) {
|
|
109
|
+
sumTabWidth += getTabWidth(j);
|
|
110
|
+
}
|
|
47
111
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
sumTabWidth += getTabWidth(j);
|
|
51
|
-
}
|
|
112
|
+
const indicatorWidth = getWidth?.(i);
|
|
113
|
+
const tabOffset = sumTabWidth + (gap ? gap * i : 0);
|
|
52
114
|
|
|
53
|
-
|
|
54
|
-
|
|
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)];
|
|
115
|
+
if (indicatorWidth === undefined) {
|
|
116
|
+
return tabOffset;
|
|
60
117
|
}
|
|
61
|
-
|
|
118
|
+
|
|
119
|
+
return tabOffset + getTabWidth(i) / 2 - indicatorWidth / 2;
|
|
120
|
+
});
|
|
62
121
|
|
|
63
122
|
const translateX = position.interpolate({
|
|
64
123
|
inputRange,
|
|
@@ -83,31 +142,60 @@ export function TabBarIndicator<T extends Route>({
|
|
|
83
142
|
const isIndicatorShown = React.useRef(false);
|
|
84
143
|
const isWidthDynamic = width === 'auto';
|
|
85
144
|
|
|
145
|
+
const flattenedStyle = StyleSheet.flatten(style);
|
|
146
|
+
|
|
147
|
+
const hasCustomIndicatorWidth =
|
|
148
|
+
typeof flattenedStyle?.width === 'number' ||
|
|
149
|
+
(typeof flattenedStyle?.width === 'string' &&
|
|
150
|
+
flattenedStyle?.width.endsWith('%'));
|
|
151
|
+
|
|
152
|
+
const constantIndicatorWidth =
|
|
153
|
+
typeof flattenedStyle?.width === 'number'
|
|
154
|
+
? flattenedStyle.width
|
|
155
|
+
: undefined;
|
|
156
|
+
|
|
157
|
+
const isCentered =
|
|
158
|
+
hasCustomIndicatorWidth &&
|
|
159
|
+
(flattenedStyle?.margin === 'auto' ||
|
|
160
|
+
flattenedStyle?.marginHorizontal === 'auto');
|
|
161
|
+
|
|
162
|
+
// If indicator has a custom width, we need to adjust calculations to account for it
|
|
163
|
+
// It should be centered relative to the tab if the margin is set to auto
|
|
164
|
+
const getCenteredIndicatorWidth = (tabWidth: number) => {
|
|
165
|
+
if (isCentered) {
|
|
166
|
+
return calculateSize(flattenedStyle?.width, tabWidth);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (typeof width === 'number') {
|
|
170
|
+
return width;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return undefined;
|
|
174
|
+
};
|
|
175
|
+
|
|
86
176
|
const opacity = useAnimatedValue(isWidthDynamic ? 0 : 1);
|
|
87
177
|
|
|
178
|
+
// We should fade-in the indicator when we have widths for all the tab items
|
|
88
179
|
const indicatorVisible = isWidthDynamic
|
|
89
180
|
? layout.width &&
|
|
90
181
|
navigationState.routes
|
|
91
|
-
.slice(0, navigationState.index)
|
|
182
|
+
.slice(0, navigationState.index + 1)
|
|
92
183
|
.every((_, r) => getTabWidth(r))
|
|
93
184
|
: true;
|
|
94
185
|
|
|
95
186
|
React.useEffect(() => {
|
|
96
187
|
const fadeInIndicator = () => {
|
|
97
|
-
if (
|
|
98
|
-
!isIndicatorShown.current &&
|
|
99
|
-
isWidthDynamic &&
|
|
100
|
-
// We should fade-in the indicator when we have widths for all the tab items
|
|
101
|
-
indicatorVisible
|
|
102
|
-
) {
|
|
103
|
-
isIndicatorShown.current = true;
|
|
104
|
-
|
|
188
|
+
if (!isIndicatorShown.current && isWidthDynamic && indicatorVisible) {
|
|
105
189
|
Animated.timing(opacity, {
|
|
106
190
|
toValue: 1,
|
|
107
191
|
duration: 150,
|
|
108
192
|
easing: Easing.in(Easing.linear),
|
|
109
193
|
useNativeDriver,
|
|
110
|
-
}).start()
|
|
194
|
+
}).start(({ finished }) => {
|
|
195
|
+
if (finished) {
|
|
196
|
+
isIndicatorShown.current = true;
|
|
197
|
+
}
|
|
198
|
+
});
|
|
111
199
|
}
|
|
112
200
|
};
|
|
113
201
|
|
|
@@ -120,18 +208,25 @@ export function TabBarIndicator<T extends Route>({
|
|
|
120
208
|
|
|
121
209
|
const transform = [];
|
|
122
210
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
routes
|
|
126
|
-
|
|
127
|
-
|
|
211
|
+
const translateX =
|
|
212
|
+
layout.width && routes.length > 1
|
|
213
|
+
? getTranslateX(position, routes, getTabWidth, direction, gap, (index) =>
|
|
214
|
+
getCenteredIndicatorWidth(getTabWidth(index))
|
|
215
|
+
)
|
|
216
|
+
: 0;
|
|
128
217
|
|
|
129
|
-
|
|
130
|
-
}
|
|
218
|
+
transform.push({ translateX });
|
|
131
219
|
|
|
132
|
-
if (width === 'auto') {
|
|
220
|
+
if (width === 'auto' && constantIndicatorWidth == null) {
|
|
133
221
|
const inputRange = routes.map((_, i) => i);
|
|
134
|
-
const outputRange = inputRange.map(
|
|
222
|
+
const outputRange = inputRange.map((i) => {
|
|
223
|
+
const tabW = getTabWidth(i);
|
|
224
|
+
|
|
225
|
+
return (
|
|
226
|
+
calculateSize(flattenedStyle?.width, tabW) ??
|
|
227
|
+
getIndicatorWidthWithMargins(tabW, flattenedStyle, direction)
|
|
228
|
+
);
|
|
229
|
+
});
|
|
135
230
|
|
|
136
231
|
transform.push(
|
|
137
232
|
{
|
|
@@ -150,27 +245,73 @@ export function TabBarIndicator<T extends Route>({
|
|
|
150
245
|
|
|
151
246
|
const styleList: StyleProp<ViewStyle> = [];
|
|
152
247
|
|
|
153
|
-
//
|
|
154
|
-
|
|
248
|
+
// transform doesn't work properly on chrome and opera for linux and android
|
|
249
|
+
// so we need to use width and left/right instead of scaleX and translateX
|
|
250
|
+
// https://github.com/react-navigation/react-navigation/pull/11440
|
|
251
|
+
if (
|
|
252
|
+
Platform.OS === 'web' &&
|
|
253
|
+
width === 'auto' &&
|
|
254
|
+
constantIndicatorWidth == null
|
|
255
|
+
) {
|
|
256
|
+
const start = flattenedStyle?.start;
|
|
257
|
+
const translate =
|
|
258
|
+
direction === 'rtl' ? Animated.multiply(translateX, -1) : translateX;
|
|
259
|
+
const offset =
|
|
260
|
+
typeof start === 'number' ? Animated.add(translate, start) : translate;
|
|
261
|
+
|
|
155
262
|
styleList.push(
|
|
156
263
|
{ width: transform[1].scaleX },
|
|
157
|
-
{ left:
|
|
264
|
+
direction === 'rtl' ? { right: offset } : { left: offset }
|
|
158
265
|
);
|
|
159
266
|
} else {
|
|
160
267
|
styleList.push(
|
|
161
|
-
{
|
|
268
|
+
{
|
|
269
|
+
width:
|
|
270
|
+
width === 'auto'
|
|
271
|
+
? // if the indicator has a constant width, use it as is
|
|
272
|
+
// we don't need to scale it to match tab width
|
|
273
|
+
(constantIndicatorWidth ?? 1)
|
|
274
|
+
: getIndicatorWidth(
|
|
275
|
+
getTabWidth(navigationState.index),
|
|
276
|
+
width,
|
|
277
|
+
flattenedStyle,
|
|
278
|
+
direction
|
|
279
|
+
),
|
|
280
|
+
},
|
|
162
281
|
{ start: `${(100 / routes.length) * navigationState.index}%` },
|
|
163
282
|
{ transform }
|
|
164
283
|
);
|
|
165
284
|
}
|
|
166
285
|
|
|
286
|
+
let finalStyle;
|
|
287
|
+
|
|
288
|
+
if (hasCustomIndicatorWidth && style != null) {
|
|
289
|
+
const rest = { ...flattenedStyle };
|
|
290
|
+
|
|
291
|
+
delete rest.width;
|
|
292
|
+
|
|
293
|
+
if (isCentered) {
|
|
294
|
+
if (rest.margin === 'auto') {
|
|
295
|
+
delete rest.margin;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (rest.marginHorizontal === 'auto') {
|
|
299
|
+
delete rest.marginHorizontal;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
finalStyle = rest;
|
|
304
|
+
} else {
|
|
305
|
+
finalStyle = style;
|
|
306
|
+
}
|
|
307
|
+
|
|
167
308
|
return (
|
|
168
309
|
<Animated.View
|
|
169
310
|
style={[
|
|
170
311
|
styles.indicator,
|
|
171
312
|
styleList,
|
|
172
313
|
width === 'auto' ? { opacity: opacity } : null,
|
|
173
|
-
|
|
314
|
+
finalStyle,
|
|
174
315
|
]}
|
|
175
316
|
>
|
|
176
317
|
{children}
|
package/src/TabBarItem.tsx
CHANGED
|
@@ -169,7 +169,7 @@ const TabBarItemInternal = <T extends Route>({
|
|
|
169
169
|
<Animated.View style={{ opacity }}>{iconEle}</Animated.View>
|
|
170
170
|
</View>
|
|
171
171
|
);
|
|
172
|
-
}, [
|
|
172
|
+
}, [route, customIcon, isFocused, ReAnimatedStyleRef, opacity]);
|
|
173
173
|
|
|
174
174
|
const renderLabel = React.useCallback(
|
|
175
175
|
() =>
|
package/src/TabBarItemLabel.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import type { StyleProp, TextStyle } from 'react-native';
|
|
3
3
|
import { StyleSheet } from 'react-native';
|
|
4
4
|
import Animated, { type AnimatedStyle } from 'react-native-reanimated';
|
|
5
|
+
|
|
5
6
|
interface TabBarItemLabelProps {
|
|
6
7
|
label?: string;
|
|
7
8
|
style: StyleProp<TextStyle>;
|
package/src/useAnimatedValue.tsx
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { Animated } from 'react-native';
|
|
3
3
|
|
|
4
|
-
export function useAnimatedValue(initialValue: number) {
|
|
4
|
+
export function useAnimatedValue(initialValue: number): Animated.Value {
|
|
5
5
|
const lazyRef = React.useRef<Animated.Value>(undefined);
|
|
6
6
|
|
|
7
7
|
if (lazyRef.current === undefined) {
|
|
8
8
|
lazyRef.current = new Animated.Value(initialValue);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
return lazyRef.current
|
|
11
|
+
return lazyRef.current;
|
|
12
12
|
}
|