react-native-collapsible-header-tab-view 1.0.0 → 1.0.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/dist/TabFlatList.d.ts +9 -0
- package/dist/TabFlatList.d.ts.map +1 -0
- package/dist/TabScrollView.d.ts +10 -0
- package/dist/TabScrollView.d.ts.map +1 -0
- package/dist/context.d.ts +6 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +446 -0
- package/dist/index.mjs +443 -0
- package/dist/types.d.ts +36 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +9 -2
- package/dist/a.txt +0 -0
- package/src/index.tsx +0 -357
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { FlatList, FlatListProps, NativeScrollEvent, NativeSyntheticEvent } from "react-native";
|
|
3
|
+
export type TabFlatListProps<T> = Omit<FlatListProps<T>, "onScroll"> & {
|
|
4
|
+
onScroll?: (e: NativeSyntheticEvent<NativeScrollEvent>) => void;
|
|
5
|
+
};
|
|
6
|
+
export declare const TabFlatList: <T>(props: TabFlatListProps<T> & {
|
|
7
|
+
ref?: React.Ref<FlatList<T>>;
|
|
8
|
+
}) => React.ReactElement;
|
|
9
|
+
//# sourceMappingURL=TabFlatList.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TabFlatList.d.ts","sourceRoot":"","sources":["../../src/TabFlatList.tsx"],"names":[],"mappings":"AAAA,OAAO,KAQN,MAAM,OAAO,CAAC;AACf,OAAO,EAEL,QAAQ,EACR,aAAa,EAEb,iBAAiB,EACjB,oBAAoB,EAErB,MAAM,cAAc,CAAC;AAUtB,MAAM,MAAM,gBAAgB,CAAC,CAAC,IAAI,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC,GAAG;IACrE,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAE,oBAAoB,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAC;CACjE,CAAC;AAkGF,eAAO,MAAM,WAAW,EAAmC,CAAC,CAAC,EAC3D,KAAK,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAAG;IAAE,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAA;CAAE,KAC1D,KAAK,CAAC,YAAY,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { NativeScrollEvent, NativeSyntheticEvent, ScrollView, ScrollViewProps } from "react-native";
|
|
3
|
+
export type TabScrollViewProps = Omit<ScrollViewProps, "onScroll"> & {
|
|
4
|
+
onScroll?: (e: NativeSyntheticEvent<NativeScrollEvent>) => void;
|
|
5
|
+
};
|
|
6
|
+
declare const TabScrollView: React.ForwardRefExoticComponent<Omit<ScrollViewProps, "onScroll"> & {
|
|
7
|
+
onScroll?: (e: NativeSyntheticEvent<NativeScrollEvent>) => void;
|
|
8
|
+
} & React.RefAttributes<ScrollView>>;
|
|
9
|
+
export { TabScrollView };
|
|
10
|
+
//# sourceMappingURL=TabScrollView.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TabScrollView.d.ts","sourceRoot":"","sources":["../../src/TabScrollView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAON,MAAM,OAAO,CAAA;AACd,OAAO,EAGL,iBAAiB,EACjB,oBAAoB,EACpB,UAAU,EACV,eAAe,EAChB,MAAM,cAAc,CAAA;AAIrB,MAAM,MAAM,kBAAkB,GAAG,IAAI,CAAC,eAAe,EAAE,UAAU,CAAC,GAAG;IACnE,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAE,oBAAoB,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAA;CAChE,CAAA;AAED,QAAA,MAAM,aAAa;eAHN,CAAC,CAAC,EAAE,oBAAoB,CAAC,iBAAiB,CAAC,KAAK,IAAI;oCA+DhE,CAAA;AAED,OAAO,EAAE,aAAa,EAAE,CAAA"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { CollapsibleContextValue } from "./types";
|
|
2
|
+
export declare const CollapsibleContext: import("react").Context<CollapsibleContextValue | null>;
|
|
3
|
+
export declare const TabIndexContext: import("react").Context<number>;
|
|
4
|
+
export declare const useCollapsible: () => CollapsibleContextValue;
|
|
5
|
+
export declare const useTabIndex: () => number;
|
|
6
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/context.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAA;AAEjD,eAAO,MAAM,kBAAkB,yDAAsD,CAAA;AAErF,eAAO,MAAM,eAAe,iCAA2B,CAAA;AAEvD,eAAO,MAAM,cAAc,+BAM1B,CAAA;AAED,eAAO,MAAM,WAAW,cAAoC,CAAA"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { CollapsibleTabViewProps, CollapsibleTabViewRef } from "./types";
|
|
3
|
+
export { TabFlatList } from "./TabFlatList";
|
|
4
|
+
export { TabScrollView } from "./TabScrollView";
|
|
5
|
+
export type { CollapsibleTabViewProps, CollapsibleTabViewRef, TabBarProps, } from "./types";
|
|
6
|
+
declare const CollapsibleTabView: React.ForwardRefExoticComponent<CollapsibleTabViewProps & React.RefAttributes<CollapsibleTabViewRef>>;
|
|
7
|
+
export default CollapsibleTabView;
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAON,MAAM,OAAO,CAAC;AAaf,OAAO,EAEL,uBAAuB,EACvB,qBAAqB,EAEtB,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,YAAY,EACV,uBAAuB,EACvB,qBAAqB,EACrB,WAAW,GACZ,MAAM,SAAS,CAAC;AAEjB,QAAA,MAAM,kBAAkB,uGAySvB,CAAC;AAwBF,eAAe,kBAAkB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.tsx
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
TabFlatList: () => TabFlatList,
|
|
34
|
+
TabScrollView: () => TabScrollView,
|
|
35
|
+
default: () => index_default
|
|
36
|
+
});
|
|
37
|
+
module.exports = __toCommonJS(index_exports);
|
|
38
|
+
var import_react4 = __toESM(require("react"));
|
|
39
|
+
var import_react_native3 = require("react-native");
|
|
40
|
+
var import_react_native_pager_view = __toESM(require("react-native-pager-view"));
|
|
41
|
+
|
|
42
|
+
// src/context.ts
|
|
43
|
+
var import_react = require("react");
|
|
44
|
+
var CollapsibleContext = (0, import_react.createContext)(null);
|
|
45
|
+
var TabIndexContext = (0, import_react.createContext)(0);
|
|
46
|
+
var useCollapsible = () => {
|
|
47
|
+
const ctx = (0, import_react.useContext)(CollapsibleContext);
|
|
48
|
+
if (!ctx) {
|
|
49
|
+
throw new Error("useCollapsible must be used within CollapsibleTabView");
|
|
50
|
+
}
|
|
51
|
+
return ctx;
|
|
52
|
+
};
|
|
53
|
+
var useTabIndex = () => (0, import_react.useContext)(TabIndexContext);
|
|
54
|
+
|
|
55
|
+
// src/TabFlatList.tsx
|
|
56
|
+
var import_react2 = __toESM(require("react"));
|
|
57
|
+
var import_react_native = require("react-native");
|
|
58
|
+
var AnimatedFlatListComponent = import_react_native.Animated.createAnimatedComponent(import_react_native.FlatList);
|
|
59
|
+
var AnimatedFlatList = AnimatedFlatListComponent;
|
|
60
|
+
var TabFlatListInner = ({
|
|
61
|
+
contentContainerStyle,
|
|
62
|
+
onScroll,
|
|
63
|
+
ListHeaderComponent,
|
|
64
|
+
...props
|
|
65
|
+
}, ref) => {
|
|
66
|
+
const index = useTabIndex();
|
|
67
|
+
const {
|
|
68
|
+
scrollY,
|
|
69
|
+
activeIndex,
|
|
70
|
+
stickyEnabled,
|
|
71
|
+
headerHeight,
|
|
72
|
+
tabBarHeight,
|
|
73
|
+
renderHeader,
|
|
74
|
+
renderTabBar,
|
|
75
|
+
registerRef,
|
|
76
|
+
syncScrollY
|
|
77
|
+
} = useCollapsible();
|
|
78
|
+
const innerRef = (0, import_react2.useRef)(null);
|
|
79
|
+
const isActive = activeIndex === index;
|
|
80
|
+
(0, import_react2.useEffect)(() => {
|
|
81
|
+
registerRef(index, innerRef.current);
|
|
82
|
+
return () => registerRef(index, null);
|
|
83
|
+
}, [index]);
|
|
84
|
+
(0, import_react2.useImperativeHandle)(ref, () => innerRef.current, []);
|
|
85
|
+
const handleScroll = (0, import_react2.useCallback)(
|
|
86
|
+
(e) => {
|
|
87
|
+
const y = e.nativeEvent.contentOffset.y;
|
|
88
|
+
syncScrollY(index, y);
|
|
89
|
+
onScroll?.(e);
|
|
90
|
+
},
|
|
91
|
+
[index, onScroll, syncScrollY]
|
|
92
|
+
);
|
|
93
|
+
const mergedHeader = (0, import_react2.useMemo)(() => {
|
|
94
|
+
if (stickyEnabled) return ListHeaderComponent;
|
|
95
|
+
const OriginalHeader = typeof ListHeaderComponent === "function" ? /* @__PURE__ */ import_react2.default.createElement(ListHeaderComponent, null) : ListHeaderComponent ?? null;
|
|
96
|
+
return /* @__PURE__ */ import_react2.default.createElement(import_react_native.View, null, renderHeader?.(), renderTabBar?.(), OriginalHeader);
|
|
97
|
+
}, [stickyEnabled, ListHeaderComponent, renderHeader, renderTabBar]);
|
|
98
|
+
const paddingTop = stickyEnabled ? headerHeight + tabBarHeight : 0;
|
|
99
|
+
const collapseRange = stickyEnabled ? headerHeight : 0;
|
|
100
|
+
const [containerHeight, setContainerHeight] = (0, import_react2.useState)(0);
|
|
101
|
+
const handleLayout = (0, import_react2.useCallback)((e) => {
|
|
102
|
+
setContainerHeight(e.nativeEvent.layout.height);
|
|
103
|
+
}, []);
|
|
104
|
+
const minHeight = containerHeight > 0 ? containerHeight + collapseRange : 0;
|
|
105
|
+
return /* @__PURE__ */ import_react2.default.createElement(
|
|
106
|
+
AnimatedFlatList,
|
|
107
|
+
{
|
|
108
|
+
...props,
|
|
109
|
+
ref: innerRef,
|
|
110
|
+
onLayout: handleLayout,
|
|
111
|
+
ListHeaderComponent: mergedHeader,
|
|
112
|
+
contentContainerStyle: [
|
|
113
|
+
paddingTop > 0 && { paddingTop },
|
|
114
|
+
minHeight > 0 && { minHeight },
|
|
115
|
+
contentContainerStyle
|
|
116
|
+
],
|
|
117
|
+
onScroll: isActive && stickyEnabled ? import_react_native.Animated.event(
|
|
118
|
+
[{ nativeEvent: { contentOffset: { y: scrollY } } }],
|
|
119
|
+
{
|
|
120
|
+
useNativeDriver: true,
|
|
121
|
+
listener: handleScroll
|
|
122
|
+
}
|
|
123
|
+
) : handleScroll,
|
|
124
|
+
scrollEventThrottle: 16,
|
|
125
|
+
showsVerticalScrollIndicator: false
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
};
|
|
129
|
+
var TabFlatList = (0, import_react2.forwardRef)(TabFlatListInner);
|
|
130
|
+
|
|
131
|
+
// src/TabScrollView.tsx
|
|
132
|
+
var import_react3 = __toESM(require("react"));
|
|
133
|
+
var import_react_native2 = require("react-native");
|
|
134
|
+
var TabScrollView = (0, import_react3.forwardRef)(
|
|
135
|
+
({ contentContainerStyle, onScroll, children, ...props }, ref) => {
|
|
136
|
+
const index = useTabIndex();
|
|
137
|
+
const { scrollY, activeIndex, headerHeight, tabBarHeight, registerRef, syncScrollY } = useCollapsible();
|
|
138
|
+
const innerRef = (0, import_react3.useRef)(null);
|
|
139
|
+
const isActive = activeIndex === index;
|
|
140
|
+
(0, import_react3.useEffect)(() => {
|
|
141
|
+
registerRef(index, innerRef.current);
|
|
142
|
+
return () => registerRef(index, null);
|
|
143
|
+
}, [index]);
|
|
144
|
+
(0, import_react3.useImperativeHandle)(ref, () => innerRef.current, []);
|
|
145
|
+
const handleScroll = (0, import_react3.useCallback)(
|
|
146
|
+
(e) => {
|
|
147
|
+
const y = e.nativeEvent.contentOffset.y;
|
|
148
|
+
syncScrollY(index, y);
|
|
149
|
+
onScroll?.(e);
|
|
150
|
+
},
|
|
151
|
+
[index, onScroll, syncScrollY]
|
|
152
|
+
);
|
|
153
|
+
const paddingTop = headerHeight + tabBarHeight;
|
|
154
|
+
const collapseRange = headerHeight;
|
|
155
|
+
const [containerHeight, setContainerHeight] = (0, import_react3.useState)(0);
|
|
156
|
+
const handleLayout = (0, import_react3.useCallback)((e) => {
|
|
157
|
+
setContainerHeight(e.nativeEvent.layout.height);
|
|
158
|
+
}, []);
|
|
159
|
+
const minHeight = containerHeight > 0 ? containerHeight + collapseRange : 0;
|
|
160
|
+
return /* @__PURE__ */ import_react3.default.createElement(
|
|
161
|
+
import_react_native2.Animated.ScrollView,
|
|
162
|
+
{
|
|
163
|
+
ref: innerRef,
|
|
164
|
+
...props,
|
|
165
|
+
onLayout: handleLayout,
|
|
166
|
+
contentContainerStyle: [
|
|
167
|
+
{ paddingTop },
|
|
168
|
+
minHeight > 0 && { minHeight },
|
|
169
|
+
contentContainerStyle
|
|
170
|
+
],
|
|
171
|
+
onScroll: isActive ? import_react_native2.Animated.event([{ nativeEvent: { contentOffset: { y: scrollY } } }], {
|
|
172
|
+
useNativeDriver: true,
|
|
173
|
+
listener: handleScroll
|
|
174
|
+
}) : handleScroll,
|
|
175
|
+
scrollEventThrottle: 16,
|
|
176
|
+
showsVerticalScrollIndicator: false
|
|
177
|
+
},
|
|
178
|
+
children
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
// src/index.tsx
|
|
184
|
+
var CollapsibleTabView = (0, import_react4.forwardRef)(
|
|
185
|
+
({
|
|
186
|
+
children,
|
|
187
|
+
renderHeader,
|
|
188
|
+
estimatedHeaderHeight = 0,
|
|
189
|
+
estimatedTabBarHeight = 0,
|
|
190
|
+
stickyEnabled = true,
|
|
191
|
+
stickyTop = 0,
|
|
192
|
+
renderTabBar,
|
|
193
|
+
initialTabIndex = 0,
|
|
194
|
+
onTabChange,
|
|
195
|
+
onScroll: onScrollProp,
|
|
196
|
+
swipeEnabled = true,
|
|
197
|
+
style
|
|
198
|
+
}, ref) => {
|
|
199
|
+
const [activeIndex, setActiveIndex] = (0, import_react4.useState)(initialTabIndex);
|
|
200
|
+
const pagerRef = (0, import_react4.useRef)(null);
|
|
201
|
+
const pages = (0, import_react4.useMemo)(
|
|
202
|
+
() => import_react4.default.Children.toArray(children).filter(import_react4.default.isValidElement),
|
|
203
|
+
[children]
|
|
204
|
+
);
|
|
205
|
+
(0, import_react4.useImperativeHandle)(ref, () => ({
|
|
206
|
+
scrollToTab: (index, animated = true) => {
|
|
207
|
+
if (index === activeIndex || index < 0 || index >= pages.length) return;
|
|
208
|
+
syncTabOnSwitch(index);
|
|
209
|
+
setActiveIndex(index);
|
|
210
|
+
if (animated) {
|
|
211
|
+
pagerRef.current?.setPage(index);
|
|
212
|
+
} else {
|
|
213
|
+
pagerRef.current?.setPageWithoutAnimation(index);
|
|
214
|
+
}
|
|
215
|
+
onTabChange?.(index);
|
|
216
|
+
},
|
|
217
|
+
getActiveIndex: () => activeIndex
|
|
218
|
+
}));
|
|
219
|
+
const hasEstimate = estimatedHeaderHeight > 0;
|
|
220
|
+
const headerHeightRef = (0, import_react4.useRef)(0);
|
|
221
|
+
const tabBarHeightRef = (0, import_react4.useRef)(0);
|
|
222
|
+
const [layout, setLayout] = (0, import_react4.useState)({
|
|
223
|
+
headerHeight: estimatedHeaderHeight,
|
|
224
|
+
tabBarHeight: estimatedTabBarHeight,
|
|
225
|
+
ready: hasEstimate
|
|
226
|
+
});
|
|
227
|
+
const { headerHeight, tabBarHeight } = layout;
|
|
228
|
+
const visible = layout.ready;
|
|
229
|
+
const adjustY = (0, import_react4.useRef)(new import_react_native3.Animated.Value(0)).current;
|
|
230
|
+
const tryCommitLayout = (0, import_react4.useCallback)(() => {
|
|
231
|
+
const h = headerHeightRef.current;
|
|
232
|
+
const t = tabBarHeightRef.current;
|
|
233
|
+
if (h > 0 && t > 0) {
|
|
234
|
+
setLayout((prev) => {
|
|
235
|
+
if (Math.abs(prev.headerHeight - h) <= 1 && Math.abs(prev.tabBarHeight - t) <= 1) {
|
|
236
|
+
return prev;
|
|
237
|
+
}
|
|
238
|
+
const diff = h + t - (prev.headerHeight + prev.tabBarHeight);
|
|
239
|
+
if (prev.ready && Math.abs(diff) > 1) {
|
|
240
|
+
adjustY.setValue(-diff);
|
|
241
|
+
import_react_native3.Animated.timing(adjustY, {
|
|
242
|
+
toValue: 0,
|
|
243
|
+
duration: 200,
|
|
244
|
+
useNativeDriver: true
|
|
245
|
+
}).start();
|
|
246
|
+
}
|
|
247
|
+
return { headerHeight: h, tabBarHeight: t, ready: true };
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}, []);
|
|
251
|
+
const handleHeaderLayout = (0, import_react4.useCallback)(
|
|
252
|
+
(e) => {
|
|
253
|
+
headerHeightRef.current = e.nativeEvent.layout.height;
|
|
254
|
+
tryCommitLayout();
|
|
255
|
+
},
|
|
256
|
+
[tryCommitLayout]
|
|
257
|
+
);
|
|
258
|
+
const handleTabBarLayout = (0, import_react4.useCallback)(
|
|
259
|
+
(e) => {
|
|
260
|
+
tabBarHeightRef.current = e.nativeEvent.layout.height;
|
|
261
|
+
tryCommitLayout();
|
|
262
|
+
},
|
|
263
|
+
[tryCommitLayout]
|
|
264
|
+
);
|
|
265
|
+
const scrollY = (0, import_react4.useRef)(new import_react_native3.Animated.Value(0)).current;
|
|
266
|
+
const tabScrollYMap = (0, import_react4.useRef)(/* @__PURE__ */ new Map());
|
|
267
|
+
const tabRefs = (0, import_react4.useRef)(
|
|
268
|
+
/* @__PURE__ */ new Map()
|
|
269
|
+
);
|
|
270
|
+
const collapseRange = stickyEnabled ? Math.max(headerHeight - stickyTop, 0) : 0;
|
|
271
|
+
const headerTranslateY = (0, import_react4.useMemo)(
|
|
272
|
+
() => collapseRange > 0 ? scrollY.interpolate({
|
|
273
|
+
inputRange: [0, collapseRange],
|
|
274
|
+
outputRange: [0, -collapseRange],
|
|
275
|
+
extrapolate: "clamp"
|
|
276
|
+
}) : new import_react_native3.Animated.Value(0),
|
|
277
|
+
[scrollY, collapseRange]
|
|
278
|
+
);
|
|
279
|
+
const registerRef = (0, import_react4.useCallback)(
|
|
280
|
+
(index, ref2) => {
|
|
281
|
+
if (ref2) {
|
|
282
|
+
tabRefs.current.set(index, ref2);
|
|
283
|
+
} else {
|
|
284
|
+
tabRefs.current.delete(index);
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
[]
|
|
288
|
+
);
|
|
289
|
+
const syncScrollY = (0, import_react4.useCallback)(
|
|
290
|
+
(index, y) => {
|
|
291
|
+
tabScrollYMap.current.set(index, y);
|
|
292
|
+
onScrollProp?.(y);
|
|
293
|
+
},
|
|
294
|
+
[onScrollProp]
|
|
295
|
+
);
|
|
296
|
+
const scrollTabTo = (0, import_react4.useCallback)((index, offset) => {
|
|
297
|
+
const ref2 = tabRefs.current.get(index);
|
|
298
|
+
if (!ref2) return;
|
|
299
|
+
if (ref2.scrollToOffset) {
|
|
300
|
+
ref2.scrollToOffset({ offset, animated: false });
|
|
301
|
+
} else if (ref2.scrollTo) {
|
|
302
|
+
ref2.scrollTo({ y: offset, animated: false });
|
|
303
|
+
} else if (ref2.getNode) {
|
|
304
|
+
const node = ref2.getNode();
|
|
305
|
+
if (node?.scrollToOffset) {
|
|
306
|
+
node.scrollToOffset({ offset, animated: false });
|
|
307
|
+
} else if (node?.scrollTo) {
|
|
308
|
+
node.scrollTo({ y: offset, animated: false });
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}, []);
|
|
312
|
+
const syncTabOnSwitch = (0, import_react4.useCallback)(
|
|
313
|
+
(newIndex) => {
|
|
314
|
+
const currentY = tabScrollYMap.current.get(activeIndex) ?? 0;
|
|
315
|
+
const newTabSavedY = tabScrollYMap.current.get(newIndex) ?? 0;
|
|
316
|
+
const isCollapsed = currentY >= collapseRange - 1;
|
|
317
|
+
let targetY;
|
|
318
|
+
if (isCollapsed) {
|
|
319
|
+
targetY = Math.max(newTabSavedY, collapseRange);
|
|
320
|
+
} else {
|
|
321
|
+
targetY = Math.min(currentY, collapseRange);
|
|
322
|
+
}
|
|
323
|
+
import_react_native3.InteractionManager.runAfterInteractions(() => {
|
|
324
|
+
scrollTabTo(newIndex, targetY);
|
|
325
|
+
tabScrollYMap.current.set(newIndex, targetY);
|
|
326
|
+
scrollY.setValue(Math.min(targetY, collapseRange));
|
|
327
|
+
});
|
|
328
|
+
},
|
|
329
|
+
[activeIndex, collapseRange, scrollTabTo, scrollY]
|
|
330
|
+
);
|
|
331
|
+
const handleTabPress = (0, import_react4.useCallback)(
|
|
332
|
+
(index) => {
|
|
333
|
+
if (index === activeIndex) return;
|
|
334
|
+
syncTabOnSwitch(index);
|
|
335
|
+
setActiveIndex(index);
|
|
336
|
+
pagerRef.current?.setPageWithoutAnimation(index);
|
|
337
|
+
onTabChange?.(index);
|
|
338
|
+
},
|
|
339
|
+
[activeIndex, onTabChange, syncTabOnSwitch]
|
|
340
|
+
);
|
|
341
|
+
const handlePageSelected = (0, import_react4.useCallback)(
|
|
342
|
+
(e) => {
|
|
343
|
+
const newIndex = e.nativeEvent.position;
|
|
344
|
+
if (newIndex === activeIndex) return;
|
|
345
|
+
syncTabOnSwitch(newIndex);
|
|
346
|
+
setActiveIndex(newIndex);
|
|
347
|
+
onTabChange?.(newIndex);
|
|
348
|
+
},
|
|
349
|
+
[activeIndex, onTabChange, syncTabOnSwitch]
|
|
350
|
+
);
|
|
351
|
+
const tabBarProps = (0, import_react4.useMemo)(
|
|
352
|
+
() => ({
|
|
353
|
+
activeIndex,
|
|
354
|
+
onTabPress: handleTabPress
|
|
355
|
+
}),
|
|
356
|
+
[activeIndex, handleTabPress]
|
|
357
|
+
);
|
|
358
|
+
const renderTabBarNode = (0, import_react4.useCallback)(
|
|
359
|
+
() => renderTabBar(tabBarProps),
|
|
360
|
+
[renderTabBar, tabBarProps]
|
|
361
|
+
);
|
|
362
|
+
const contextValue = (0, import_react4.useMemo)(
|
|
363
|
+
() => ({
|
|
364
|
+
scrollY,
|
|
365
|
+
activeIndex,
|
|
366
|
+
stickyEnabled,
|
|
367
|
+
headerHeight,
|
|
368
|
+
tabBarHeight,
|
|
369
|
+
renderHeader: stickyEnabled ? void 0 : renderHeader,
|
|
370
|
+
renderTabBar: stickyEnabled ? void 0 : renderTabBarNode,
|
|
371
|
+
registerRef,
|
|
372
|
+
syncScrollY
|
|
373
|
+
}),
|
|
374
|
+
[
|
|
375
|
+
scrollY,
|
|
376
|
+
activeIndex,
|
|
377
|
+
stickyEnabled,
|
|
378
|
+
headerHeight,
|
|
379
|
+
tabBarHeight,
|
|
380
|
+
renderHeader,
|
|
381
|
+
renderTabBarNode,
|
|
382
|
+
registerRef,
|
|
383
|
+
syncScrollY
|
|
384
|
+
]
|
|
385
|
+
);
|
|
386
|
+
return /* @__PURE__ */ import_react4.default.createElement(CollapsibleContext.Provider, { value: contextValue }, /* @__PURE__ */ import_react4.default.createElement(import_react_native3.View, { style: [styles.container, style] }, /* @__PURE__ */ import_react4.default.createElement(
|
|
387
|
+
import_react_native3.Animated.View,
|
|
388
|
+
{
|
|
389
|
+
style: [
|
|
390
|
+
styles.pager,
|
|
391
|
+
!visible && styles.hidden,
|
|
392
|
+
{ transform: [{ translateY: adjustY }] }
|
|
393
|
+
]
|
|
394
|
+
},
|
|
395
|
+
/* @__PURE__ */ import_react4.default.createElement(
|
|
396
|
+
import_react_native_pager_view.default,
|
|
397
|
+
{
|
|
398
|
+
ref: pagerRef,
|
|
399
|
+
style: styles.pager,
|
|
400
|
+
initialPage: initialTabIndex,
|
|
401
|
+
onPageSelected: handlePageSelected,
|
|
402
|
+
scrollEnabled: swipeEnabled
|
|
403
|
+
},
|
|
404
|
+
pages.map((page, i) => /* @__PURE__ */ import_react4.default.createElement(import_react_native3.View, { key: page?.key ?? i, style: styles.page }, /* @__PURE__ */ import_react4.default.createElement(TabIndexContext.Provider, { value: i }, page)))
|
|
405
|
+
)
|
|
406
|
+
), stickyEnabled && /* @__PURE__ */ import_react4.default.createElement(
|
|
407
|
+
import_react_native3.Animated.View,
|
|
408
|
+
{
|
|
409
|
+
style: [
|
|
410
|
+
styles.overlay,
|
|
411
|
+
{ transform: [{ translateY: headerTranslateY }] }
|
|
412
|
+
],
|
|
413
|
+
pointerEvents: "box-none"
|
|
414
|
+
},
|
|
415
|
+
/* @__PURE__ */ import_react4.default.createElement(import_react_native3.View, { pointerEvents: "box-none", onLayout: handleHeaderLayout }, renderHeader()),
|
|
416
|
+
/* @__PURE__ */ import_react4.default.createElement(import_react_native3.View, { onLayout: handleTabBarLayout }, renderTabBarNode())
|
|
417
|
+
)));
|
|
418
|
+
}
|
|
419
|
+
);
|
|
420
|
+
var styles = import_react_native3.StyleSheet.create({
|
|
421
|
+
container: {
|
|
422
|
+
flex: 1
|
|
423
|
+
},
|
|
424
|
+
pager: {
|
|
425
|
+
flex: 1
|
|
426
|
+
},
|
|
427
|
+
hidden: {
|
|
428
|
+
opacity: 0
|
|
429
|
+
},
|
|
430
|
+
page: {
|
|
431
|
+
flex: 1
|
|
432
|
+
},
|
|
433
|
+
overlay: {
|
|
434
|
+
position: "absolute",
|
|
435
|
+
top: 0,
|
|
436
|
+
left: 0,
|
|
437
|
+
right: 0,
|
|
438
|
+
zIndex: 10
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
var index_default = CollapsibleTabView;
|
|
442
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
443
|
+
0 && (module.exports = {
|
|
444
|
+
TabFlatList,
|
|
445
|
+
TabScrollView
|
|
446
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
// src/index.tsx
|
|
2
|
+
import React3, {
|
|
3
|
+
forwardRef as forwardRef3,
|
|
4
|
+
useCallback as useCallback3,
|
|
5
|
+
useImperativeHandle as useImperativeHandle3,
|
|
6
|
+
useMemo as useMemo2,
|
|
7
|
+
useRef as useRef3,
|
|
8
|
+
useState as useState3
|
|
9
|
+
} from "react";
|
|
10
|
+
import {
|
|
11
|
+
Animated as Animated3,
|
|
12
|
+
InteractionManager,
|
|
13
|
+
StyleSheet,
|
|
14
|
+
View as View2
|
|
15
|
+
} from "react-native";
|
|
16
|
+
import PagerView from "react-native-pager-view";
|
|
17
|
+
|
|
18
|
+
// src/context.ts
|
|
19
|
+
import { createContext, useContext } from "react";
|
|
20
|
+
var CollapsibleContext = createContext(null);
|
|
21
|
+
var TabIndexContext = createContext(0);
|
|
22
|
+
var useCollapsible = () => {
|
|
23
|
+
const ctx = useContext(CollapsibleContext);
|
|
24
|
+
if (!ctx) {
|
|
25
|
+
throw new Error("useCollapsible must be used within CollapsibleTabView");
|
|
26
|
+
}
|
|
27
|
+
return ctx;
|
|
28
|
+
};
|
|
29
|
+
var useTabIndex = () => useContext(TabIndexContext);
|
|
30
|
+
|
|
31
|
+
// src/TabFlatList.tsx
|
|
32
|
+
import React, {
|
|
33
|
+
forwardRef,
|
|
34
|
+
useCallback,
|
|
35
|
+
useEffect,
|
|
36
|
+
useImperativeHandle,
|
|
37
|
+
useMemo,
|
|
38
|
+
useRef,
|
|
39
|
+
useState
|
|
40
|
+
} from "react";
|
|
41
|
+
import {
|
|
42
|
+
Animated,
|
|
43
|
+
FlatList,
|
|
44
|
+
View
|
|
45
|
+
} from "react-native";
|
|
46
|
+
var AnimatedFlatListComponent = Animated.createAnimatedComponent(FlatList);
|
|
47
|
+
var AnimatedFlatList = AnimatedFlatListComponent;
|
|
48
|
+
var TabFlatListInner = ({
|
|
49
|
+
contentContainerStyle,
|
|
50
|
+
onScroll,
|
|
51
|
+
ListHeaderComponent,
|
|
52
|
+
...props
|
|
53
|
+
}, ref) => {
|
|
54
|
+
const index = useTabIndex();
|
|
55
|
+
const {
|
|
56
|
+
scrollY,
|
|
57
|
+
activeIndex,
|
|
58
|
+
stickyEnabled,
|
|
59
|
+
headerHeight,
|
|
60
|
+
tabBarHeight,
|
|
61
|
+
renderHeader,
|
|
62
|
+
renderTabBar,
|
|
63
|
+
registerRef,
|
|
64
|
+
syncScrollY
|
|
65
|
+
} = useCollapsible();
|
|
66
|
+
const innerRef = useRef(null);
|
|
67
|
+
const isActive = activeIndex === index;
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
registerRef(index, innerRef.current);
|
|
70
|
+
return () => registerRef(index, null);
|
|
71
|
+
}, [index]);
|
|
72
|
+
useImperativeHandle(ref, () => innerRef.current, []);
|
|
73
|
+
const handleScroll = useCallback(
|
|
74
|
+
(e) => {
|
|
75
|
+
const y = e.nativeEvent.contentOffset.y;
|
|
76
|
+
syncScrollY(index, y);
|
|
77
|
+
onScroll?.(e);
|
|
78
|
+
},
|
|
79
|
+
[index, onScroll, syncScrollY]
|
|
80
|
+
);
|
|
81
|
+
const mergedHeader = useMemo(() => {
|
|
82
|
+
if (stickyEnabled) return ListHeaderComponent;
|
|
83
|
+
const OriginalHeader = typeof ListHeaderComponent === "function" ? /* @__PURE__ */ React.createElement(ListHeaderComponent, null) : ListHeaderComponent ?? null;
|
|
84
|
+
return /* @__PURE__ */ React.createElement(View, null, renderHeader?.(), renderTabBar?.(), OriginalHeader);
|
|
85
|
+
}, [stickyEnabled, ListHeaderComponent, renderHeader, renderTabBar]);
|
|
86
|
+
const paddingTop = stickyEnabled ? headerHeight + tabBarHeight : 0;
|
|
87
|
+
const collapseRange = stickyEnabled ? headerHeight : 0;
|
|
88
|
+
const [containerHeight, setContainerHeight] = useState(0);
|
|
89
|
+
const handleLayout = useCallback((e) => {
|
|
90
|
+
setContainerHeight(e.nativeEvent.layout.height);
|
|
91
|
+
}, []);
|
|
92
|
+
const minHeight = containerHeight > 0 ? containerHeight + collapseRange : 0;
|
|
93
|
+
return /* @__PURE__ */ React.createElement(
|
|
94
|
+
AnimatedFlatList,
|
|
95
|
+
{
|
|
96
|
+
...props,
|
|
97
|
+
ref: innerRef,
|
|
98
|
+
onLayout: handleLayout,
|
|
99
|
+
ListHeaderComponent: mergedHeader,
|
|
100
|
+
contentContainerStyle: [
|
|
101
|
+
paddingTop > 0 && { paddingTop },
|
|
102
|
+
minHeight > 0 && { minHeight },
|
|
103
|
+
contentContainerStyle
|
|
104
|
+
],
|
|
105
|
+
onScroll: isActive && stickyEnabled ? Animated.event(
|
|
106
|
+
[{ nativeEvent: { contentOffset: { y: scrollY } } }],
|
|
107
|
+
{
|
|
108
|
+
useNativeDriver: true,
|
|
109
|
+
listener: handleScroll
|
|
110
|
+
}
|
|
111
|
+
) : handleScroll,
|
|
112
|
+
scrollEventThrottle: 16,
|
|
113
|
+
showsVerticalScrollIndicator: false
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
};
|
|
117
|
+
var TabFlatList = forwardRef(TabFlatListInner);
|
|
118
|
+
|
|
119
|
+
// src/TabScrollView.tsx
|
|
120
|
+
import React2, {
|
|
121
|
+
forwardRef as forwardRef2,
|
|
122
|
+
useCallback as useCallback2,
|
|
123
|
+
useEffect as useEffect2,
|
|
124
|
+
useImperativeHandle as useImperativeHandle2,
|
|
125
|
+
useRef as useRef2,
|
|
126
|
+
useState as useState2
|
|
127
|
+
} from "react";
|
|
128
|
+
import {
|
|
129
|
+
Animated as Animated2
|
|
130
|
+
} from "react-native";
|
|
131
|
+
var TabScrollView = forwardRef2(
|
|
132
|
+
({ contentContainerStyle, onScroll, children, ...props }, ref) => {
|
|
133
|
+
const index = useTabIndex();
|
|
134
|
+
const { scrollY, activeIndex, headerHeight, tabBarHeight, registerRef, syncScrollY } = useCollapsible();
|
|
135
|
+
const innerRef = useRef2(null);
|
|
136
|
+
const isActive = activeIndex === index;
|
|
137
|
+
useEffect2(() => {
|
|
138
|
+
registerRef(index, innerRef.current);
|
|
139
|
+
return () => registerRef(index, null);
|
|
140
|
+
}, [index]);
|
|
141
|
+
useImperativeHandle2(ref, () => innerRef.current, []);
|
|
142
|
+
const handleScroll = useCallback2(
|
|
143
|
+
(e) => {
|
|
144
|
+
const y = e.nativeEvent.contentOffset.y;
|
|
145
|
+
syncScrollY(index, y);
|
|
146
|
+
onScroll?.(e);
|
|
147
|
+
},
|
|
148
|
+
[index, onScroll, syncScrollY]
|
|
149
|
+
);
|
|
150
|
+
const paddingTop = headerHeight + tabBarHeight;
|
|
151
|
+
const collapseRange = headerHeight;
|
|
152
|
+
const [containerHeight, setContainerHeight] = useState2(0);
|
|
153
|
+
const handleLayout = useCallback2((e) => {
|
|
154
|
+
setContainerHeight(e.nativeEvent.layout.height);
|
|
155
|
+
}, []);
|
|
156
|
+
const minHeight = containerHeight > 0 ? containerHeight + collapseRange : 0;
|
|
157
|
+
return /* @__PURE__ */ React2.createElement(
|
|
158
|
+
Animated2.ScrollView,
|
|
159
|
+
{
|
|
160
|
+
ref: innerRef,
|
|
161
|
+
...props,
|
|
162
|
+
onLayout: handleLayout,
|
|
163
|
+
contentContainerStyle: [
|
|
164
|
+
{ paddingTop },
|
|
165
|
+
minHeight > 0 && { minHeight },
|
|
166
|
+
contentContainerStyle
|
|
167
|
+
],
|
|
168
|
+
onScroll: isActive ? Animated2.event([{ nativeEvent: { contentOffset: { y: scrollY } } }], {
|
|
169
|
+
useNativeDriver: true,
|
|
170
|
+
listener: handleScroll
|
|
171
|
+
}) : handleScroll,
|
|
172
|
+
scrollEventThrottle: 16,
|
|
173
|
+
showsVerticalScrollIndicator: false
|
|
174
|
+
},
|
|
175
|
+
children
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// src/index.tsx
|
|
181
|
+
var CollapsibleTabView = forwardRef3(
|
|
182
|
+
({
|
|
183
|
+
children,
|
|
184
|
+
renderHeader,
|
|
185
|
+
estimatedHeaderHeight = 0,
|
|
186
|
+
estimatedTabBarHeight = 0,
|
|
187
|
+
stickyEnabled = true,
|
|
188
|
+
stickyTop = 0,
|
|
189
|
+
renderTabBar,
|
|
190
|
+
initialTabIndex = 0,
|
|
191
|
+
onTabChange,
|
|
192
|
+
onScroll: onScrollProp,
|
|
193
|
+
swipeEnabled = true,
|
|
194
|
+
style
|
|
195
|
+
}, ref) => {
|
|
196
|
+
const [activeIndex, setActiveIndex] = useState3(initialTabIndex);
|
|
197
|
+
const pagerRef = useRef3(null);
|
|
198
|
+
const pages = useMemo2(
|
|
199
|
+
() => React3.Children.toArray(children).filter(React3.isValidElement),
|
|
200
|
+
[children]
|
|
201
|
+
);
|
|
202
|
+
useImperativeHandle3(ref, () => ({
|
|
203
|
+
scrollToTab: (index, animated = true) => {
|
|
204
|
+
if (index === activeIndex || index < 0 || index >= pages.length) return;
|
|
205
|
+
syncTabOnSwitch(index);
|
|
206
|
+
setActiveIndex(index);
|
|
207
|
+
if (animated) {
|
|
208
|
+
pagerRef.current?.setPage(index);
|
|
209
|
+
} else {
|
|
210
|
+
pagerRef.current?.setPageWithoutAnimation(index);
|
|
211
|
+
}
|
|
212
|
+
onTabChange?.(index);
|
|
213
|
+
},
|
|
214
|
+
getActiveIndex: () => activeIndex
|
|
215
|
+
}));
|
|
216
|
+
const hasEstimate = estimatedHeaderHeight > 0;
|
|
217
|
+
const headerHeightRef = useRef3(0);
|
|
218
|
+
const tabBarHeightRef = useRef3(0);
|
|
219
|
+
const [layout, setLayout] = useState3({
|
|
220
|
+
headerHeight: estimatedHeaderHeight,
|
|
221
|
+
tabBarHeight: estimatedTabBarHeight,
|
|
222
|
+
ready: hasEstimate
|
|
223
|
+
});
|
|
224
|
+
const { headerHeight, tabBarHeight } = layout;
|
|
225
|
+
const visible = layout.ready;
|
|
226
|
+
const adjustY = useRef3(new Animated3.Value(0)).current;
|
|
227
|
+
const tryCommitLayout = useCallback3(() => {
|
|
228
|
+
const h = headerHeightRef.current;
|
|
229
|
+
const t = tabBarHeightRef.current;
|
|
230
|
+
if (h > 0 && t > 0) {
|
|
231
|
+
setLayout((prev) => {
|
|
232
|
+
if (Math.abs(prev.headerHeight - h) <= 1 && Math.abs(prev.tabBarHeight - t) <= 1) {
|
|
233
|
+
return prev;
|
|
234
|
+
}
|
|
235
|
+
const diff = h + t - (prev.headerHeight + prev.tabBarHeight);
|
|
236
|
+
if (prev.ready && Math.abs(diff) > 1) {
|
|
237
|
+
adjustY.setValue(-diff);
|
|
238
|
+
Animated3.timing(adjustY, {
|
|
239
|
+
toValue: 0,
|
|
240
|
+
duration: 200,
|
|
241
|
+
useNativeDriver: true
|
|
242
|
+
}).start();
|
|
243
|
+
}
|
|
244
|
+
return { headerHeight: h, tabBarHeight: t, ready: true };
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}, []);
|
|
248
|
+
const handleHeaderLayout = useCallback3(
|
|
249
|
+
(e) => {
|
|
250
|
+
headerHeightRef.current = e.nativeEvent.layout.height;
|
|
251
|
+
tryCommitLayout();
|
|
252
|
+
},
|
|
253
|
+
[tryCommitLayout]
|
|
254
|
+
);
|
|
255
|
+
const handleTabBarLayout = useCallback3(
|
|
256
|
+
(e) => {
|
|
257
|
+
tabBarHeightRef.current = e.nativeEvent.layout.height;
|
|
258
|
+
tryCommitLayout();
|
|
259
|
+
},
|
|
260
|
+
[tryCommitLayout]
|
|
261
|
+
);
|
|
262
|
+
const scrollY = useRef3(new Animated3.Value(0)).current;
|
|
263
|
+
const tabScrollYMap = useRef3(/* @__PURE__ */ new Map());
|
|
264
|
+
const tabRefs = useRef3(
|
|
265
|
+
/* @__PURE__ */ new Map()
|
|
266
|
+
);
|
|
267
|
+
const collapseRange = stickyEnabled ? Math.max(headerHeight - stickyTop, 0) : 0;
|
|
268
|
+
const headerTranslateY = useMemo2(
|
|
269
|
+
() => collapseRange > 0 ? scrollY.interpolate({
|
|
270
|
+
inputRange: [0, collapseRange],
|
|
271
|
+
outputRange: [0, -collapseRange],
|
|
272
|
+
extrapolate: "clamp"
|
|
273
|
+
}) : new Animated3.Value(0),
|
|
274
|
+
[scrollY, collapseRange]
|
|
275
|
+
);
|
|
276
|
+
const registerRef = useCallback3(
|
|
277
|
+
(index, ref2) => {
|
|
278
|
+
if (ref2) {
|
|
279
|
+
tabRefs.current.set(index, ref2);
|
|
280
|
+
} else {
|
|
281
|
+
tabRefs.current.delete(index);
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
[]
|
|
285
|
+
);
|
|
286
|
+
const syncScrollY = useCallback3(
|
|
287
|
+
(index, y) => {
|
|
288
|
+
tabScrollYMap.current.set(index, y);
|
|
289
|
+
onScrollProp?.(y);
|
|
290
|
+
},
|
|
291
|
+
[onScrollProp]
|
|
292
|
+
);
|
|
293
|
+
const scrollTabTo = useCallback3((index, offset) => {
|
|
294
|
+
const ref2 = tabRefs.current.get(index);
|
|
295
|
+
if (!ref2) return;
|
|
296
|
+
if (ref2.scrollToOffset) {
|
|
297
|
+
ref2.scrollToOffset({ offset, animated: false });
|
|
298
|
+
} else if (ref2.scrollTo) {
|
|
299
|
+
ref2.scrollTo({ y: offset, animated: false });
|
|
300
|
+
} else if (ref2.getNode) {
|
|
301
|
+
const node = ref2.getNode();
|
|
302
|
+
if (node?.scrollToOffset) {
|
|
303
|
+
node.scrollToOffset({ offset, animated: false });
|
|
304
|
+
} else if (node?.scrollTo) {
|
|
305
|
+
node.scrollTo({ y: offset, animated: false });
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}, []);
|
|
309
|
+
const syncTabOnSwitch = useCallback3(
|
|
310
|
+
(newIndex) => {
|
|
311
|
+
const currentY = tabScrollYMap.current.get(activeIndex) ?? 0;
|
|
312
|
+
const newTabSavedY = tabScrollYMap.current.get(newIndex) ?? 0;
|
|
313
|
+
const isCollapsed = currentY >= collapseRange - 1;
|
|
314
|
+
let targetY;
|
|
315
|
+
if (isCollapsed) {
|
|
316
|
+
targetY = Math.max(newTabSavedY, collapseRange);
|
|
317
|
+
} else {
|
|
318
|
+
targetY = Math.min(currentY, collapseRange);
|
|
319
|
+
}
|
|
320
|
+
InteractionManager.runAfterInteractions(() => {
|
|
321
|
+
scrollTabTo(newIndex, targetY);
|
|
322
|
+
tabScrollYMap.current.set(newIndex, targetY);
|
|
323
|
+
scrollY.setValue(Math.min(targetY, collapseRange));
|
|
324
|
+
});
|
|
325
|
+
},
|
|
326
|
+
[activeIndex, collapseRange, scrollTabTo, scrollY]
|
|
327
|
+
);
|
|
328
|
+
const handleTabPress = useCallback3(
|
|
329
|
+
(index) => {
|
|
330
|
+
if (index === activeIndex) return;
|
|
331
|
+
syncTabOnSwitch(index);
|
|
332
|
+
setActiveIndex(index);
|
|
333
|
+
pagerRef.current?.setPageWithoutAnimation(index);
|
|
334
|
+
onTabChange?.(index);
|
|
335
|
+
},
|
|
336
|
+
[activeIndex, onTabChange, syncTabOnSwitch]
|
|
337
|
+
);
|
|
338
|
+
const handlePageSelected = useCallback3(
|
|
339
|
+
(e) => {
|
|
340
|
+
const newIndex = e.nativeEvent.position;
|
|
341
|
+
if (newIndex === activeIndex) return;
|
|
342
|
+
syncTabOnSwitch(newIndex);
|
|
343
|
+
setActiveIndex(newIndex);
|
|
344
|
+
onTabChange?.(newIndex);
|
|
345
|
+
},
|
|
346
|
+
[activeIndex, onTabChange, syncTabOnSwitch]
|
|
347
|
+
);
|
|
348
|
+
const tabBarProps = useMemo2(
|
|
349
|
+
() => ({
|
|
350
|
+
activeIndex,
|
|
351
|
+
onTabPress: handleTabPress
|
|
352
|
+
}),
|
|
353
|
+
[activeIndex, handleTabPress]
|
|
354
|
+
);
|
|
355
|
+
const renderTabBarNode = useCallback3(
|
|
356
|
+
() => renderTabBar(tabBarProps),
|
|
357
|
+
[renderTabBar, tabBarProps]
|
|
358
|
+
);
|
|
359
|
+
const contextValue = useMemo2(
|
|
360
|
+
() => ({
|
|
361
|
+
scrollY,
|
|
362
|
+
activeIndex,
|
|
363
|
+
stickyEnabled,
|
|
364
|
+
headerHeight,
|
|
365
|
+
tabBarHeight,
|
|
366
|
+
renderHeader: stickyEnabled ? void 0 : renderHeader,
|
|
367
|
+
renderTabBar: stickyEnabled ? void 0 : renderTabBarNode,
|
|
368
|
+
registerRef,
|
|
369
|
+
syncScrollY
|
|
370
|
+
}),
|
|
371
|
+
[
|
|
372
|
+
scrollY,
|
|
373
|
+
activeIndex,
|
|
374
|
+
stickyEnabled,
|
|
375
|
+
headerHeight,
|
|
376
|
+
tabBarHeight,
|
|
377
|
+
renderHeader,
|
|
378
|
+
renderTabBarNode,
|
|
379
|
+
registerRef,
|
|
380
|
+
syncScrollY
|
|
381
|
+
]
|
|
382
|
+
);
|
|
383
|
+
return /* @__PURE__ */ React3.createElement(CollapsibleContext.Provider, { value: contextValue }, /* @__PURE__ */ React3.createElement(View2, { style: [styles.container, style] }, /* @__PURE__ */ React3.createElement(
|
|
384
|
+
Animated3.View,
|
|
385
|
+
{
|
|
386
|
+
style: [
|
|
387
|
+
styles.pager,
|
|
388
|
+
!visible && styles.hidden,
|
|
389
|
+
{ transform: [{ translateY: adjustY }] }
|
|
390
|
+
]
|
|
391
|
+
},
|
|
392
|
+
/* @__PURE__ */ React3.createElement(
|
|
393
|
+
PagerView,
|
|
394
|
+
{
|
|
395
|
+
ref: pagerRef,
|
|
396
|
+
style: styles.pager,
|
|
397
|
+
initialPage: initialTabIndex,
|
|
398
|
+
onPageSelected: handlePageSelected,
|
|
399
|
+
scrollEnabled: swipeEnabled
|
|
400
|
+
},
|
|
401
|
+
pages.map((page, i) => /* @__PURE__ */ React3.createElement(View2, { key: page?.key ?? i, style: styles.page }, /* @__PURE__ */ React3.createElement(TabIndexContext.Provider, { value: i }, page)))
|
|
402
|
+
)
|
|
403
|
+
), stickyEnabled && /* @__PURE__ */ React3.createElement(
|
|
404
|
+
Animated3.View,
|
|
405
|
+
{
|
|
406
|
+
style: [
|
|
407
|
+
styles.overlay,
|
|
408
|
+
{ transform: [{ translateY: headerTranslateY }] }
|
|
409
|
+
],
|
|
410
|
+
pointerEvents: "box-none"
|
|
411
|
+
},
|
|
412
|
+
/* @__PURE__ */ React3.createElement(View2, { pointerEvents: "box-none", onLayout: handleHeaderLayout }, renderHeader()),
|
|
413
|
+
/* @__PURE__ */ React3.createElement(View2, { onLayout: handleTabBarLayout }, renderTabBarNode())
|
|
414
|
+
)));
|
|
415
|
+
}
|
|
416
|
+
);
|
|
417
|
+
var styles = StyleSheet.create({
|
|
418
|
+
container: {
|
|
419
|
+
flex: 1
|
|
420
|
+
},
|
|
421
|
+
pager: {
|
|
422
|
+
flex: 1
|
|
423
|
+
},
|
|
424
|
+
hidden: {
|
|
425
|
+
opacity: 0
|
|
426
|
+
},
|
|
427
|
+
page: {
|
|
428
|
+
flex: 1
|
|
429
|
+
},
|
|
430
|
+
overlay: {
|
|
431
|
+
position: "absolute",
|
|
432
|
+
top: 0,
|
|
433
|
+
left: 0,
|
|
434
|
+
right: 0,
|
|
435
|
+
zIndex: 10
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
var index_default = CollapsibleTabView;
|
|
439
|
+
export {
|
|
440
|
+
TabFlatList,
|
|
441
|
+
TabScrollView,
|
|
442
|
+
index_default as default
|
|
443
|
+
};
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Animated, FlatList, ScrollView, StyleProp, ViewStyle } from "react-native";
|
|
3
|
+
export interface TabBarProps {
|
|
4
|
+
activeIndex: number;
|
|
5
|
+
onTabPress: (index: number) => void;
|
|
6
|
+
}
|
|
7
|
+
export interface CollapsibleTabViewProps {
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
renderHeader: () => React.ReactNode;
|
|
10
|
+
estimatedHeaderHeight?: number;
|
|
11
|
+
estimatedTabBarHeight?: number;
|
|
12
|
+
stickyEnabled?: boolean;
|
|
13
|
+
stickyTop?: number;
|
|
14
|
+
renderTabBar: (props: TabBarProps) => React.ReactNode;
|
|
15
|
+
initialTabIndex?: number;
|
|
16
|
+
onTabChange?: (index: number) => void;
|
|
17
|
+
onScroll?: (scrollY: number) => void;
|
|
18
|
+
swipeEnabled?: boolean;
|
|
19
|
+
style?: StyleProp<ViewStyle>;
|
|
20
|
+
}
|
|
21
|
+
export interface CollapsibleTabViewRef {
|
|
22
|
+
scrollToTab: (index: number, animated?: boolean) => void;
|
|
23
|
+
getActiveIndex: () => number;
|
|
24
|
+
}
|
|
25
|
+
export interface CollapsibleContextValue {
|
|
26
|
+
scrollY: Animated.Value;
|
|
27
|
+
activeIndex: number;
|
|
28
|
+
stickyEnabled: boolean;
|
|
29
|
+
headerHeight: number;
|
|
30
|
+
tabBarHeight: number;
|
|
31
|
+
renderHeader?: () => React.ReactNode;
|
|
32
|
+
renderTabBar?: () => React.ReactNode;
|
|
33
|
+
registerRef: (index: number, ref: FlatList<any> | ScrollView | null) => void;
|
|
34
|
+
syncScrollY: (index: number, y: number) => void;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAEnF,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CACpC;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,YAAY,EAAE,MAAM,KAAK,CAAC,SAAS,CAAA;IACnC,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,KAAK,CAAC,SAAS,CAAA;IACrD,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACrC,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IACpC,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAA;CAC7B;AAED,MAAM,WAAW,qBAAqB;IACpC,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;IACxD,cAAc,EAAE,MAAM,MAAM,CAAA;CAC7B;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,QAAQ,CAAC,KAAK,CAAA;IACvB,WAAW,EAAE,MAAM,CAAA;IACnB,aAAa,EAAE,OAAO,CAAA;IACtB,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,MAAM,KAAK,CAAC,SAAS,CAAA;IACpC,YAAY,CAAC,EAAE,MAAM,KAAK,CAAC,SAAS,CAAA;IACpC,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,CAAC,GAAG,CAAC,GAAG,UAAU,GAAG,IAAI,KAAK,IAAI,CAAA;IAC5E,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;CAChD"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-collapsible-header-tab-view",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "不依赖 react-native-reanimated ,可吸顶的tab view",
|
|
5
|
-
"main": "
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
6
7
|
"repository": {
|
|
7
8
|
"type": "git",
|
|
8
9
|
"url": "https://github.com/MasterZuom/react-native-collapsible-header-tab-view.git"
|
|
@@ -23,11 +24,17 @@
|
|
|
23
24
|
"react-native-pager-view": "^6.0.0"
|
|
24
25
|
},
|
|
25
26
|
"devDependencies": {
|
|
27
|
+
"@types/react": "^19.2.14",
|
|
28
|
+
"@types/react-native": "^0.72.8",
|
|
26
29
|
"react": "^18.0.0",
|
|
27
30
|
"react-native": "^0.71.0",
|
|
28
31
|
"react-native-pager-view": "^6.7.0",
|
|
32
|
+
"tsup": "^8.5.1",
|
|
29
33
|
"typescript": "^5.0.0"
|
|
30
34
|
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsup src/index.tsx --format cjs,esm --clean && tsc --emitDeclarationOnly --outDir dist --skipLibCheck"
|
|
37
|
+
},
|
|
31
38
|
"files": [
|
|
32
39
|
"dist"
|
|
33
40
|
]
|
package/dist/a.txt
DELETED
|
File without changes
|
package/src/index.tsx
DELETED
|
@@ -1,357 +0,0 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
forwardRef,
|
|
3
|
-
useCallback,
|
|
4
|
-
useImperativeHandle,
|
|
5
|
-
useMemo,
|
|
6
|
-
useRef,
|
|
7
|
-
useState,
|
|
8
|
-
} from "react";
|
|
9
|
-
import {
|
|
10
|
-
Animated,
|
|
11
|
-
FlatList,
|
|
12
|
-
InteractionManager,
|
|
13
|
-
LayoutChangeEvent,
|
|
14
|
-
ScrollView,
|
|
15
|
-
StyleSheet,
|
|
16
|
-
View,
|
|
17
|
-
} from "react-native";
|
|
18
|
-
import PagerView from "react-native-pager-view";
|
|
19
|
-
|
|
20
|
-
import { CollapsibleContext, TabIndexContext } from "./context";
|
|
21
|
-
import {
|
|
22
|
-
CollapsibleContextValue,
|
|
23
|
-
CollapsibleTabViewProps,
|
|
24
|
-
CollapsibleTabViewRef,
|
|
25
|
-
TabBarProps,
|
|
26
|
-
} from "./types";
|
|
27
|
-
|
|
28
|
-
export { TabFlatList } from "./TabFlatList";
|
|
29
|
-
export { TabScrollView } from "./TabScrollView";
|
|
30
|
-
export type {
|
|
31
|
-
CollapsibleTabViewProps,
|
|
32
|
-
CollapsibleTabViewRef,
|
|
33
|
-
TabBarProps,
|
|
34
|
-
} from "./types";
|
|
35
|
-
|
|
36
|
-
const CollapsibleTabView = forwardRef<
|
|
37
|
-
CollapsibleTabViewRef,
|
|
38
|
-
CollapsibleTabViewProps
|
|
39
|
-
>(
|
|
40
|
-
(
|
|
41
|
-
{
|
|
42
|
-
children,
|
|
43
|
-
renderHeader,
|
|
44
|
-
estimatedHeaderHeight = 0,
|
|
45
|
-
estimatedTabBarHeight = 0,
|
|
46
|
-
stickyEnabled = true,
|
|
47
|
-
stickyTop = 0,
|
|
48
|
-
renderTabBar,
|
|
49
|
-
initialTabIndex = 0,
|
|
50
|
-
onTabChange,
|
|
51
|
-
onScroll: onScrollProp,
|
|
52
|
-
swipeEnabled = true,
|
|
53
|
-
style,
|
|
54
|
-
},
|
|
55
|
-
ref,
|
|
56
|
-
) => {
|
|
57
|
-
const [activeIndex, setActiveIndex] = useState(initialTabIndex);
|
|
58
|
-
const pagerRef = useRef<PagerView>(null);
|
|
59
|
-
|
|
60
|
-
const pages = useMemo(
|
|
61
|
-
() => React.Children.toArray(children).filter(React.isValidElement),
|
|
62
|
-
[children],
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
useImperativeHandle(ref, () => ({
|
|
66
|
-
scrollToTab: (index: number, animated = true) => {
|
|
67
|
-
if (index === activeIndex || index < 0 || index >= pages.length) return;
|
|
68
|
-
syncTabOnSwitch(index);
|
|
69
|
-
setActiveIndex(index);
|
|
70
|
-
if (animated) {
|
|
71
|
-
pagerRef.current?.setPage(index);
|
|
72
|
-
} else {
|
|
73
|
-
pagerRef.current?.setPageWithoutAnimation(index);
|
|
74
|
-
}
|
|
75
|
-
onTabChange?.(index);
|
|
76
|
-
},
|
|
77
|
-
getActiveIndex: () => activeIndex,
|
|
78
|
-
}));
|
|
79
|
-
|
|
80
|
-
// 解决预估高度与实际高度差的问题
|
|
81
|
-
const hasEstimate = estimatedHeaderHeight > 0;
|
|
82
|
-
const headerHeightRef = useRef(0);
|
|
83
|
-
const tabBarHeightRef = useRef(0);
|
|
84
|
-
const [layout, setLayout] = useState({
|
|
85
|
-
headerHeight: estimatedHeaderHeight,
|
|
86
|
-
tabBarHeight: estimatedTabBarHeight,
|
|
87
|
-
ready: hasEstimate,
|
|
88
|
-
});
|
|
89
|
-
const { headerHeight, tabBarHeight } = layout;
|
|
90
|
-
const visible = layout.ready;
|
|
91
|
-
|
|
92
|
-
const adjustY = useRef(new Animated.Value(0)).current;
|
|
93
|
-
|
|
94
|
-
// 真实高度与预估高度如果存在高度差,则以动画形式让UI组件补偿这个高度差
|
|
95
|
-
const tryCommitLayout = useCallback(() => {
|
|
96
|
-
const h = headerHeightRef.current;
|
|
97
|
-
const t = tabBarHeightRef.current;
|
|
98
|
-
if (h > 0 && t > 0) {
|
|
99
|
-
setLayout((prev) => {
|
|
100
|
-
if (
|
|
101
|
-
Math.abs(prev.headerHeight - h) <= 1 &&
|
|
102
|
-
Math.abs(prev.tabBarHeight - t) <= 1
|
|
103
|
-
) {
|
|
104
|
-
return prev;
|
|
105
|
-
}
|
|
106
|
-
const diff = h + t - (prev.headerHeight + prev.tabBarHeight);
|
|
107
|
-
if (prev.ready && Math.abs(diff) > 1) {
|
|
108
|
-
adjustY.setValue(-diff);
|
|
109
|
-
Animated.timing(adjustY, {
|
|
110
|
-
toValue: 0,
|
|
111
|
-
duration: 200,
|
|
112
|
-
useNativeDriver: true,
|
|
113
|
-
}).start();
|
|
114
|
-
}
|
|
115
|
-
return { headerHeight: h, tabBarHeight: t, ready: true };
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
}, []);
|
|
119
|
-
|
|
120
|
-
const handleHeaderLayout = useCallback(
|
|
121
|
-
(e: LayoutChangeEvent) => {
|
|
122
|
-
headerHeightRef.current = e.nativeEvent.layout.height;
|
|
123
|
-
tryCommitLayout();
|
|
124
|
-
},
|
|
125
|
-
[tryCommitLayout],
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
const handleTabBarLayout = useCallback(
|
|
129
|
-
(e: LayoutChangeEvent) => {
|
|
130
|
-
tabBarHeightRef.current = e.nativeEvent.layout.height;
|
|
131
|
-
tryCommitLayout();
|
|
132
|
-
},
|
|
133
|
-
[tryCommitLayout],
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
const scrollY = useRef(new Animated.Value(0)).current;
|
|
137
|
-
const tabScrollYMap = useRef(new Map<number, number>());
|
|
138
|
-
const tabRefs = useRef(
|
|
139
|
-
new Map<number, FlatList<any> | ScrollView | null>(),
|
|
140
|
-
);
|
|
141
|
-
// header浮层 在Y轴 偏移的最大距离
|
|
142
|
-
const collapseRange = stickyEnabled
|
|
143
|
-
? Math.max(headerHeight - stickyTop, 0)
|
|
144
|
-
: 0;
|
|
145
|
-
|
|
146
|
-
// header浮层 垂直偏移动画值,驱动 header + tabBar 整体上移实现折叠效果。
|
|
147
|
-
const headerTranslateY = useMemo(
|
|
148
|
-
() =>
|
|
149
|
-
collapseRange > 0
|
|
150
|
-
? scrollY.interpolate({
|
|
151
|
-
inputRange: [0, collapseRange],
|
|
152
|
-
outputRange: [0, -collapseRange],
|
|
153
|
-
extrapolate: "clamp",
|
|
154
|
-
})
|
|
155
|
-
: new Animated.Value(0),
|
|
156
|
-
[scrollY, collapseRange],
|
|
157
|
-
);
|
|
158
|
-
|
|
159
|
-
const registerRef = useCallback(
|
|
160
|
-
(index: number, ref: FlatList<any> | ScrollView | null) => {
|
|
161
|
-
if (ref) {
|
|
162
|
-
tabRefs.current.set(index, ref);
|
|
163
|
-
} else {
|
|
164
|
-
tabRefs.current.delete(index);
|
|
165
|
-
}
|
|
166
|
-
},
|
|
167
|
-
[],
|
|
168
|
-
);
|
|
169
|
-
|
|
170
|
-
// 记录每个 tab 的当前滚动位置
|
|
171
|
-
const syncScrollY = useCallback(
|
|
172
|
-
(index: number, y: number) => {
|
|
173
|
-
tabScrollYMap.current.set(index, y);
|
|
174
|
-
onScrollProp?.(y);
|
|
175
|
-
},
|
|
176
|
-
[onScrollProp],
|
|
177
|
-
);
|
|
178
|
-
|
|
179
|
-
// 切换 tab 时,把新 tab 的 FlatList/ScrollView 滚动到计算好的 targetY,确保 header 吸顶状态和列表位置一致。
|
|
180
|
-
const scrollTabTo = useCallback((index: number, offset: number) => {
|
|
181
|
-
const ref: any = tabRefs.current.get(index);
|
|
182
|
-
if (!ref) return;
|
|
183
|
-
if (ref.scrollToOffset) {
|
|
184
|
-
ref.scrollToOffset({ offset, animated: false });
|
|
185
|
-
} else if (ref.scrollTo) {
|
|
186
|
-
ref.scrollTo({ y: offset, animated: false });
|
|
187
|
-
} else if (ref.getNode) {
|
|
188
|
-
const node = ref.getNode();
|
|
189
|
-
if (node?.scrollToOffset) {
|
|
190
|
-
node.scrollToOffset({ offset, animated: false });
|
|
191
|
-
} else if (node?.scrollTo) {
|
|
192
|
-
node.scrollTo({ y: offset, animated: false });
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}, []);
|
|
196
|
-
|
|
197
|
-
//核心代码: 切换Tab时 计算页面滚动的位置
|
|
198
|
-
const syncTabOnSwitch = useCallback(
|
|
199
|
-
(newIndex: number) => {
|
|
200
|
-
// 读取当前 Tab 和目标 Tab 的滚动位置
|
|
201
|
-
const currentY = tabScrollYMap.current.get(activeIndex) ?? 0;
|
|
202
|
-
const newTabSavedY = tabScrollYMap.current.get(newIndex) ?? 0;
|
|
203
|
-
|
|
204
|
-
// 判断当前 Tab 的头部是否已折叠
|
|
205
|
-
const isCollapsed = currentY >= collapseRange - 1;
|
|
206
|
-
|
|
207
|
-
// 计算目标 Tab 应该滚动到的位置
|
|
208
|
-
let targetY: number;
|
|
209
|
-
if (isCollapsed) {
|
|
210
|
-
targetY = Math.max(newTabSavedY, collapseRange);
|
|
211
|
-
} else {
|
|
212
|
-
targetY = Math.min(currentY, collapseRange);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// 在交互完成后执行滚动并更新状态
|
|
216
|
-
// runAfterInteractions 会等待所有正在进行的动画和触摸交互完成后再执行回调,确保:
|
|
217
|
-
// 1. 新 Tab 页已经渲染就绪,scrollTo 能生效
|
|
218
|
-
// 2. 不与切换动画争抢帧,体验更流畅
|
|
219
|
-
InteractionManager.runAfterInteractions(() => {
|
|
220
|
-
scrollTabTo(newIndex, targetY);
|
|
221
|
-
tabScrollYMap.current.set(newIndex, targetY);
|
|
222
|
-
scrollY.setValue(Math.min(targetY, collapseRange));
|
|
223
|
-
});
|
|
224
|
-
},
|
|
225
|
-
[activeIndex, collapseRange, scrollTabTo, scrollY],
|
|
226
|
-
);
|
|
227
|
-
|
|
228
|
-
const handleTabPress = useCallback(
|
|
229
|
-
(index: number) => {
|
|
230
|
-
if (index === activeIndex) return;
|
|
231
|
-
syncTabOnSwitch(index);
|
|
232
|
-
setActiveIndex(index);
|
|
233
|
-
pagerRef.current?.setPageWithoutAnimation(index);
|
|
234
|
-
onTabChange?.(index);
|
|
235
|
-
},
|
|
236
|
-
[activeIndex, onTabChange, syncTabOnSwitch],
|
|
237
|
-
);
|
|
238
|
-
|
|
239
|
-
const handlePageSelected = useCallback(
|
|
240
|
-
(e: { nativeEvent: { position: number } }) => {
|
|
241
|
-
const newIndex = e.nativeEvent.position;
|
|
242
|
-
if (newIndex === activeIndex) return;
|
|
243
|
-
syncTabOnSwitch(newIndex);
|
|
244
|
-
setActiveIndex(newIndex);
|
|
245
|
-
onTabChange?.(newIndex);
|
|
246
|
-
},
|
|
247
|
-
[activeIndex, onTabChange, syncTabOnSwitch],
|
|
248
|
-
);
|
|
249
|
-
|
|
250
|
-
const tabBarProps: TabBarProps = useMemo(
|
|
251
|
-
() => ({
|
|
252
|
-
activeIndex,
|
|
253
|
-
onTabPress: handleTabPress,
|
|
254
|
-
}),
|
|
255
|
-
[activeIndex, handleTabPress],
|
|
256
|
-
);
|
|
257
|
-
|
|
258
|
-
const renderTabBarNode = useCallback(
|
|
259
|
-
() => renderTabBar(tabBarProps),
|
|
260
|
-
[renderTabBar, tabBarProps],
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
const contextValue: CollapsibleContextValue = useMemo(
|
|
264
|
-
() => ({
|
|
265
|
-
scrollY,
|
|
266
|
-
activeIndex,
|
|
267
|
-
stickyEnabled,
|
|
268
|
-
headerHeight,
|
|
269
|
-
tabBarHeight,
|
|
270
|
-
renderHeader: stickyEnabled ? undefined : renderHeader,
|
|
271
|
-
renderTabBar: stickyEnabled ? undefined : renderTabBarNode,
|
|
272
|
-
registerRef,
|
|
273
|
-
syncScrollY,
|
|
274
|
-
}),
|
|
275
|
-
[
|
|
276
|
-
scrollY,
|
|
277
|
-
activeIndex,
|
|
278
|
-
stickyEnabled,
|
|
279
|
-
headerHeight,
|
|
280
|
-
tabBarHeight,
|
|
281
|
-
renderHeader,
|
|
282
|
-
renderTabBarNode,
|
|
283
|
-
registerRef,
|
|
284
|
-
syncScrollY,
|
|
285
|
-
],
|
|
286
|
-
);
|
|
287
|
-
|
|
288
|
-
return (
|
|
289
|
-
<CollapsibleContext.Provider value={contextValue}>
|
|
290
|
-
<View style={[styles.container, style]}>
|
|
291
|
-
<Animated.View
|
|
292
|
-
style={[
|
|
293
|
-
styles.pager,
|
|
294
|
-
!visible && styles.hidden,
|
|
295
|
-
{ transform: [{ translateY: adjustY }] },
|
|
296
|
-
]}
|
|
297
|
-
>
|
|
298
|
-
<PagerView
|
|
299
|
-
ref={pagerRef}
|
|
300
|
-
style={styles.pager}
|
|
301
|
-
initialPage={initialTabIndex}
|
|
302
|
-
onPageSelected={handlePageSelected}
|
|
303
|
-
scrollEnabled={swipeEnabled}
|
|
304
|
-
>
|
|
305
|
-
{pages.map((page: React.ReactElement, i) => (
|
|
306
|
-
<View key={page?.key ?? i} style={styles.page}>
|
|
307
|
-
<TabIndexContext.Provider value={i}>
|
|
308
|
-
{page}
|
|
309
|
-
</TabIndexContext.Provider>
|
|
310
|
-
</View>
|
|
311
|
-
))}
|
|
312
|
-
</PagerView>
|
|
313
|
-
</Animated.View>
|
|
314
|
-
|
|
315
|
-
{stickyEnabled && (
|
|
316
|
-
<Animated.View
|
|
317
|
-
style={[
|
|
318
|
-
styles.overlay,
|
|
319
|
-
{ transform: [{ translateY: headerTranslateY }] },
|
|
320
|
-
]}
|
|
321
|
-
pointerEvents="box-none"
|
|
322
|
-
>
|
|
323
|
-
<View pointerEvents="box-none" onLayout={handleHeaderLayout}>
|
|
324
|
-
{renderHeader()}
|
|
325
|
-
</View>
|
|
326
|
-
<View onLayout={handleTabBarLayout}>{renderTabBarNode()}</View>
|
|
327
|
-
</Animated.View>
|
|
328
|
-
)}
|
|
329
|
-
</View>
|
|
330
|
-
</CollapsibleContext.Provider>
|
|
331
|
-
);
|
|
332
|
-
},
|
|
333
|
-
);
|
|
334
|
-
|
|
335
|
-
const styles = StyleSheet.create({
|
|
336
|
-
container: {
|
|
337
|
-
flex: 1,
|
|
338
|
-
},
|
|
339
|
-
pager: {
|
|
340
|
-
flex: 1,
|
|
341
|
-
},
|
|
342
|
-
hidden: {
|
|
343
|
-
opacity: 0,
|
|
344
|
-
},
|
|
345
|
-
page: {
|
|
346
|
-
flex: 1,
|
|
347
|
-
},
|
|
348
|
-
overlay: {
|
|
349
|
-
position: "absolute",
|
|
350
|
-
top: 0,
|
|
351
|
-
left: 0,
|
|
352
|
-
right: 0,
|
|
353
|
-
zIndex: 10,
|
|
354
|
-
},
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
export default CollapsibleTabView;
|