@zoverdoser/react-native-tab-view 1.0.2 → 1.3.2
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 +28 -13
- package/lib/module/PagerViewAdapter.js.map +1 -1
- package/lib/module/PanResponderAdapter.js.map +1 -1
- package/lib/module/PlatformPressable.js +1 -1
- package/lib/module/PlatformPressable.js.map +1 -1
- package/lib/module/TabBar.js +27 -32
- 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 +63 -32
- package/lib/module/TabBarItem.js.map +1 -1
- package/lib/module/TabBarItemLabel.js.map +1 -1
- package/lib/module/TabView.js +2 -0
- package/lib/module/TabView.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 +3 -1
- package/lib/typescript/src/PagerViewAdapter.d.ts.map +1 -1
- package/lib/typescript/src/PanResponderAdapter.d.ts +3 -1
- package/lib/typescript/src/PanResponderAdapter.d.ts.map +1 -1
- package/lib/typescript/src/PlatformPressable.d.ts +1 -1
- package/lib/typescript/src/PlatformPressable.d.ts.map +1 -1
- package/lib/typescript/src/SceneMap.d.ts +2 -2
- package/lib/typescript/src/SceneMap.d.ts.map +1 -1
- package/lib/typescript/src/SceneView.d.ts +2 -2
- package/lib/typescript/src/SceneView.d.ts.map +1 -1
- package/lib/typescript/src/TabBar.d.ts +4 -4
- package/lib/typescript/src/TabBar.d.ts.map +1 -1
- package/lib/typescript/src/TabBarIndicator.d.ts +2 -2
- package/lib/typescript/src/TabBarIndicator.d.ts.map +1 -1
- package/lib/typescript/src/TabBarItem.d.ts +5 -2
- package/lib/typescript/src/TabBarItem.d.ts.map +1 -1
- package/lib/typescript/src/TabBarItemLabel.d.ts +1 -1
- package/lib/typescript/src/TabBarItemLabel.d.ts.map +1 -1
- package/lib/typescript/src/TabView.d.ts +2 -2
- 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/types.d.ts +2 -1
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/useAnimatedValue.d.ts.map +1 -1
- package/package.json +8 -7
- package/src/PagerViewAdapter.tsx +67 -17
- package/src/PanResponderAdapter.tsx +2 -0
- package/src/PlatformPressable.tsx +1 -1
- package/src/TabBar.tsx +50 -47
- package/src/TabBarIndicator.tsx +182 -41
- package/src/TabBarItem.tsx +111 -47
- package/src/TabBarItemLabel.tsx +1 -0
- package/src/TabView.tsx +2 -1
- package/src/types.tsx +2 -1
- 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
|
@@ -9,9 +9,11 @@ import {
|
|
|
9
9
|
View,
|
|
10
10
|
type ViewStyle,
|
|
11
11
|
} from 'react-native';
|
|
12
|
+
import type { SharedValue } from 'react-native-reanimated';
|
|
12
13
|
import {
|
|
13
14
|
interpolateColor,
|
|
14
15
|
useAnimatedStyle,
|
|
16
|
+
useDerivedValue,
|
|
15
17
|
useSharedValue,
|
|
16
18
|
} from 'react-native-reanimated';
|
|
17
19
|
import useLatestCallback from 'use-latest-callback';
|
|
@@ -27,6 +29,7 @@ import type {
|
|
|
27
29
|
|
|
28
30
|
export type Props<T extends Route> = TabDescriptor<T> & {
|
|
29
31
|
position: Animated.AnimatedInterpolation<number>;
|
|
32
|
+
animatedPosition?: SharedValue<number>;
|
|
30
33
|
route: T;
|
|
31
34
|
navigationState: NavigationState<T>;
|
|
32
35
|
pressColor?: string;
|
|
@@ -55,7 +58,6 @@ type TabBarItemInternalProps<T extends Route> = Omit<
|
|
|
55
58
|
> & {
|
|
56
59
|
isFocused: boolean;
|
|
57
60
|
index: number;
|
|
58
|
-
routesLength: number;
|
|
59
61
|
} & TabDescriptor<T>;
|
|
60
62
|
|
|
61
63
|
const ANDROID_RIPPLE_DEFAULT = { borderless: true };
|
|
@@ -69,6 +71,7 @@ const TabBarItemInternal = <T extends Route>({
|
|
|
69
71
|
onPress,
|
|
70
72
|
isFocused,
|
|
71
73
|
position,
|
|
74
|
+
animatedPosition,
|
|
72
75
|
style,
|
|
73
76
|
labelStyle,
|
|
74
77
|
onLayout,
|
|
@@ -80,18 +83,14 @@ const TabBarItemInternal = <T extends Route>({
|
|
|
80
83
|
badge: customBadge,
|
|
81
84
|
href,
|
|
82
85
|
labelText,
|
|
83
|
-
routesLength,
|
|
84
86
|
android_ripple = ANDROID_RIPPLE_DEFAULT,
|
|
85
87
|
labelAllowFontScaling,
|
|
86
88
|
route,
|
|
87
89
|
animatedStyles,
|
|
88
90
|
}: TabBarItemInternalProps<T>) => {
|
|
89
91
|
const inputRange = React.useMemo(
|
|
90
|
-
() =>
|
|
91
|
-
|
|
92
|
-
? Array.from({ length: routesLength }, (_, i) => i)
|
|
93
|
-
: [0, 1],
|
|
94
|
-
[routesLength]
|
|
92
|
+
() => [tabIndex - 1, tabIndex, tabIndex + 1],
|
|
93
|
+
[tabIndex]
|
|
95
94
|
);
|
|
96
95
|
const labelColorFromStyle = StyleSheet.flatten(labelStyle || {}).color;
|
|
97
96
|
|
|
@@ -106,70 +105,132 @@ const TabBarItemInternal = <T extends Route>({
|
|
|
106
105
|
? labelColorFromStyle
|
|
107
106
|
: DEFAULT_INACTIVE_COLOR;
|
|
108
107
|
|
|
109
|
-
const
|
|
108
|
+
const fallbackProgress = useSharedValue(isFocused ? 1 : 0);
|
|
110
109
|
|
|
111
110
|
React.useEffect(() => {
|
|
111
|
+
if (animatedPosition != null) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
112
115
|
const listenerId = position.addListener(({ value }) => {
|
|
113
116
|
const progress =
|
|
114
117
|
Math.abs(tabIndex - value) > 1 ? 0 : 1 - Math.abs(tabIndex - value);
|
|
115
|
-
|
|
118
|
+
fallbackProgress.value = progress;
|
|
116
119
|
});
|
|
120
|
+
|
|
117
121
|
return () => {
|
|
118
122
|
position.removeListener(listenerId);
|
|
119
123
|
};
|
|
120
|
-
}, [
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
124
|
+
}, [animatedPosition, fallbackProgress, position, tabIndex]);
|
|
125
|
+
|
|
126
|
+
const reanimatedProgress = useDerivedValue(() => {
|
|
127
|
+
if (animatedPosition != null) {
|
|
128
|
+
return Math.max(0, 1 - Math.abs(tabIndex - animatedPosition.value));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return fallbackProgress.value;
|
|
132
|
+
}, [animatedPosition, fallbackProgress, tabIndex]);
|
|
133
|
+
|
|
134
|
+
const reanimatedStyle = useAnimatedStyle(() => {
|
|
135
|
+
const color = interpolateColor(
|
|
136
|
+
reanimatedProgress.value,
|
|
137
|
+
[0, 1],
|
|
138
|
+
[inactiveColor, activeColor]
|
|
139
|
+
);
|
|
140
|
+
return { color };
|
|
141
|
+
}, [reanimatedProgress, activeColor, inactiveColor]);
|
|
142
|
+
|
|
143
|
+
const inactiveItemOpacity = animatedStyles?.opacity?.[0];
|
|
144
|
+
const activeItemOpacity = animatedStyles?.opacity?.[1];
|
|
145
|
+
const inactiveScale = animatedStyles?.scale?.[0];
|
|
146
|
+
const activeScale = animatedStyles?.scale?.[1];
|
|
147
|
+
|
|
148
|
+
const itemOpacity = React.useMemo(
|
|
149
|
+
() =>
|
|
150
|
+
inactiveItemOpacity !== undefined && activeItemOpacity !== undefined
|
|
151
|
+
? position.interpolate({
|
|
152
|
+
inputRange,
|
|
153
|
+
outputRange: [
|
|
154
|
+
inactiveItemOpacity,
|
|
155
|
+
activeItemOpacity,
|
|
156
|
+
inactiveItemOpacity,
|
|
157
|
+
],
|
|
158
|
+
extrapolate: 'clamp',
|
|
159
|
+
})
|
|
160
|
+
: 1,
|
|
161
|
+
[activeItemOpacity, inactiveItemOpacity, inputRange, position]
|
|
131
162
|
);
|
|
132
163
|
|
|
133
|
-
const
|
|
134
|
-
|
|
164
|
+
const activeOpacity = React.useMemo(
|
|
165
|
+
() =>
|
|
166
|
+
position.interpolate({
|
|
135
167
|
inputRange,
|
|
136
|
-
outputRange:
|
|
137
|
-
i === tabIndex
|
|
138
|
-
? animatedStyles.opacity![1]
|
|
139
|
-
: animatedStyles.opacity![0]
|
|
140
|
-
),
|
|
168
|
+
outputRange: [0, 1, 0],
|
|
141
169
|
extrapolate: 'clamp',
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
|
|
170
|
+
}),
|
|
171
|
+
[inputRange, position]
|
|
172
|
+
);
|
|
173
|
+
const inactiveOpacity = React.useMemo(
|
|
174
|
+
() =>
|
|
175
|
+
position.interpolate({
|
|
147
176
|
inputRange,
|
|
148
|
-
outputRange:
|
|
149
|
-
i === tabIndex ? animatedStyles.scale![1] : animatedStyles.scale![0]
|
|
150
|
-
),
|
|
177
|
+
outputRange: [1, 0, 1],
|
|
151
178
|
extrapolate: 'clamp',
|
|
152
|
-
})
|
|
153
|
-
|
|
179
|
+
}),
|
|
180
|
+
[inputRange, position]
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const scale = React.useMemo(
|
|
184
|
+
() =>
|
|
185
|
+
inactiveScale !== undefined && activeScale !== undefined
|
|
186
|
+
? position.interpolate({
|
|
187
|
+
inputRange,
|
|
188
|
+
outputRange: [inactiveScale, activeScale, inactiveScale],
|
|
189
|
+
extrapolate: 'clamp',
|
|
190
|
+
})
|
|
191
|
+
: 1,
|
|
192
|
+
[activeScale, inactiveScale, inputRange, position]
|
|
193
|
+
);
|
|
154
194
|
|
|
155
195
|
const icon = React.useMemo(() => {
|
|
156
196
|
if (!customIcon) {
|
|
157
197
|
return null;
|
|
158
198
|
}
|
|
159
199
|
|
|
160
|
-
const
|
|
161
|
-
focused:
|
|
162
|
-
color:
|
|
200
|
+
const inactiveIcon = customIcon({
|
|
201
|
+
focused: false,
|
|
202
|
+
color: inactiveColor,
|
|
203
|
+
size: ICON_SIZE,
|
|
204
|
+
route,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const activeIcon = customIcon({
|
|
208
|
+
focused: true,
|
|
209
|
+
color: activeColor,
|
|
163
210
|
size: ICON_SIZE,
|
|
164
211
|
route,
|
|
165
212
|
});
|
|
166
213
|
|
|
167
214
|
return (
|
|
168
215
|
<View style={styles.icon}>
|
|
169
|
-
<Animated.View style={{ opacity }}>
|
|
216
|
+
<Animated.View style={{ opacity: inactiveOpacity }}>
|
|
217
|
+
{inactiveIcon}
|
|
218
|
+
</Animated.View>
|
|
219
|
+
<Animated.View
|
|
220
|
+
style={[StyleSheet.absoluteFill, { opacity: activeOpacity }]}
|
|
221
|
+
>
|
|
222
|
+
{activeIcon}
|
|
223
|
+
</Animated.View>
|
|
170
224
|
</View>
|
|
171
225
|
);
|
|
172
|
-
}, [
|
|
226
|
+
}, [
|
|
227
|
+
activeColor,
|
|
228
|
+
activeOpacity,
|
|
229
|
+
customIcon,
|
|
230
|
+
inactiveColor,
|
|
231
|
+
inactiveOpacity,
|
|
232
|
+
route,
|
|
233
|
+
]);
|
|
173
234
|
|
|
174
235
|
const renderLabel = React.useCallback(
|
|
175
236
|
() =>
|
|
@@ -177,7 +238,7 @@ const TabBarItemInternal = <T extends Route>({
|
|
|
177
238
|
customlabel({
|
|
178
239
|
focused: isFocused,
|
|
179
240
|
style: labelStyle,
|
|
180
|
-
animatedStyles:
|
|
241
|
+
animatedStyles: reanimatedStyle,
|
|
181
242
|
labelText,
|
|
182
243
|
allowFontScaling: labelAllowFontScaling,
|
|
183
244
|
route,
|
|
@@ -187,7 +248,7 @@ const TabBarItemInternal = <T extends Route>({
|
|
|
187
248
|
icon={icon}
|
|
188
249
|
label={labelText}
|
|
189
250
|
style={labelStyle}
|
|
190
|
-
animatedStyles={
|
|
251
|
+
animatedStyles={reanimatedStyle}
|
|
191
252
|
labelAllowFontScaling={labelAllowFontScaling}
|
|
192
253
|
/>
|
|
193
254
|
),
|
|
@@ -199,7 +260,7 @@ const TabBarItemInternal = <T extends Route>({
|
|
|
199
260
|
route,
|
|
200
261
|
icon,
|
|
201
262
|
isFocused,
|
|
202
|
-
|
|
263
|
+
reanimatedStyle,
|
|
203
264
|
]
|
|
204
265
|
);
|
|
205
266
|
|
|
@@ -232,7 +293,11 @@ const TabBarItemInternal = <T extends Route>({
|
|
|
232
293
|
>
|
|
233
294
|
<Animated.View
|
|
234
295
|
pointerEvents="none"
|
|
235
|
-
style={[
|
|
296
|
+
style={[
|
|
297
|
+
styles.item,
|
|
298
|
+
tabStyle,
|
|
299
|
+
{ opacity: itemOpacity, transform: [{ scale }] },
|
|
300
|
+
]}
|
|
236
301
|
>
|
|
237
302
|
{icon}
|
|
238
303
|
<View>{renderLabel()}</View>
|
|
@@ -267,7 +332,6 @@ export function TabBarItem<T extends Route>(props: Props<T>) {
|
|
|
267
332
|
isFocused={navigationState.index === tabIndex}
|
|
268
333
|
route={route}
|
|
269
334
|
index={tabIndex}
|
|
270
|
-
routesLength={navigationState.routes.length}
|
|
271
335
|
/>
|
|
272
336
|
);
|
|
273
337
|
}
|
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/TabView.tsx
CHANGED
|
@@ -132,11 +132,12 @@ export function TabView<T extends Route>({
|
|
|
132
132
|
style={pagerStyle}
|
|
133
133
|
layoutDirection={direction}
|
|
134
134
|
>
|
|
135
|
-
{({ position, render, addEnterListener, jumpTo }) => {
|
|
135
|
+
{({ position, animatedPosition, render, addEnterListener, jumpTo }) => {
|
|
136
136
|
// All the props here must not change between re-renders
|
|
137
137
|
// This is crucial to optimizing the routes with PureComponent
|
|
138
138
|
const sceneRendererProps = {
|
|
139
139
|
position,
|
|
140
|
+
animatedPosition,
|
|
140
141
|
layout,
|
|
141
142
|
jumpTo,
|
|
142
143
|
};
|
package/src/types.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Animated, StyleProp, TextStyle, ViewStyle } from 'react-native';
|
|
2
2
|
import type { PagerViewProps } from 'react-native-pager-view';
|
|
3
|
-
import type { AnimatedStyle } from 'react-native-reanimated';
|
|
3
|
+
import type { AnimatedStyle, SharedValue } from 'react-native-reanimated';
|
|
4
4
|
|
|
5
5
|
export type TabDescriptor<T extends Route> = {
|
|
6
6
|
accessibilityLabel?: string;
|
|
@@ -63,6 +63,7 @@ export type Listener = (value: number) => void;
|
|
|
63
63
|
export type SceneRendererProps = {
|
|
64
64
|
layout: Layout;
|
|
65
65
|
position: Animated.AnimatedInterpolation<number>;
|
|
66
|
+
animatedPosition?: SharedValue<number>;
|
|
66
67
|
jumpTo: (key: string) => void;
|
|
67
68
|
};
|
|
68
69
|
|
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
|
}
|