react-native-collapsible-header-tab-view 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +105 -0
- package/dist/a.txt +0 -0
- package/package.json +34 -0
- package/src/index.tsx +357 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 MasterZuom
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# react-native-collapsible-header-tab-view
|
|
2
|
+
|
|
3
|
+
> 不依赖 react-native-reanimated,可吸顶的 tab view 组件
|
|
4
|
+
|
|
5
|
+
[](https://github.com/MasterZuom/react-native-collapsible-header-tab-view/blob/master/LICENSE)
|
|
6
|
+
|
|
7
|
+
## 特性
|
|
8
|
+
|
|
9
|
+
✨ **轻量级** - 不依赖 react-native-reanimated
|
|
10
|
+
🎯 **高性能** - 优化的滚动性能
|
|
11
|
+
📱 **吸顶效果** - 完整的粘性头部和标签栏支持
|
|
12
|
+
🔄 **灵活** - 支持 FlatList 和 ScrollView
|
|
13
|
+
⚡ **TypeScript** - 完整的类型支持
|
|
14
|
+
|
|
15
|
+
## 安装
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install react-native-collapsible-header-tab-view
|
|
19
|
+
# or
|
|
20
|
+
yarn add react-native-collapsible-header-tab-view
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### 依赖
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install react-native-pager-view
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 使用
|
|
30
|
+
|
|
31
|
+
### 基础示例
|
|
32
|
+
|
|
33
|
+
```jsx
|
|
34
|
+
import { CollapsibleTabView, TabFlatList } from 'react-native-collapsible-header-tab-view';
|
|
35
|
+
|
|
36
|
+
export function App() {
|
|
37
|
+
return (
|
|
38
|
+
<CollapsibleTabView
|
|
39
|
+
renderHeader={() => <HeaderComponent />}
|
|
40
|
+
renderTabBar={(props) => <TabBarComponent {...props} />}
|
|
41
|
+
>
|
|
42
|
+
<TabFlatList
|
|
43
|
+
index={0}
|
|
44
|
+
data={data1}
|
|
45
|
+
renderItem={({ item }) => <ItemComponent item={item} />}
|
|
46
|
+
keyExtractor={(item) => item.id}
|
|
47
|
+
/>
|
|
48
|
+
<TabFlatList
|
|
49
|
+
index={1}
|
|
50
|
+
data={data2}
|
|
51
|
+
renderItem={({ item }) => <ItemComponent item={item} />}
|
|
52
|
+
keyExtractor={(item) => item.id}
|
|
53
|
+
/>
|
|
54
|
+
</CollapsibleTabView>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Props
|
|
60
|
+
|
|
61
|
+
### CollapsibleTabView
|
|
62
|
+
|
|
63
|
+
| 属性 | 类型 | 默认值 | 说明 |
|
|
64
|
+
|------|------|--------|------|
|
|
65
|
+
| renderHeader | `() => ReactNode` | 必需 | 头部组件 |
|
|
66
|
+
| renderTabBar | `(props: TabBarProps) => ReactNode` | 必需 | 标签栏组件 |
|
|
67
|
+
| estimatedHeaderHeight | `number` | 0 | 预估头部高度 |
|
|
68
|
+
| estimatedTabBarHeight | `number` | 0 | 预估标签栏高度 |
|
|
69
|
+
| stickyEnabled | `boolean` | true | 是否启用吸顶效果 |
|
|
70
|
+
| stickyTop | `number` | 0 | 吸顶距离顶部的距离 |
|
|
71
|
+
| swipeEnabled | `boolean` | true | 是否启用滑动切换 |
|
|
72
|
+
| initialTabIndex | `number` | 0 | 初始标签页索引 |
|
|
73
|
+
| onTabChange | `(index: number) => void` | - | 标签页切换回调 |
|
|
74
|
+
| onScroll | `(scrollY: number) => void` | - | 滚动回调 |
|
|
75
|
+
| style | `StyleProp<ViewStyle>` | - | 容器样式 |
|
|
76
|
+
|
|
77
|
+
### TabFlatList
|
|
78
|
+
|
|
79
|
+
| 属性 | 类型 | 说明 |
|
|
80
|
+
|------|------|------|
|
|
81
|
+
| index | `number` | 必需,标签页索引 |
|
|
82
|
+
| data | `T[]` | 列表数据 |
|
|
83
|
+
| renderItem | `(info: { item: T, index: number }) => ReactNode` | 列表项渲染函数 |
|
|
84
|
+
| keyExtractor | `(item: T, index: number) => string` | 列表项 key |
|
|
85
|
+
|
|
86
|
+
## Ref Methods
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
interface CollapsibleTabViewRef {
|
|
90
|
+
scrollToTab(index: number, animated?: boolean): void;
|
|
91
|
+
getActiveIndex(): number;
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## 许可证
|
|
96
|
+
|
|
97
|
+
MIT
|
|
98
|
+
|
|
99
|
+
## 贡献
|
|
100
|
+
|
|
101
|
+
欢迎提交 Issue 和 Pull Request!
|
|
102
|
+
|
|
103
|
+
## 作者
|
|
104
|
+
|
|
105
|
+
- GitHub: [@MasterZuom](https://github.com/MasterZuom)
|
package/dist/a.txt
ADDED
|
File without changes
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-native-collapsible-header-tab-view",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "不依赖 react-native-reanimated ,可吸顶的tab view",
|
|
5
|
+
"main": "src/index.tsx",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/MasterZuom/react-native-collapsible-header-tab-view.git"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"react-native",
|
|
12
|
+
"collapsible",
|
|
13
|
+
"tab-view",
|
|
14
|
+
"sticky",
|
|
15
|
+
"header",
|
|
16
|
+
"pager"
|
|
17
|
+
],
|
|
18
|
+
"author": "",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"react": "*",
|
|
22
|
+
"react-native": ">=0.61.0",
|
|
23
|
+
"react-native-pager-view": "^6.0.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"react": "^18.0.0",
|
|
27
|
+
"react-native": "^0.71.0",
|
|
28
|
+
"react-native-pager-view": "^6.7.0",
|
|
29
|
+
"typescript": "^5.0.0"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist"
|
|
33
|
+
]
|
|
34
|
+
}
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
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;
|