├── .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 | [![NPM Version](http://img.shields.io/npm/v/@itenl/react-native-scrollable-tabview.svg?style=flat)](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 | [![NPM Version](http://img.shields.io/npm/v/@itenl/react-native-scrollable-tabview.svg?style=flat)](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 | --------------------------------------------------------------------------------