├── .gitignore
├── .npmignore
├── .prettierrc
├── LICENSE
├── README-zh_CN.md
├── README.md
├── index.js
├── package-lock.json
├── package.json
├── snapshot
├── e18k6-3jmxk-2.gif
├── e18k6-3jmxk.gif
└── qoz8r-klpuc.gif
└── src
├── HocComponent.js
├── index.js
└── useRefreshEndReached.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 |
78 | # Next.js build output
79 | .next
80 |
81 | # Nuxt.js build / generate output
82 | .nuxt
83 | dist
84 |
85 | # Gatsby files
86 | .cache/
87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
88 | # https://nextjs.org/blog/next-9-1#public-directory-support
89 | # public
90 |
91 | # vuepress build output
92 | .vuepress/dist
93 |
94 | # Serverless directories
95 | .serverless/
96 |
97 | # FuseBox cache
98 | .fusebox/
99 |
100 | # DynamoDB Local files
101 | .dynamodb/
102 |
103 | # TernJS port file
104 | .tern-port
105 |
106 | .expo/**
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | snapshot/
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "bracketSpacing": true,
4 | "semi": true,
5 | "trailingComma": "none",
6 | "printWidth": 200,
7 | "tabWidth": 2,
8 | "jsxBracketSameLine": false,
9 | "arrowParens": "avoid",
10 | "requirePragma": false,
11 | "proseWrap": "preserve"
12 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 itenl
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.
22 |
--------------------------------------------------------------------------------
/README-zh_CN.md:
--------------------------------------------------------------------------------
1 | # react-native-scrollable-tabview
2 |
3 | [](https://www.npmjs.com/package/@itenl/react-native-scrollable-tabview)
4 |
5 | [English](./README.md) | 简体中文
6 |
7 | 基于纯 `JS` 脚本,不依赖原生,无需 `react-native link`,`Title` / `Header` / `Tabs` / `Sticky` / `Screen` 组件可灵活配置,其中 `Tabs` / `Sticky` 可在滑动到顶部时会进行吸顶;我们所支持的是以栈的形式独立管理自身的 `Sticky` / `Screen` / `Badge` / `tabLabel` 各项配置,并且为 `Screen` 注入[生命周期](#InjectionLifecycle) `onRefresh` / `onEndReached`它们将在下拉刷新与滚动条触底时触发,最后还为 `Screen` / `Sticky` 注入了更多 [props](#InjectionScreenProps)
8 |
9 | ##### Table of Contents
10 | * [Example-API](https://github.com/itenl/react-native-scrollable-tabview-example-app)
11 | * [Example-TikTok](https://github.com/itenl/react-native-scrollable-tabview-example-tiktok)
12 | * [Features](#features)
13 | * [Installation](#installation)
14 | * [Usage](#usage)
15 | * [Props](#props)
16 | * [Method](#method)
17 | * [Stack Property](#StackProperty)
18 | * [Badge Property](#BadgeProperty)
19 | * [Injection lifecycle to Screen](#InjectionLifecycle)
20 | * [Injection props to Screen](#InjectionScreenProps)
21 | * [Injection props to Sticky](#InjectionStickyProps)
22 | * [Known Issues](#KnownIssues)
23 | * [Snapshot](#Snapshot)
24 |
25 | ## Features
26 | * 支持为每个 `Screen` 设置下拉刷新与上滑加载更多(生命周期注入或属性注入)
27 | * Tabs 支持固定自适应与水平滚动两种配置方式
28 | * 允许为每个 `Screen` 独立配置 `Sticky` 组件
29 | * 允许为每个 `Tab` 独立配置自定义的徽章
30 | * 支持下拉刷新与上滑加载更多前置函数 `onBeforeRefresh` / `onBeforeEndReached`
31 | * 支持动画标题,可支持动画为 `interpolate.opacity` 与 `interpolate.height`
32 |
33 | ## Installation
34 |
35 | ```shell
36 | npm i @itenl/react-native-scrollable-tabview
37 | ```
38 | or
39 | ```shell
40 | yarn add @itenl/react-native-scrollable-tabview
41 | ```
42 |
43 | ## Usage
44 |
45 | ```jsx
46 | import React from 'react';
47 | import ScrollableTabView from '@itenl/react-native-scrollable-tabview';
48 |
49 | function App() {
50 | return (
51 | (this.scrollableTabView = rf)}
53 | mappingProps={{
54 | fromRootEst: this.state.est,
55 | }}
56 | badges={[
57 | null,
58 | [
59 |
67 | new
68 | ,
69 |
83 | Three Tips
84 | ,
85 | ],
86 | ]}
87 | stacks={[
88 | {
89 | screen: One,
90 | sticky: Sticky,
91 | tabLabel: 'OneTab',
92 | tabLabelRender: tabLabel => {
93 | return `--- ${tabLabel} ---`;
94 | },
95 | badge: [one, two],
96 | toProps: {
97 | xx: 123,
98 | },
99 | }, {
100 | screen: ({
101 | layoutHeight,
102 | refresh,
103 | scrollTo,
104 | toTabView,
105 | initScreen,
106 | onRefresh,
107 | onEndReached,
108 | }) => {
109 | // The code is required
110 | initScreen();
111 | const [datetime, setDatetime] = useState(Date.now());
112 | useEffect(() => {
113 | setInterval(() => {
114 | setDatetime(Date.now());
115 | }, 1000);
116 | }, []);
117 | onRefresh((toggled) => {
118 | toggled(true);
119 | alert("onRefresh start");
120 | setTimeout(() => {
121 | toggled(false);
122 | alert("onRefresh stop");
123 | }, 3000);
124 | });
125 | onEndReached(() => {
126 | alert("onEndReached");
127 | });
128 | return (
129 |
137 |
138 | Test function component {datetime}
139 |
140 |
141 | );
142 | },
143 | tabLabel: "TestFunctionComponent",
144 | }
145 | ]}
146 | tabsStyle={{}}
147 | tabWrapStyle={{}}
148 | tabInnerStyle={{}}
149 | tabActiveOpacity={0.6}
150 | tabStyle={{}}
151 | textStyle={{}}
152 | textActiveStyle={{}}
153 | tabUnderlineStyle={{}}
154 | firstIndex={0}
155 | syncToSticky={true}
156 | onEndReachedThreshold={0.1}
157 | onBeforeRefresh={(next, toggled) => {
158 | toggled();
159 | next();
160 | }}
161 | onBeforeEndReached={next => {
162 | next();
163 | }}
164 | onTabviewChanged={(index, tabLabel, isFirst) => {
165 | alert(index);
166 | }}
167 | header={() => {
168 | return ;
169 | }}
170 | oneTabHidden={true}
171 | enableCachePage={true}
172 | carouselProps={{}}
173 | sectionListProps={{}}
174 | toHeaderOnTab={true}
175 | toTabsOnTab={true}
176 | tabsShown={false}
177 | fixedTabs={false}
178 | fixedHeader={false}
179 | useScroll={false}
180 | fillScreen={true}
181 | >
182 | );
183 | }
184 | ```
185 |
186 | ## Props
187 |
188 | All props are optional
189 |
190 | Prop | Type | Default | Description
191 | ----------------- | -------- | ----------- | -----------
192 | **`stacks`** | Array | [] | 页面栈 < [阅读 Stack Property](#StackProperty) >
193 | **`mappingProps`** | Object | {} | 关联映射数据到 Stack / Sticky
194 | **`badges`** | Array | [] | 针对每个Tab的徽章 < [阅读 Badge Property](#BadgeProperty) >
195 | **`tabsStyle`** | Object | {} | 整个Tabs样式
196 | **`tabWrapStyle`** | Object / Function | {} | 单个Tab外包装样式 (函数参数提供了item, index, 需要返回样式对象,eg. **`return index == 1 && { zIndex : 10}`**)
197 | **`tabInnerStyle`** | Object | {} | 单个Tab内包装样式
198 | **`tabActiveOpacity`** | Number | 0.6 | Tab按钮点击后透明度
199 | **`tabStyle`** | Object | {} | 单个Tab样式
200 | **`textStyle`** | Object | {} | Tab内文本样式
201 | **`textActiveStyle`** | Object | {} | 选中激活的text样式
202 | **`tabUnderlineStyle`** | Object | {} | 选中激活的下划线样式
203 | **`firstIndex`** | Number / Null | null | 设置 **`firstIndex`** 的栈为活动状态 (请在设定 **`firstIndex`** 值的时候确保 **`stacks`** 的个数大于 **`firstIndex`** )
204 | **`syncToSticky`** | Boolean | true | 是否同步(Screen中发生 **`render`** 触发 **`componentDidUpdate`** 将更新Sticky)
205 | **`onEndReachedThreshold`** | Number | 0.2 | 触底回调阈值
206 | **`onBeforeRefresh`** | Function | null | 下拉刷新前置函数, 执行 **`next`** 将执行Screen中 **`onRefresh`** 函数,执行 **`toggled`** 将切换系统loading,可传 true / false 进行指定 (回调含有 **`next`** , **`toggled`** 两个形参)
207 | **`onBeforeEndReached`** | Function | null | 上滑加载更多前置函数, 执行next将执行Screen中 **`onEndReached`** 函数 (回调含有 **`next`** 形参)
208 | **`onTabviewChanged`** | Function | null | Tab切换完成回调 (回调含有 **`index`**, **`tabLabel`**, **`isFirst`** 形参)
209 | **`screenScrollThrottle`** | Number | 60 | **`Screen`** 横向滑动时节流参数,单位 (毫秒)
210 | **`header`** | Function / JSX Element / Class Component | null | 顶部组件 (若是函数需要返回 Element)
211 | **`stickyHeader`** | Function / JSX Element / Class Component | null | 顶部带吸顶效果组件 (若是函数需要返回 Element)
212 | **`oneTabHidden`** | Boolean | false | 仅一个Tab时将隐藏自身
213 | **`enableCachePage`** | Boolean | true | 是否持久化页面切换后不销毁
214 | **`carouselProps`** | Object | {} | 传递给 Carousel 的剩余属性 < [阅读 Carousel](https://github.com/meliorence/react-native-snap-carousel/blob/master/doc/PROPS_METHODS_AND_GETTERS.md) >
215 | **`sectionListProps`** | Object | {} | 传递给 SectionList 的剩余属性 < [阅读 SectionList](https://reactnative.dev/docs/sectionlist) >
216 | **`toHeaderOnTab`** | Boolean | false | 点击触发已激活的Tab将回到Header(高优先级)
217 | **`toTabsOnTab`** | Boolean | false | 点击触发已激活的Tab将回到Tabs
218 | **`tabsShown`** | Boolean | true | 配置 Tabs 显示隐藏
219 | **`fixedTabs`** | Boolean | false | 在 **`enableCachePage`** 为true的情况下滑动切换Screen设置最小高度保障Header与Tabs不会弹跳
220 | **`fixedHeader`** | Boolean | false | 与Tabs一同渲染,固定顶部Header,不跟随滚动
221 | **`useScroll`** | Boolean | false | Tabs是否支持横向滚动(存在多个类目Tab的情况需要启用,建议 **`tabStyle`** 传入固定宽度)
222 | **`useScrollStyle`** | Object | {} | 为滚动的 **`Tabs`** 设置 **`contentContainerStyle`**,常见为左右两侧添加边距 **`paddingLeft`** **`paddingHorizontal`**
223 | **`fillScreen`** | Boolean | true | 填充整个 Screen
224 | **`title`** | Function / JSX Element / Class Component | null | 动画标题
225 | **`titleArgs`** | Object | **`{ style: {}, interpolateOpacity: {}, interpolateHeight: {} }`** | 标题配置 < [阅读 interpolate](https://reactnative.dev/docs/animations#interpolation) >
226 | **`onScroll`** | Function | null | 滚动事件监听
227 | **`onScroll2Horizontal`** | Function | null | 滚动事件监听(横向)
228 | **`tabsEnableAnimated`** | Boolean | false | 为Tabs启用滑动效果,需要为 **`tabStyle`** 指定 **`width`**
229 | **`tabsEnableAnimatedUnderlineWidth`** | Number | 0 | 为Tabs Underline设定固定宽度并添加弹跳动画,需要启用 **`tabsEnableAnimated=true`**.( 建议传入 **`tabStyle.width`** 的三分之一或固定 30px )
230 | **`errorToThrow`** | Boolean | false | **`console.error`** 将会抛出错误 **`throw new Error()`**
231 |
232 |
233 | ## Method
234 |
235 | ``` javascript
236 | (this.scrollableTabView = rf)}
238 | >
239 |
240 | this.scrollableTabView.getCurrentRef();
241 | this.scrollableTabView.toTabView(1);
242 | this.scrollableTabView.scrollTo(0);
243 | this.scrollableTabView.clearStacks(()=>alert('done'));
244 |
245 | ```
246 |
247 | Name | Type | Description
248 | ----------------- | -------- | -----------
249 | **`getCurrentRef(index: number.optional)`** | Function | 获取当前活动的视图的实例,可传 **`index`** 获取指定实例
250 | **`toTabView(index: number.required / label: string.required)`** | Function | 跳到指定 Screen
251 | **`scrollTo(index: number.required)`** | Function | 上下滑动至指定位置 (传入 0 默认定位至 tabs / 传入负数则置顶)
252 | **`clearStacks(callback: function.optional)`** | Function | 清空栈以及相关状态 (Tabs / Badge / Stacks))
253 |
254 | ## Stack Property
255 |
256 | Name | Type | Description
257 | ----------------- | -------- | -----------
258 | **`screen`** | Class Component | Screen 类组件
259 | **`sticky`** | Class Component | 吸顶类组件, 实例内将返回该类组件的上下文
260 | **`tabLabel`** | String | Tab 昵称
261 | **`tabLabelRender`** | Function | 自定义 Tab渲染函数,优先级高于 **`tabLabel`**
262 | **`badge`** | Array | 针对当前 Tab 的徽章,与 **`badges`** 属性互斥,优先级高于最外层属性 **`badges`** < [阅读 Badge Property](#BadgeProperty) >
263 | **`toProps`** | Object | **`toProps`** 仅传递给 Screen,不作数据关联
264 |
265 | ## Badge Property
266 |
267 | Type | Description
268 | -------- | -----------
269 | JSX Element | 基于当前Tab进行渲染的 徽章 / 悬浮 Tips 等
270 |
271 |
272 | ## Injection lifecycle to Screen(仅在类组件)
273 |
274 | Name | Type | Description
275 | ----------------- | -------- | -----------
276 | **`onRefresh`** | Function | 下拉刷新时触发,形参 **`toggled`** 函数用于切换原生 loading 状态的显隐,若在 loading 中用户切换 tab 将会强制隐藏并重置状态
277 | **`onEndReached`** | Function | 上滑加载更多触发
278 |
279 |
280 | ## Injection props to Screen
281 |
282 | Name | Type | Description
283 | ----------------- | -------- | -----------
284 | **`refresh()`** | Function | 手动触发刷新、同步Screen状态至Sticky
285 | **`scrollTo(index: number.required)`** | Function | 上下滑动至指定位置 (传入 0 默认定位至 tabs / 传入负数则置顶)
286 | **`toTabView(index: number.required / label: string.required)`** | Function | 跳到指定 Screen
287 | **`layoutHeight.container`** | Number | Container 容器总高度
288 | **`layoutHeight.header`** | Number | Header 高度
289 | **`layoutHeight.tabs`** | Number | Tabs 高度
290 | **`layoutHeight.screen`** | Number | 视图 高度
291 | **`initScreen`** | Function | (仅在函数组件) 如果是函数组件则必须调用
292 | **`onRefresh`** | Function | (仅在函数组件) < [阅读 onRefresh 描述](#InjectionLifecycle) >
293 | **`onEndReached`** | Function | (仅在函数组件) < [阅读 onEndReached 描述](#InjectionLifecycle) >
294 |
295 | ## Injection props to Sticky
296 |
297 | Name | Type | Description
298 | ----------------- | -------- | -----------
299 | **`screenContext`** | Object | 获取 Screen 上下文
300 |
301 | ## Known Issues
302 | - 如果你仅仅是新增 `Stack` 可以 `Push` 即可,但如果需要更新或者删除 `Stack` 请使用 [clearStacks](#Method) 后在添加你所需要的 `Stacks`
303 |
304 | ## Snapshot
305 |
306 | ### Android (Sliding Tabs)
307 |
308 |
309 | ### iOS (Bounce Tabs)
310 |
311 |
312 | ### API Example
313 |
314 |
315 |
316 | ---
317 |
318 | **MIT Licensed**
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-native-scrollable-tabview
2 |
3 | [](https://www.npmjs.com/package/@itenl/react-native-scrollable-tabview)
4 |
5 | English | [简体中文](./README-zh_CN.md)
6 |
7 | Based on pure `JS` scripts, without relying on native, no need for `react-native link`,`Title` / `Header` / `Tabs` / `Sticky` / `Screen` components can be flexibly configured, among which `Tabs` / `Sticky` can slide When it reaches the top, it will be topped; what we support is to independently manage its own `Sticky` / `Screen` / `Badge` / `tabLabel` configuration in the form of a stack, and inject the `Screen` [lifecycle](#InjectionLifecycle) `onRefresh` / `onEndReached` They will be triggered when the pull-down refresh and the scroll bar hit the bottom, and finally inject more into `Screen` / `Sticky` [props](#InjectionScreenProps)
8 |
9 | ##### Table of Contents
10 | * [Example-API](https://github.com/itenl/react-native-scrollable-tabview-example-app)
11 | * [Example-TikTok](https://github.com/itenl/react-native-scrollable-tabview-example-tiktok)
12 | * [Features](#features)
13 | * [Installation](#installation)
14 | * [Usage](#usage)
15 | * [Props](#props)
16 | * [Method](#method)
17 | * [Stack Property](#StackProperty)
18 | * [Badge Property](#BadgeProperty)
19 | * [Injection lifecycle to Screen](#InjectionLifecycle)
20 | * [Injection props to Screen](#InjectionScreenProps)
21 | * [Injection props to Sticky](#InjectionStickyProps)
22 | * [Known Issues](#KnownIssues)
23 | * [Snapshot](#Snapshot)
24 |
25 | ## Features
26 | * Support to individually set pull-down refresh and up-slide load for each screen (Lifecycle injection or props injection)
27 | * Flex Tabs and multiple Tabs horizontal scrolling support configuration method
28 | * Allow to set up each Screen’s own Sticky component
29 | * Custom badges can be configured for each Tab
30 | * Support pull down to refresh and slide up to load more pre-functions `onBeforeRefresh` / `onBeforeEndReached`
31 | * Support animation title, can support animation as `interpolate.opacity` and `interpolate.height`
32 |
33 | ## Installation
34 |
35 | ```shell
36 | npm i @itenl/react-native-scrollable-tabview
37 | ```
38 | or
39 | ```shell
40 | yarn add @itenl/react-native-scrollable-tabview
41 | ```
42 |
43 | ## Usage
44 |
45 | ```jsx
46 | import React from 'react';
47 | import ScrollableTabView from '@itenl/react-native-scrollable-tabview';
48 |
49 | function App() {
50 | return (
51 | (this.scrollableTabView = rf)}
53 | mappingProps={{
54 | fromRootEst: this.state.est,
55 | }}
56 | badges={[
57 | null,
58 | [
59 |
67 | new
68 | ,
69 |
83 | Three Tips
84 | ,
85 | ],
86 | ]}
87 | stacks={[
88 | {
89 | screen: One,
90 | sticky: Sticky,
91 | tabLabel: 'OneTab',
92 | tabLabelRender: tabLabel => {
93 | return `--- ${tabLabel} ---`;
94 | },
95 | badge: [one, two],
96 | toProps: {
97 | xx: 123,
98 | },
99 | }, {
100 | screen: ({
101 | layoutHeight,
102 | refresh,
103 | scrollTo,
104 | toTabView,
105 | initScreen,
106 | onRefresh,
107 | onEndReached,
108 | }) => {
109 | // The code is required
110 | initScreen();
111 | const [datetime, setDatetime] = useState(Date.now());
112 | useEffect(() => {
113 | setInterval(() => {
114 | setDatetime(Date.now());
115 | }, 1000);
116 | }, []);
117 | onRefresh((toggled) => {
118 | toggled(true);
119 | alert("onRefresh start");
120 | setTimeout(() => {
121 | toggled(false);
122 | alert("onRefresh stop");
123 | }, 3000);
124 | });
125 | onEndReached(() => {
126 | alert("onEndReached");
127 | });
128 | return (
129 |
137 |
138 | Test function component {datetime}
139 |
140 |
141 | );
142 | },
143 | tabLabel: "TestFunctionComponent",
144 | }
145 | ]}
146 | tabsStyle={{}}
147 | tabWrapStyle={{}}
148 | tabInnerStyle={{}}
149 | tabActiveOpacity={0.6}
150 | tabStyle={{}}
151 | textStyle={{}}
152 | textActiveStyle={{}}
153 | tabUnderlineStyle={{}}
154 | firstIndex={0}
155 | syncToSticky={true}
156 | onEndReachedThreshold={0.1}
157 | onBeforeRefresh={(next, toggled) => {
158 | toggled();
159 | next();
160 | }}
161 | onBeforeEndReached={next => {
162 | next();
163 | }}
164 | onTabviewChanged={(index, tabLabel, isFirst) => {
165 | alert(index);
166 | }}
167 | header={() => {
168 | return ;
169 | }}
170 | oneTabHidden={true}
171 | enableCachePage={true}
172 | carouselProps={{}}
173 | sectionListProps={{}}
174 | toHeaderOnTab={true}
175 | toTabsOnTab={true}
176 | tabsShown={false}
177 | fixedTabs={false}
178 | fixedHeader={false}
179 | useScroll={false}
180 | fillScreen={true}
181 | >
182 | );
183 | }
184 | ```
185 |
186 | ## Props
187 |
188 | All props are optional
189 |
190 | Prop | Type | Default | Description
191 | ----------------- | -------- | ----------- | -----------
192 | **`stacks`** | Array | [] | Page Stack < [Read Stack Property](#StackProperty) >
193 | **`mappingProps`** | Object | {} | Associate mapping data to Stack / Sticky
194 | **`badges`** | Array | [] | Badges for each Tab < [Read Badge Property](#BadgeProperty) >
195 | **`tabsStyle`** | Object | {} | The entire Tabs style
196 | **`tabWrapStyle`** | Object / Function | {} | Single Tab wrap style (The function parameters provide item, index, and need to return the style object, eg. **`return index == 1 && {zIndex: 10}`**)
197 | **`tabInnerStyle`** | Object | {} | Single Tab inner style
198 | **`tabActiveOpacity`** | Number | 0.6 | Transparency after Tab button click
199 | **`tabStyle`** | Object | {} | Single Tab style
200 | **`textStyle`** | Object | {} | Text style in Tab
201 | **`textActiveStyle`** | Object | {} | Select the active text style
202 | **`tabUnderlineStyle`** | Object | {} | Select the active underline style
203 | **`firstIndex`** | Number / Null | null | Set the stack of **`firstIndex`** to active (make sure that the number of **`stacks`** is greater than to **`firstIndex`** when setting the **`firstIndex`** value)
204 | **`syncToSticky`** | Boolean | true | Whether it is synchronized (**`render`** triggered in the Screen **`componentDidUpdate`** will update Sticky)
205 | **`onEndReachedThreshold`** | Number | 0.2 | Bottom callback threshold
206 | **`onBeforeRefresh`** | Function | null | Pull down to refresh the pre-functions, execute **`next`** to execute **`onRefresh`** function in Screen, execute **`toggled`** to switch system loading, you can pass true / false to specify (callback contains **`next`**, **`toggled`** two formal parameters)
207 | **`onBeforeEndReached`** | Function | null | Slide up to load more pre-functions, execute next will execute the **`onEndReached`** function in the Screen (callback contains **`next`** formal parameters)
208 | **`onTabviewChanged`** | Function | null | Tab switch completion callback (callback contains **`index`**, **`tabLabel`**, **`isFirst`** parameters)
209 | **`screenScrollThrottle`** | Number | 60 | **`Screen`** Throttle parameters during lateral sliding, Unit (ms)
210 | **`header`** | Function / JSX Element / Class Component | null | Top component (if the function needs to return Element)
211 | **`stickyHeader`** | Function / JSX Element / Class Component | null | Top component (if the function needs to return Element) for sticky
212 | **`oneTabHidden`** | Boolean | false | Hide itself when there is only one Tab
213 | **`enableCachePage`** | Boolean | true | Whether the persistent page will not be destroyed after switching
214 | **`carouselProps`** | Object | {} | The remaining attributes passed to Carousel < [Read Carousel](https://github.com/meliorence/react-native-snap-carousel/blob/master/doc/PROPS_METHODS_AND_GETTERS.md) >
215 | **`sectionListProps`** | Object | {} | Remaining attributes passed to SectionList < [Read SectionList](https://reactnative.dev/docs/sectionlist) >
216 | **`toHeaderOnTab`** | Boolean | false | Click to trigger the activated Tab will scroll to Header (high priority)
217 | **`toTabsOnTab`** | Boolean | false | Click to trigger the activated Tab will scroll to Tabs
218 | **`tabsShown`** | Boolean | true | Configure Tabs display and hide
219 | **`fixedTabs`** | Boolean | false | When **`enableCachePage`** is true, slide to switch Screen to set the minimum height to ensure that the Header and Tabs will not bounce
220 | **`fixedHeader`** | Boolean | false | Render together with Tabs, fix the top Header, do not follow the scroll
221 | **`useScroll`** | Boolean | false | Does Tabs support horizontal scrolling (it needs to be enabled when there are multiple category Tabs, it is recommended that **`tabStyle`** pass in a fixed width)
222 | **`useScrollStyle`** | Object | {} | Set **`contentContainerStyle`** for scrolling **`Tabs`**, usually add margins to the left and right sides **`paddingLeft`** **`paddingHorizontal`**
223 | **`fillScreen`** | Boolean | true | Fill the entire Screen
224 | **`title`** | Function / JSX Element / Class Component | null | Animation title
225 | **`titleArgs`** | Object | **`{ style: {}, interpolateOpacity: {}, interpolateHeight: {} }`** | Title parameter configuration < [Read interpolate](https://reactnative.dev/docs/animations#interpolation) >
226 | **`onScroll`** | Function | null | Scroll event monitoring
227 | **`onScroll2Horizontal`** | Function | null | Scroll event monitoring for orizontal
228 | **`tabsEnableAnimated`** | Boolean | false | Enable sliding effect for Tabs, Need to specify **`width`** for **`tabStyle`**
229 | **`tabsEnableAnimatedUnderlineWidth`** | Number | 0 | To set a fixed width for the Tabs Underline and add a jumping animation, you need to enable **`tabsEnableAnimated=true`**. (It is recommended to pass in one third of **`tabStyle.width`** or a fixed 30px)
230 | **`errorToThrow`** | Boolean | false | **`console.error`** will throw an error **`throw new Error()`**
231 |
232 | ## Method
233 |
234 | ``` javascript
235 | (this.scrollableTabView = rf)}
237 | >
238 |
239 | this.scrollableTabView.getCurrentRef();
240 | this.scrollableTabView.toTabView(1);
241 | this.scrollableTabView.scrollTo(0);
242 | this.scrollableTabView.clearStacks(()=>alert('done'));
243 | ```
244 |
245 | Name | Type | Description
246 | ----------------- | -------- | -----------
247 | **`getCurrentRef(index: number.optional)`** | Function | Get the instance of the currently active view, you can pass **`index`** to get the specified instance
248 | **`toTabView(index: number.required / label: string.required)`** | Function | Jump to the specified Screen
249 | **`scrollTo(index: number.required)`** | Function | Swipe up and down to the specified position (passing in 0 is the default positioning to tabs / passing in a negative number is set to the top)
250 | **`clearStacks(callback: function.optional)`** | Function | Clear the Stacks and related state (Tabs / Badge / Stacks))
251 |
252 | ## Stack Property
253 |
254 | Name | Type | Description
255 | ----------------- | -------- | -----------
256 | **`screen`** | Class / Function Component | Screen components ( If the function component must call initScreen )
257 | **`sticky`** | Class Component | Sticky component, The context of this type of component will be returned in the instance
258 | **`tabLabel`** | String | Tab display name
259 | **`tabLabelRender`** | Function | Custom Tab rendering function, priority is higher than **`tabLabel`**
260 | **`badge`** | Array | For the current Tab badge, it is mutually exclusive with the **`badges`** attribute, and has a higher priority than the outermost attribute **`badges`** < [Read Badge Property](#BadgeProperty) >
261 | **`toProps`** | Object | **`toProps`** Only pass to Screen without data association
262 |
263 | ## Badge Property
264 |
265 | Type | Description
266 | -------- | -----------
267 | JSX Element | Badges/Hovering Tips, etc. rendered based on the current Tab
268 |
269 |
270 | ## Injection lifecycle to Screen (On Class Component)
271 |
272 | Name | Type | Description
273 | ----------------- | -------- | -----------
274 | **`onRefresh`** | Function | Triggered when pull-down refresh, the formal parameter **`toggled`** function is used to switch the display of the native loading state, if the user switches the tab during loading, it will be forced to hide and reset the state
275 | **`onEndReached`** | Function | Swipe up to load more triggers
276 |
277 |
278 | ## Injection props to Screen
279 |
280 | Name | Type | Description
281 | ----------------- | -------- | -----------
282 | **`refresh()`** | Function | Manually trigger refresh and synchronize Screen status to Sticky
283 | **`scrollTo(index: number.required)`** | Function | Swipe up and down to the specified position (passing in 0 is the default positioning to tabs / passing in a negative number is set to the top)
284 | **`toTabView(index: number.required / label: string.required)`** | Function | Jump to the specified Screen
285 | **`layoutHeight.container`** | Number | Total height of the Container
286 | **`layoutHeight.header`** | Number | Total height of the Header
287 | **`layoutHeight.tabs`** | Number | Total height of the Tabs
288 | **`layoutHeight.screen`** | Number | Total height of the Screen
289 | **`initScreen`** | Function | (On Function Component) If it is a function component, it must be called
290 | **`onRefresh`** | Function | (On Function Component) < [Read onRefresh description](#InjectionLifecycle) >
291 | **`onEndReached`** | Function | (On Function Component) < [Read onEndReached description](#InjectionLifecycle) >
292 |
293 | ## Injection props to Sticky
294 |
295 | Name | Type | Description
296 | ----------------- | -------- | -----------
297 | **`screenContext`** | Object | Get Screen context
298 |
299 | ## Known Issues
300 | - If you just add a `Stack`, you can `Push`, but if you need to update or delete a `Stack`, please use [clearStacks](#Method) and then add the `Stacks` you need
301 |
302 | ## Snapshot
303 |
304 | ### Android (Sliding Tabs)
305 |
306 |
307 | ### iOS (Bounce Tabs)
308 |
309 |
310 | ### API Example
311 |
312 |
313 |
314 | ---
315 |
316 | **MIT Licensed**
317 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import ScrollableTabView from './src';
2 |
3 | export default ScrollableTabView;
4 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@itenl/react-native-scrollable-tabview",
3 | "version": "1.1.2",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "@itenl/react-native-snap-carousel": {
8 | "version": "4.0.0-beta.6",
9 | "resolved": "https://registry.npmjs.org/@itenl/react-native-snap-carousel/-/react-native-snap-carousel-4.0.0-beta.6.tgz",
10 | "integrity": "sha512-w8lBAmDNBWlTkp+SqM9xpm7J1naiNr0kdtHFak6LLbMOU4g2m78Uhxd8z2YWoZ0qIxiKNIRzcQM7pK4uCiNL0A==",
11 | "requires": {
12 | "@types/react-addons-shallow-compare": "^0.14.22",
13 | "react-addons-shallow-compare": "15.6.2"
14 | }
15 | },
16 | "@types/prop-types": {
17 | "version": "15.7.4",
18 | "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz",
19 | "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ=="
20 | },
21 | "@types/react": {
22 | "version": "17.0.18",
23 | "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.18.tgz",
24 | "integrity": "sha512-YTLgu7oS5zvSqq49X5Iue5oAbVGhgPc5Au29SJC4VeE17V6gASoOxVkUDy9pXFMRFxCWCD9fLeweNFizo3UzOg==",
25 | "requires": {
26 | "@types/prop-types": "*",
27 | "@types/scheduler": "*",
28 | "csstype": "^3.0.2"
29 | }
30 | },
31 | "@types/react-addons-shallow-compare": {
32 | "version": "0.14.22",
33 | "resolved": "https://registry.npmjs.org/@types/react-addons-shallow-compare/-/react-addons-shallow-compare-0.14.22.tgz",
34 | "integrity": "sha512-krgFRorWtbVJLzpJsJD6O27Lew3YHuemVZbL9RFvq8TF1w9DbrHjiiLuIyWIL6AjunBkUrQlErfbUv1TYKiK9w==",
35 | "requires": {
36 | "@types/react": "*"
37 | }
38 | },
39 | "@types/scheduler": {
40 | "version": "0.16.2",
41 | "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
42 | "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew=="
43 | },
44 | "asap": {
45 | "version": "2.0.6",
46 | "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
47 | "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY="
48 | },
49 | "core-js": {
50 | "version": "1.2.7",
51 | "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz",
52 | "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY="
53 | },
54 | "csstype": {
55 | "version": "3.0.8",
56 | "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.8.tgz",
57 | "integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw=="
58 | },
59 | "encoding": {
60 | "version": "0.1.13",
61 | "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
62 | "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
63 | "requires": {
64 | "iconv-lite": "^0.6.2"
65 | }
66 | },
67 | "fbjs": {
68 | "version": "0.8.17",
69 | "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz",
70 | "integrity": "sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=",
71 | "requires": {
72 | "core-js": "^1.0.0",
73 | "isomorphic-fetch": "^2.1.1",
74 | "loose-envify": "^1.0.0",
75 | "object-assign": "^4.1.0",
76 | "promise": "^7.1.1",
77 | "setimmediate": "^1.0.5",
78 | "ua-parser-js": "^0.7.18"
79 | }
80 | },
81 | "iconv-lite": {
82 | "version": "0.6.3",
83 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
84 | "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
85 | "requires": {
86 | "safer-buffer": ">= 2.1.2 < 3.0.0"
87 | }
88 | },
89 | "is-stream": {
90 | "version": "1.1.0",
91 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
92 | "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
93 | },
94 | "isomorphic-fetch": {
95 | "version": "2.2.1",
96 | "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz",
97 | "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=",
98 | "requires": {
99 | "node-fetch": "^1.0.1",
100 | "whatwg-fetch": ">=0.10.0"
101 | }
102 | },
103 | "js-tokens": {
104 | "version": "4.0.0",
105 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
106 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
107 | },
108 | "lodash.throttle": {
109 | "version": "4.1.1",
110 | "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
111 | "integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ="
112 | },
113 | "loose-envify": {
114 | "version": "1.4.0",
115 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
116 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
117 | "requires": {
118 | "js-tokens": "^3.0.0 || ^4.0.0"
119 | }
120 | },
121 | "node-fetch": {
122 | "version": "1.7.3",
123 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz",
124 | "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==",
125 | "requires": {
126 | "encoding": "^0.1.11",
127 | "is-stream": "^1.0.1"
128 | }
129 | },
130 | "object-assign": {
131 | "version": "4.1.1",
132 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
133 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
134 | },
135 | "promise": {
136 | "version": "7.3.1",
137 | "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
138 | "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
139 | "requires": {
140 | "asap": "~2.0.3"
141 | }
142 | },
143 | "react-addons-shallow-compare": {
144 | "version": "15.6.2",
145 | "resolved": "https://registry.npmjs.org/react-addons-shallow-compare/-/react-addons-shallow-compare-15.6.2.tgz",
146 | "integrity": "sha1-GYoAuR/DdiPbZKKP0XtZa6NicC8=",
147 | "requires": {
148 | "fbjs": "^0.8.4",
149 | "object-assign": "^4.1.0"
150 | }
151 | },
152 | "safer-buffer": {
153 | "version": "2.1.2",
154 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
155 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
156 | },
157 | "setimmediate": {
158 | "version": "1.0.5",
159 | "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
160 | "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU="
161 | },
162 | "ua-parser-js": {
163 | "version": "0.7.28",
164 | "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.28.tgz",
165 | "integrity": "sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g=="
166 | },
167 | "whatwg-fetch": {
168 | "version": "3.6.2",
169 | "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz",
170 | "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA=="
171 | }
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@itenl/react-native-scrollable-tabview",
3 | "version": "1.1.7",
4 | "description": "react-native-scrollable-tabview",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "dependencies": {
10 | "lodash.throttle": "^4.1.1",
11 | "@itenl/react-native-snap-carousel": "^4.0.0-beta.6"
12 | },
13 | "keywords": [
14 | "react",
15 | "sticky",
16 | "tabbar",
17 | "tabview",
18 | "react-native",
19 | "scrollabletabview"
20 | ],
21 | "repository": {
22 | "type": "git",
23 | "url": "https://github.com/itenl/react-native-scrollable-tabview.git"
24 | },
25 | "author": "itenl",
26 | "license": "MIT",
27 | "homepage": "https://github.com/itenl/react-native-scrollable-tabview"
28 | }
29 |
--------------------------------------------------------------------------------
/snapshot/e18k6-3jmxk-2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itenl/react-native-scrollable-tabview/61e8f887e45bd25c0d962f24232fe992fc4951b5/snapshot/e18k6-3jmxk-2.gif
--------------------------------------------------------------------------------
/snapshot/e18k6-3jmxk.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itenl/react-native-scrollable-tabview/61e8f887e45bd25c0d962f24232fe992fc4951b5/snapshot/e18k6-3jmxk.gif
--------------------------------------------------------------------------------
/snapshot/qoz8r-klpuc.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itenl/react-native-scrollable-tabview/61e8f887e45bd25c0d962f24232fe992fc4951b5/snapshot/qoz8r-klpuc.gif
--------------------------------------------------------------------------------
/src/HocComponent.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 |
3 | export default (WrappedComponent, getRef) => {
4 | return class HocComponent extends PureComponent {
5 | static __HOCNAME__ = 'HocComponent';
6 | constructor(props) {
7 | super(props);
8 | this.__HOCNAME__ = 'HocComponent';
9 | }
10 |
11 | render() {
12 | return (
13 | {
15 | // 在使用RN release打包后在sticky获取到的screenContext为HOC上下文,需健全区分__HOCNAME__
16 | if (comp && !comp.__HOCNAME__) comp && getRef && getRef(comp);
17 | }}
18 | {...this.props}
19 | />
20 | );
21 | }
22 | };
23 | };
24 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, Text, View, SectionList, RefreshControl, TouchableOpacity, Animated, Dimensions, ScrollView } from 'react-native';
3 | import PropTypes from 'prop-types';
4 | import Carousel from '@itenl/react-native-snap-carousel';
5 | import HocComponent from './HocComponent';
6 | import _throttle from 'lodash.throttle';
7 | import packagejson from '../package.json';
8 | import { initScreen, triggerOnce, refreshMap, onRefresh, triggerRefresh, onEndReached, triggerEndReached } from './useRefreshEndReached';
9 | const deviceWidth = Dimensions.get('window').width;
10 |
11 | const AnimatedSectionList = Animated.createAnimatedComponent(SectionList);
12 | const AnimatedCarousel = Animated.createAnimatedComponent(Carousel);
13 |
14 | const CONSOLE_LEVEL = {
15 | LOG: 'log',
16 | INFO: 'info',
17 | WARN: 'warn',
18 | ERROR: 'error'
19 | };
20 |
21 | /**
22 | * @author itenl
23 | * @class ScrollableTabView
24 | * @extends {React.Component}
25 | */
26 | export default class ScrollableTabView extends React.Component {
27 | static propTypes = {
28 | stacks: PropTypes.array.isRequired,
29 | firstIndex: PropTypes.number,
30 | mappingProps: PropTypes.object,
31 | header: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
32 | stickyHeader: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
33 | badges: PropTypes.array,
34 | tabsStyle: PropTypes.object,
35 | tabWrapStyle: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
36 | tabInnerStyle: PropTypes.object,
37 | tabActiveOpacity: PropTypes.number,
38 | tabStyle: PropTypes.object,
39 | tabsEnableAnimated: PropTypes.bool,
40 | tabsEnableAnimatedUnderlineWidth: PropTypes.number,
41 | useScrollStyle: PropTypes.object,
42 | textStyle: PropTypes.object,
43 | textActiveStyle: PropTypes.object,
44 | tabUnderlineStyle: PropTypes.object,
45 | syncToSticky: PropTypes.bool,
46 | onEndReachedThreshold: PropTypes.number,
47 | onBeforeRefresh: PropTypes.func,
48 | onBeforeEndReached: PropTypes.func,
49 | onTabviewChanged: PropTypes.func,
50 | oneTabHidden: PropTypes.bool,
51 | enableCachePage: PropTypes.bool,
52 | carouselProps: PropTypes.object,
53 | sectionListProps: PropTypes.object,
54 | toHeaderOnTab: PropTypes.bool,
55 | toTabsOnTab: PropTypes.bool,
56 | tabsShown: PropTypes.bool,
57 | fixedTabs: PropTypes.bool,
58 | fixedHeader: PropTypes.bool,
59 | useScroll: PropTypes.bool,
60 | fillScreen: PropTypes.bool,
61 | title: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
62 | titleArgs: PropTypes.object,
63 | onScroll: PropTypes.func,
64 | onScroll2Horizontal: PropTypes.func,
65 | screenScrollThrottle: PropTypes.number,
66 | errorToThrow: PropTypes.bool
67 | };
68 |
69 | static defaultProps = {
70 | stacks: [],
71 | firstIndex: null,
72 | mappingProps: {},
73 | header: null,
74 | stickyHeader: null,
75 | badges: [],
76 | tabsStyle: {},
77 | tabWrapStyle: {},
78 | tabInnerStyle: {},
79 | tabActiveOpacity: 0.6,
80 | tabStyle: {},
81 | tabsEnableAnimated: false,
82 | tabsEnableAnimatedUnderlineWidth: 0,
83 | useScrollStyle: {},
84 | textStyle: {},
85 | textActiveStyle: {},
86 | tabUnderlineStyle: {},
87 | syncToSticky: true,
88 | onEndReachedThreshold: 0.2,
89 | onBeforeRefresh: null,
90 | onBeforeEndReached: null,
91 | onTabviewChanged: null,
92 | oneTabHidden: false,
93 | enableCachePage: true,
94 | carouselProps: {},
95 | sectionListProps: {},
96 | toHeaderOnTab: false,
97 | toTabsOnTab: false,
98 | tabsShown: true,
99 | fixedTabs: false,
100 | fixedHeader: false,
101 | useScroll: false,
102 | fillScreen: true,
103 | title: null,
104 | titleArgs: {
105 | style: {},
106 | interpolateOpacity: {},
107 | interpolateHeight: {}
108 | },
109 | onScroll: null,
110 | onScroll2Horizontal: null,
111 | screenScrollThrottle: 60,
112 | errorToThrow: false
113 | };
114 |
115 | constructor(props) {
116 | super(props);
117 | this.state = {
118 | ...this._initialState()
119 | };
120 | this._initialProperty();
121 | this._initial();
122 | }
123 |
124 | UNSAFE_componentWillReceiveProps(newProps) {
125 | this._initial(newProps, true);
126 | }
127 |
128 | _initialState() {
129 | return {
130 | checkedIndex: this._getFirstIndex(),
131 | refsObj: {},
132 | lazyIndexs: this._initLazyIndexs(),
133 | isRefreshing: false
134 | };
135 | }
136 |
137 | _initialProperty() {
138 | const { screenScrollThrottle } = this.props;
139 | this.scroll2VerticalPos = new Animated.Value(0);
140 | this.scroll2HorizontalPos = new Animated.Value(0);
141 | this.tabsMeasurements = [];
142 | this.tabWidth = 0;
143 | this.tabWidthWrap = 0;
144 | this.layoutHeight = {
145 | container: 0,
146 | header: 0,
147 | stickyHeader: 0,
148 | tabs: 0,
149 | screen: 0
150 | };
151 | this.titleInterpolateArgs = {
152 | height: {
153 | inputRange: [0, 160],
154 | outputRange: [0, 80],
155 | extrapolate: 'clamp'
156 | },
157 | opacity: {
158 | inputRange: [160, 320],
159 | outputRange: [0.2, 1],
160 | extrapolate: 'clamp'
161 | }
162 | };
163 | this.tabUnderlineInterpolateArgs = {
164 | inputRange: [],
165 | outputRange: [],
166 | extrapolate: 'clamp'
167 | };
168 | this._throttleCallback = _throttle(this._onTabviewChange.bind(this, true), screenScrollThrottle, {
169 | leading: false,
170 | trailing: true
171 | });
172 | this._renderSectionHeader = this._renderSectionHeader.bind(this);
173 | this._onRefresh = this._onRefresh.bind(this);
174 | this._onEndReached = this._onEndReached.bind(this);
175 | this._renderItem = this._renderItem.bind(this);
176 | this._toggledRefreshing = this._toggledRefreshing.bind(this);
177 | this._onScroll2Vertical = this._onScroll2Vertical.bind(this);
178 | this._onScroll2Horizontal = this._onScroll2Horizontal.bind(this);
179 | this._setScrollHandler2Vertical();
180 | this._setScrollHandler2Horizontal();
181 | }
182 |
183 | _initial(props = this.props, isProcess = false) {
184 | isProcess && this._toProcess(props);
185 | this.tabs = this._getTabs(props);
186 | this.badges = this._getBadges(props);
187 | this.stacks = this._getWrapChildren(props);
188 | if (props.firstIndex > Math.max(this.stacks.length - 1, 0)) this._displayConsole('firstIndex cannot exceed the total number of stacks.length', CONSOLE_LEVEL.ERROR);
189 | }
190 |
191 | /**
192 | * 避免reset栈时的默认 firstIndex 超出当前选中索引导致无法显示视图
193 | * @param {*} props
194 | * @memberof ScrollableTabView
195 | */
196 | _toProcess(props) {
197 | if (props.stacks && props.stacks.length && props.stacks.length != this.stacks.length && props.firstIndex != this.state.checkedIndex) {
198 | const timer = setTimeout(() => {
199 | this._onTabviewChange(false, props.firstIndex);
200 | clearTimeout(timer);
201 | });
202 | }
203 | }
204 |
205 | _getTabs(props) {
206 | return (
207 | props.stacks &&
208 | props.stacks.map((item, index) => {
209 | return {
210 | tabLabel: item.tabLabel || item.screen?.name,
211 | tabLabelRender: item.tabLabelRender ?? null,
212 | index
213 | };
214 | })
215 | );
216 | }
217 |
218 | _getBadges(props) {
219 | return (
220 | props.stacks &&
221 | props.stacks.map(item => {
222 | return item.badge ?? null;
223 | })
224 | );
225 | }
226 |
227 | _makeStacksID(item) {
228 | if (item && !item.__id__) item.__id__ = `${Math.random().toString().slice(2, 8)}_${Date.now().toString().slice(2, 8)}`;
229 | }
230 |
231 | _getWrapChildren(props) {
232 | return (
233 | props.stacks &&
234 | props.stacks.map((item, index) => {
235 | if (item.screen) {
236 | if (this.isClassCompoent(item.screen) && !item.screen.__HOCNAME__) {
237 | this._makeStacksID(item);
238 | item.screen = HocComponent(item.screen, this._setCurrentRef(index, item.__id__));
239 | } else {
240 | this._makeStacksID(item);
241 | triggerOnce(item.screen, this._setCurrentRef(index, item.__id__));
242 | }
243 | }
244 | return item;
245 | })
246 | );
247 | }
248 |
249 | _setCurrentRef(index, id) {
250 | return ref => {
251 | if (this.state.refsObj[index] && this.state.refsObj[index] === ref) return;
252 | this.state.refsObj[index] = ref;
253 | this.state.refsObj[index].__id__ = id;
254 | this.setState({
255 | refsObj: this.state.refsObj
256 | });
257 | };
258 | }
259 |
260 | clearStacks = callback => {
261 | this.tabs = [];
262 | this.badges = [];
263 | this.stacks = [];
264 | this.setState(
265 | {
266 | ...this._initialState()
267 | },
268 | () => typeof callback === 'function' && callback()
269 | );
270 | };
271 |
272 | getCurrentRef(index) {
273 | return this.state.refsObj[index ?? this.state.checkedIndex];
274 | }
275 |
276 | toTabView = indexOrLabel => {
277 | switch (typeof indexOrLabel) {
278 | case 'number':
279 | this._onTabviewChange(false, indexOrLabel);
280 | break;
281 | case 'string':
282 | const tab = this.tabs.filter(f => f.tabLabel == indexOrLabel)[0];
283 | if (tab) {
284 | this._onTabviewChange(false, tab.index);
285 | }
286 | break;
287 | }
288 | };
289 |
290 | /**
291 | * y 轴偏移量,0以Tab为基准点
292 | * @memberof ScrollableTabView
293 | */
294 | _scrollTo = y => {
295 | if (typeof y == 'number') {
296 | this.section?.scrollToLocation({
297 | itemIndex: 0,
298 | viewOffset: 0 - y,
299 | sectionIndex: 0
300 | });
301 | }
302 | };
303 |
304 | _initLazyIndexs() {
305 | let lazyIndexs = [],
306 | firstIndex = this._getFirstIndex();
307 | if (firstIndex != null) lazyIndexs.push(firstIndex);
308 | return lazyIndexs;
309 | }
310 |
311 | _getFirstIndex() {
312 | const { firstIndex, stacks } = this.props;
313 | if (typeof firstIndex === 'number' && stacks && stacks.length) {
314 | return this.props.firstIndex;
315 | } else {
316 | return null;
317 | }
318 | }
319 |
320 | /**
321 | * 作为吸顶组件与Screen之间的桥梁,用于同步吸顶组件与Screen之间的状态
322 | * @memberof ScrollableTabView
323 | */
324 | _refresh = () => {
325 | this.setState({});
326 | };
327 |
328 | _getProps(props, screen) {
329 | return Object.assign(
330 | {
331 | refresh: this._refresh,
332 | scrollTo: this._scrollTo,
333 | toTabView: this.toTabView,
334 | layoutHeight: this.layoutHeight
335 | },
336 | !!screen && {
337 | initScreen: () => initScreen(screen),
338 | onRefresh: callback => {
339 | if (!screen.onRefresh) {
340 | screen.onRefresh = () => callback(this._toggledRefreshing);
341 | }
342 | onRefresh(screen, callback);
343 | },
344 | onEndReached: callback => onEndReached(screen, callback)
345 | },
346 | props || {}
347 | );
348 | }
349 |
350 | _renderSticky() {
351 | const stacks = this.props.stacks[this.state.checkedIndex];
352 | const ref = this.getCurrentRef();
353 | if (stacks && stacks.sticky && typeof stacks.sticky == 'function' && ref && stacks.__id__ === ref.__id__) {
354 | // 用于自动同步 Screen 数据流改变后仅会 render 自身 Screen 的问题,用于自动同步 screenContext 给吸顶组件
355 | if (this.props.syncToSticky && !ref.__isOverride__ && this.isClassCompoent(ref.constructor)) {
356 | const originalDidUpdate = ref.componentDidUpdate,
357 | context = this;
358 | ref.componentDidUpdate = function () {
359 | context._refresh();
360 | originalDidUpdate && originalDidUpdate.apply(this, [...arguments]);
361 | };
362 | ref.__isOverride__ = true;
363 | }
364 | return ;
365 | }
366 | return null;
367 | }
368 |
369 | _renderBadges(tabIndex) {
370 | const { useScroll, badges } = this.props;
371 | const _badges = this.badges[tabIndex] || badges[tabIndex];
372 | if (_badges && _badges.length) {
373 | if (useScroll) this._displayConsole('When useScroll and badges exist at the same time, the badge will not overflow the Tabs container');
374 | return _badges.map(item => {
375 | return item;
376 | });
377 | }
378 | return null;
379 | }
380 |
381 | _measureTab(pageIndex, { nativeEvent }) {
382 | const { x, width, height } = nativeEvent.layout;
383 | this.tabsMeasurements[pageIndex] = { left: x, right: x + width, width, height };
384 | }
385 |
386 | _renderTab({ item, index }) {
387 | const { tabActiveOpacity, tabWrapStyle, tabInnerStyle, tabStyle, textStyle, textActiveStyle, tabUnderlineStyle, tabsEnableAnimated } = this.props;
388 | const _tabUnderlineStyle = Object.assign({ top: 6 }, styles.tabUnderlineStyle, tabUnderlineStyle);
389 | const _checked = this.state.checkedIndex == index;
390 | const _tabWrapStyle = typeof tabWrapStyle === 'function' ? tabWrapStyle(item, index, _checked) : tabWrapStyle;
391 | const _tab = typeof item.tabLabelRender === 'function' ? item.tabLabelRender(item.tabLabel, index, _checked) : item.tabLabel;
392 | return (
393 |
394 | {this._renderBadges(index)}
395 | this._onTabviewChange(false, index)} style={[styles.tabStyle, tabStyle]}>
396 |
397 | {typeof _tab === 'string' ? {_tab} : _tab}
398 | {!tabsEnableAnimated && _checked && }
399 |
400 |
401 |
402 | );
403 | }
404 |
405 | _getTabUnderlineInterpolateArgs(tabsEnableAnimatedUnderlineWidth) {
406 | const maxTranslateXCount = this.tabs.length * 2 - 1;
407 | if (maxTranslateXCount === this.tabUnderlineInterpolateArgs.inputRange.length) return this.tabUnderlineInterpolateArgs;
408 | const _outputRange = [];
409 | const _inputRange = Array.from({ length: maxTranslateXCount }, (v, k) => {
410 | _outputRange.push(k % 2 ? this.tabWidth / tabsEnableAnimatedUnderlineWidth : 1);
411 | return k == 0 ? k : (k * deviceWidth) / 2;
412 | });
413 | this.tabUnderlineInterpolateArgs.inputRange = _inputRange;
414 | this.tabUnderlineInterpolateArgs.outputRange = _outputRange;
415 | return this.tabUnderlineInterpolateArgs;
416 | }
417 |
418 | _renderAnimatedTabUnderline() {
419 | const { useScroll, tabUnderlineStyle, useScrollStyle, tabStyle, tabsEnableAnimatedUnderlineWidth } = this.props;
420 | const { marginLeft, marginRight, marginHorizontal } = tabStyle;
421 | const { paddingLeft, paddingHorizontal } = useScrollStyle;
422 | const _tabUnderlineStyle = Object.assign({ zIndex: 100, width: this.tabWidth, position: 'absolute' }, styles.tabUnderlineStyle, tabUnderlineStyle);
423 | if (!_tabUnderlineStyle.top && _tabUnderlineStyle.height) _tabUnderlineStyle.top = this.layoutHeight['tabs'] - _tabUnderlineStyle.height;
424 | let underlineLeft = marginLeft || marginHorizontal || 0;
425 | let underlineRight = marginRight || marginHorizontal || 0;
426 | let outputLeft = 0;
427 | let outputRight = deviceWidth;
428 | this.tabWidthWrap = this.tabWidth + underlineLeft + underlineRight;
429 | if (useScroll) {
430 | outputLeft = paddingLeft || paddingHorizontal || 0;
431 | outputRight = this.tabWidthWrap * this.tabs.length;
432 | }
433 | outputLeft = outputLeft + underlineLeft;
434 | outputRight = outputRight + outputLeft;
435 | const interpolateAnimated = {
436 | transform: [
437 | {
438 | translateX: this.scroll2HorizontalPos.interpolate({
439 | inputRange: [0, this.tabs.length * deviceWidth],
440 | outputRange: [outputLeft, outputRight],
441 | extrapolate: 'clamp'
442 | })
443 | }
444 | ]
445 | };
446 | if (!!tabsEnableAnimatedUnderlineWidth) {
447 | if (tabsEnableAnimatedUnderlineWidth >= this.tabWidth / 2) this._displayConsole('The value of tabsEnableAnimatedUnderlineWidth we recommend to be one-third of tabStyle.width or a fixed 30px');
448 | interpolateAnimated.marginLeft = this.tabWidth / 2 - tabsEnableAnimatedUnderlineWidth / 2;
449 | interpolateAnimated.width = tabsEnableAnimatedUnderlineWidth;
450 | interpolateAnimated.transform.push({ scaleX: this.scroll2HorizontalPos.interpolate(this._getTabUnderlineInterpolateArgs(tabsEnableAnimatedUnderlineWidth)) });
451 | }
452 | return ;
453 | }
454 |
455 | _displayConsole(message, level = CONSOLE_LEVEL.LOG) {
456 | const { errorToThrow } = this.props;
457 | const pluginName = packagejson.name;
458 | const msg = `${pluginName}: ${message || ' --- '}`;
459 | console[level](msg);
460 | if (errorToThrow && level == CONSOLE_LEVEL.ERROR) throw new Error(msg);
461 | }
462 |
463 | _errorProps(propName, level) {
464 | const props = this.props,
465 | property = props[propName],
466 | errorProps = {
467 | tabStyle: ['left', 'right']
468 | };
469 | if (errorProps[propName] && property) {
470 | errorProps[propName].forEach(errorProperty => {
471 | if (errorProperty in property) {
472 | this._displayConsole(`Prop ${propName} is not allowed to configure the ${errorProperty} property`, level);
473 | }
474 | });
475 | }
476 | }
477 |
478 | _renderTabs() {
479 | const { oneTabHidden, tabsShown, tabsStyle, tabStyle, useScroll, tabsEnableAnimated, useScrollStyle } = this.props;
480 | const { width } = tabStyle;
481 | if (tabsEnableAnimated && tabStyle && width == undefined) this._displayConsole('When tabsEnableAnimated is true, the width must be specified for tabStyle', CONSOLE_LEVEL.ERROR);
482 | if (useScroll && tabStyle && width == undefined) this._displayConsole('When useScroll is true, the width must be specified for tabStyle', CONSOLE_LEVEL.ERROR);
483 | const renderTab = !(oneTabHidden && this.tabs && this.tabs.length == 1) && tabsShown;
484 | const _tabsStyle = Object.assign({}, !useScroll && { alignItems: 'center', justifyContent: 'space-around' }, styles.tabsStyle, tabsStyle);
485 | this.layoutHeight['tabs'] = renderTab ? _tabsStyle.height : 0;
486 | this.tabWidth = width;
487 | this._errorProps('tabStyle', CONSOLE_LEVEL.ERROR);
488 | return (
489 | renderTab &&
490 | this.tabs &&
491 | !!this.tabs.length &&
492 | (useScroll ? (
493 | (this.scrollview = rf)}
499 | horizontal={true}
500 | >
501 | {this.tabs.map((tab, index) => this._renderTab({ item: tab, index }))}
502 | {tabsEnableAnimated && this.state.checkedIndex !== null && this._renderAnimatedTabUnderline()}
503 |
504 | ) : (
505 |
506 | {this.tabs.map((tab, index) => this._renderTab({ item: tab, index }))}
507 | {tabsEnableAnimated && this.state.checkedIndex !== null && this._renderAnimatedTabUnderline()}
508 |
509 | ))
510 | );
511 | }
512 |
513 | _renderSectionHeader() {
514 | const { fixedHeader } = this.props;
515 | return (
516 |
517 | {this._renderHeader(fixedHeader)}
518 | {this._renderStickyHeader()}
519 | {this._renderTabs()}
520 | {this._renderSticky()}
521 |
522 | );
523 | }
524 |
525 | // 启用 useScroll 情况下保证滚动条跟随
526 | _tabTranslateX(index = this.state.checkedIndex) {
527 | const { useScroll } = this.props;
528 | const width = this.tabWidthWrap || this.tabWidth;
529 | if (useScroll && this.scrollview && width) {
530 | this.scrollview.scrollTo({
531 | x: (index - 1) * width + width / 2
532 | });
533 | }
534 | }
535 |
536 | _resetOtherRefs() {
537 | const checkedIndex = this.state.checkedIndex;
538 | if (this.state.refsObj && this.state.refsObj[checkedIndex]) this.state.refsObj[checkedIndex] = null;
539 | return this.state.refsObj;
540 | }
541 |
542 | _onTabviewChange(isCarouselScroll, index) {
543 | if (!this.stacks.length) return;
544 | if (!this.stacks[index]) return;
545 | const { enableCachePage, toHeaderOnTab, toTabsOnTab, onTabviewChanged } = this.props;
546 | if (index == this.state.checkedIndex) {
547 | if (!isCarouselScroll && toHeaderOnTab) return this._scrollTo(-(this.layoutHeight['header'] + this.layoutHeight['stickyHeader']));
548 | if (!isCarouselScroll && toTabsOnTab) return this._scrollTo(0);
549 | return void 0;
550 | }
551 | let state = {
552 | checkedIndex: index,
553 | lazyIndexs: this.state.lazyIndexs
554 | },
555 | isFirst = false;
556 | if (!enableCachePage) {
557 | isFirst = true;
558 | state.refsObj = this._resetOtherRefs();
559 | state.lazyIndexs = [index];
560 | } else {
561 | if (!this._getLazyIndexs(index)) state.lazyIndexs.push(index), (isFirst = true);
562 | }
563 | this.setState(state, () => {
564 | if (onTabviewChanged) {
565 | const tab = this.tabs[this.state.checkedIndex];
566 | onTabviewChanged(this.state.checkedIndex, tab && tab.tabLabel, isFirst);
567 | }
568 | });
569 | this._tabTranslateX(index);
570 | // 切换后强制重置刷新状态
571 | this._toggledRefreshing(false);
572 | }
573 |
574 | _getLazyIndexs(index) {
575 | return this.state.lazyIndexs.includes(index);
576 | }
577 |
578 | _getScreenHeight() {
579 | this.layoutHeight['screen'] = this.layoutHeight['container'] - (this.layoutHeight['header'] + this.layoutHeight['stickyHeader'] + this.layoutHeight['tabs']);
580 | return this.layoutHeight['screen'];
581 | }
582 |
583 | _getMaximumScreenHeight() {
584 | return this.layoutHeight['container'] - this.layoutHeight['stickyHeader'] - this.layoutHeight['tabs'];
585 | }
586 |
587 | isClassCompoent(component) {
588 | return !!(component.prototype && component.prototype.isReactComponent);
589 | }
590 |
591 | _renderItem({ item, index }) {
592 | const { enableCachePage, fillScreen, fixedTabs, mappingProps } = this.props;
593 | const screenHeight = this._getScreenHeight();
594 | return (
595 | (enableCachePage ? enableCachePage : this.state.checkedIndex == index) &&
596 | (this.getCurrentRef(index) || this.getCurrentRef(index) == undefined) &&
597 | this._getLazyIndexs(index) && (
598 |
607 |
608 |
609 | )
610 | );
611 | }
612 |
613 | _onEndReached() {
614 | const next = () => {
615 | const ref = this.getCurrentRef();
616 | !ref && this._displayConsole(`The Screen Ref is lost when calling onEndReached. Please confirm whether the Stack is working properly.(index: ${this.state.checkedIndex})`);
617 | !!ref && this.isClassCompoent(ref.constructor) ? ref && ref.onEndReached && typeof ref.onEndReached === 'function' && ref.onEndReached() : triggerEndReached(ref);
618 | };
619 | if (this.state.checkedIndex != null) {
620 | const { onBeforeEndReached } = this.props;
621 | onBeforeEndReached && typeof onBeforeEndReached === 'function' ? onBeforeEndReached(next) : next();
622 | }
623 | }
624 |
625 | _toggledRefreshing(status) {
626 | this.setState({
627 | isRefreshing: status ?? !this.state.isRefreshing
628 | });
629 | }
630 |
631 | _onRefresh() {
632 | const next = () => {
633 | const ref = this.getCurrentRef();
634 | !ref && this._displayConsole(`The Screen Ref is lost when calling onRefresh. Please confirm whether the Stack is working properly.(index: ${this.state.checkedIndex})`);
635 | if (ref) {
636 | this.isClassCompoent(ref.constructor) ? ref.onRefresh && typeof ref.onRefresh === 'function' && ref.onRefresh(this._toggledRefreshing) : triggerRefresh(ref, this._toggledRefreshing);
637 | } else {
638 | this._toggledRefreshing(false);
639 | }
640 | };
641 | const { onBeforeRefresh } = this.props;
642 | onBeforeRefresh && typeof onBeforeRefresh === 'function' ? onBeforeRefresh(next, this._toggledRefreshing) : next();
643 | }
644 |
645 | _renderHeader = isRender => {
646 | const { header } = this.props;
647 | return (
648 | header &&
649 | isRender && (
650 | {
652 | const { height } = nativeEvent.layout;
653 | this.layoutHeight['header'] = height;
654 | if (height !== 0) this._refresh();
655 | }}
656 | >
657 | {typeof header === 'function' ? header() : header}
658 |
659 | )
660 | );
661 | };
662 |
663 | _renderStickyHeader = () => {
664 | const { stickyHeader } = this.props;
665 | return (
666 | stickyHeader && (
667 | {
669 | const { height } = nativeEvent.layout;
670 | this.layoutHeight['stickyHeader'] = height;
671 | if (height !== 0) this._refresh();
672 | }}
673 | >
674 | {typeof stickyHeader === 'function' ? stickyHeader() : stickyHeader}
675 |
676 | )
677 | );
678 | };
679 |
680 | _renderTitle = () => {
681 | const { title, titleArgs } = this.props;
682 | if (!title) return null;
683 | const { style, interpolateHeight, interpolateOpacity } = titleArgs;
684 | return (
685 |
695 | {typeof title === 'function' ? title() : title}
696 |
697 | );
698 | };
699 |
700 | _refreshControl() {
701 | const ref = this.getCurrentRef();
702 | const enabled = !!(ref && ref.onRefresh) || refreshMap.has(ref);
703 | return ;
704 | }
705 |
706 | _onScroll2Vertical(event) {
707 | const { onScroll } = this.props;
708 | // TODO...
709 | if (typeof onScroll === 'function' && event) onScroll(event);
710 | }
711 |
712 | _setScrollHandler2Vertical() {
713 | const { title } = this.props;
714 | const scrollEventConfig = {
715 | listener: this._onScroll2Vertical,
716 | // Error: Style property 'height' is not supported by native animated module, Maybe replaced with scaleX in the future.
717 | useNativeDriver: !title
718 | };
719 | const argMapping = [];
720 | argMapping.push({ nativeEvent: { contentOffset: { y: this.scroll2VerticalPos } } });
721 | this._onScrollHandler2Vertical = Animated.event(argMapping, scrollEventConfig);
722 | }
723 |
724 | _onScroll2Horizontal(event) {
725 | const { onScroll2Horizontal } = this.props;
726 | // TODO...
727 | if (typeof onScroll2Horizontal === 'function' && event) onScroll2Horizontal(event);
728 | }
729 |
730 | _setScrollHandler2Horizontal() {
731 | const scrollEventConfig = {
732 | listener: this._onScroll2Horizontal,
733 | useNativeDriver: true
734 | };
735 | const argMapping = [];
736 | argMapping.push({ nativeEvent: { contentOffset: { x: this.scroll2HorizontalPos } } });
737 | this._onScrollHandler2Horizontal = Animated.event(argMapping, scrollEventConfig);
738 | }
739 |
740 | render() {
741 | const { style, onEndReachedThreshold, fixedHeader, carouselProps, sectionListProps } = this.props;
742 | return (
743 | {
745 | const { height } = nativeEvent.layout;
746 | this.layoutHeight['container'] = height;
747 | if (height !== 0) this._refresh();
748 | }}
749 | style={[styles.container, style]}
750 | >
751 | {this._renderTitle()}
752 | (this.section = rf)}
754 | keyExtractor={(item, index) => `scrollable-tab-view-wrap-${index}`}
755 | renderSectionHeader={this._renderSectionHeader}
756 | onEndReached={this._onEndReached}
757 | onEndReachedThreshold={onEndReachedThreshold}
758 | refreshControl={this._refreshControl()}
759 | sections={[{ data: [1] }]}
760 | stickySectionHeadersEnabled={true}
761 | ListHeaderComponent={this._renderHeader(!fixedHeader)}
762 | showsVerticalScrollIndicator={false}
763 | showsHorizontalScrollIndicator={false}
764 | renderItem={() => {
765 | return (
766 |
779 | );
780 | }}
781 | onScrollToIndexFailed={() => {}}
782 | onScroll={this._onScrollHandler2Vertical}
783 | {...sectionListProps}
784 | >
785 |
786 | );
787 | }
788 | }
789 |
790 | const styles = StyleSheet.create({
791 | container: { flex: 1 },
792 | tabsStyle: { flex: 1, zIndex: 100, flexDirection: 'row', backgroundColor: '#ffffff', height: 35, marginBottom: -0.5 },
793 | tabStyle: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#ffffff' },
794 | textStyle: { height: 20, fontSize: 12, color: '#11111180', textAlign: 'center', lineHeight: 20 },
795 | tabUnderlineStyle: { height: 2, borderRadius: 2, backgroundColor: '#00aced' }
796 | });
797 |
--------------------------------------------------------------------------------
/src/useRefreshEndReached.js:
--------------------------------------------------------------------------------
1 | export const onceMap = new Map();
2 | export const refreshMap = new Map();
3 | export const endReachedhMap = new Map();
4 |
5 | export const initScreen = screen => {
6 | if (onceMap.has(screen)) {
7 | const getRef = onceMap.get(screen);
8 | getRef && getRef(screen);
9 | onceMap.delete(screen);
10 | }
11 | };
12 |
13 | export const triggerOnce = (screen, getRef) => {
14 | if (!onceMap.has(screen)) {
15 | onceMap.set(screen, getRef);
16 | }
17 | };
18 |
19 | export const onRefresh = (screen, callback) => {
20 | if (!refreshMap.has(screen)) {
21 | refreshMap.set(screen, callback);
22 | }
23 | };
24 |
25 | export const triggerRefresh = (screen, toggled) => {
26 | if (refreshMap.has(screen)) {
27 | const callback = refreshMap.get(screen);
28 | callback && callback(toggled);
29 | }
30 | };
31 |
32 | export const onEndReached = (screen, callback) => {
33 | if (!endReachedhMap.has(screen)) {
34 | endReachedhMap.set(screen, callback);
35 | }
36 | };
37 |
38 | export const triggerEndReached = screen => {
39 | if (endReachedhMap.has(screen)) {
40 | const callback = endReachedhMap.get(screen);
41 | callback && callback();
42 | }
43 | };
44 |
--------------------------------------------------------------------------------