# react-native-scrollable-tabview

[![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)

[English](./README.md) | 简体中文

基于纯 `JS` 脚本，不依赖原生，无需 `react-native link`，`Title` / `Header` / `Tabs` / `Sticky` / `Screen` 组件可灵活配置，其中 `Tabs` / `Sticky` 可在滑动到顶部时会进行吸顶；我们所支持的是以栈的形式独立管理自身的 `Sticky` / `Screen` / `Badge` / `tabLabel` 各项配置，并且为 `Screen` 注入[生命周期](#InjectionLifecycle) `onRefresh` / `onEndReached`它们将在下拉刷新与滚动条触底时触发，最后还为 `Screen` / `Sticky` 注入了更多 [props](#InjectionScreenProps)

##### Table of Contents
* [Example-API](https://github.com/itenl/react-native-scrollable-tabview-example-app)
* [Example-TikTok](https://github.com/itenl/react-native-scrollable-tabview-example-tiktok)
* [Features](#features)
* [Installation](#installation)
* [Usage](#usage)
* [Props](#props)
* [Method](#method)
* [Stack Property](#StackProperty)
* [Badge Property](#BadgeProperty)
* [Injection lifecycle to Screen](#InjectionLifecycle)
* [Injection props to Screen](#InjectionScreenProps)
* [Injection props to Sticky](#InjectionStickyProps)
* [Known Issues](#KnownIssues)
* [Snapshot](#Snapshot)

## <a name="features"/>Features
* 支持为每个 `Screen` 设置下拉刷新与上滑加载更多(生命周期注入或属性注入)
* Tabs 支持固定自适应与水平滚动两种配置方式
* 允许为每个 `Screen` 独立配置 `Sticky` 组件
* 允许为每个 `Tab` 独立配置自定义的徽章
* 支持下拉刷新与上滑加载更多前置函数 `onBeforeRefresh` / `onBeforeEndReached` 
* 支持动画标题，可支持动画为 `interpolate.opacity` 与 `interpolate.height`

## <a name="installation"/>Installation

```shell
npm i @itenl/react-native-scrollable-tabview
```
or
```shell
yarn add @itenl/react-native-scrollable-tabview
```

## <a name="usage"/>Usage

```jsx
import React from 'react';
import ScrollableTabView from '@itenl/react-native-scrollable-tabview';

function App() {
  return (
      <ScrollableTabView
        ref={rf => (this.scrollableTabView = rf)}
        mappingProps={{
          fromRootEst: this.state.est,
        }}
        badges={[
          null,
          [
            <View
              style={{
                position: 'absolute',
                zIndex: 100,
                top: 10,
                right: 0,
              }}
            >
              <Text>new</Text>
            </View>,
            <View
              style={{
                position: 'absolute',
                width: 150,
                height: 50,
                zIndex: 100,
                marginTop: 35,
                right: 0,
                opacity: 0.6,
                backgroundColor: 'pink',
                justifyContent: 'center',
                alignItems: 'center',
              }}
            >
              <Text>Three Tips</Text>
            </View>,
          ],
        ]}
        stacks={[
          {
            screen: One,
            sticky: Sticky,
            tabLabel: 'OneTab',
            tabLabelRender: tabLabel => {
              return `--- ${tabLabel} ---`;
            },
            badge: [<Text>one</Text>, <Text>two</Text>],
            toProps: {
              xx: 123,
            },
          }, {
            screen: ({
              layoutHeight,
              refresh,
              scrollTo,
              toTabView,
              initScreen,
              onRefresh,
              onEndReached,
            }) => {
              // The code is required
              initScreen();
              const [datetime, setDatetime] = useState(Date.now());
              useEffect(() => {
                setInterval(() => {
                  setDatetime(Date.now());
                }, 1000);
              }, []);
              onRefresh((toggled) => {
                toggled(true);
                alert("onRefresh start");
                setTimeout(() => {
                  toggled(false);
                  alert("onRefresh stop");
                }, 3000);
              });
              onEndReached(() => {
                alert("onEndReached");
              });
              return (
                <View
                  style={{
                    flex: 1,
                    backgroundColor: "#151723",
                    justifyContent: "center",
                    alignItems: "center",
                  }}
                >
                  <Text style={{ color: "#ffffff" }}>
                    Test function component {datetime}
                  </Text>
                </View>
              );
            },
            tabLabel: "TestFunctionComponent",
          }
        ]}
        tabsStyle={{}}
        tabWrapStyle={{}}
        tabInnerStyle={{}}
        tabActiveOpacity={0.6}
        tabStyle={{}}
        textStyle={{}}
        textActiveStyle={{}}
        tabUnderlineStyle={{}}
        firstIndex={0}
        syncToSticky={true}
        onEndReachedThreshold={0.1}
        onBeforeRefresh={(next, toggled) => {
          toggled();
          next();
        }}
        onBeforeEndReached={next => {
          next();
        }}
        onTabviewChanged={(index, tabLabel, isFirst) => {
          alert(index);
        }}
        header={() => {
          return <View style={{ backgroundColor: 'pink', height: 120 }}></View>;
        }}
        oneTabHidden={true}
        enableCachePage={true}
        carouselProps={{}}
        sectionListProps={{}}
        toHeaderOnTab={true}
        toTabsOnTab={true}
        tabsShown={false}
        fixedTabs={false}
        fixedHeader={false}
        useScroll={false}
        fillScreen={true}
      ></ScrollableTabView>
  );
}
```

## <a name="props"/>Props

All props are optional

Prop              | Type     | Default     | Description
----------------- | -------- | ----------- | -----------
**`stacks`**            | Array    | []          | 页面栈 < [阅读 Stack Property](#StackProperty) >
**`mappingProps`**      | Object   | {}          | 关联映射数据到 Stack / Sticky
**`badges`**             | Array    | []          | 针对每个Tab的徽章 < [阅读 Badge Property](#BadgeProperty) >
**`tabsStyle`**             | Object    | {}          | 整个Tabs样式
**`tabWrapStyle`**             | Object / Function    | {}          | 单个Tab外包装样式 (函数参数提供了item, index, 需要返回样式对象，eg. **`return index == 1 && { zIndex : 10}`**)
**`tabInnerStyle`**             | Object    | {}          | 单个Tab内包装样式
**`tabActiveOpacity`**             | Number    | 0.6          | Tab按钮点击后透明度
**`tabStyle`**             | Object    | {}          | 单个Tab样式
**`textStyle`**             | Object    | {}          | Tab内文本样式
**`textActiveStyle`**             | Object    | {}          | 选中激活的text样式
**`tabUnderlineStyle`**             | Object    | {}          | 选中激活的下划线样式
**`firstIndex`**             | Number / Null    | null          | 设置 **`firstIndex`** 的栈为活动状态 (请在设定 **`firstIndex`** 值的时候确保 **`stacks`** 的个数大于 **`firstIndex`** )
**`syncToSticky`**             | Boolean    | true          | 是否同步(Screen中发生 **`render`** 触发 **`componentDidUpdate`** 将更新Sticky)
**`onEndReachedThreshold`**             | Number    | 0.2          | 触底回调阈值
**`onBeforeRefresh`**             | Function    | null          | 下拉刷新前置函数, 执行 **`next`** 将执行Screen中 **`onRefresh`** 函数，执行 **`toggled`** 将切换系统loading,可传 true / false 进行指定 (回调含有 **`next`** , **`toggled`** 两个形参)
**`onBeforeEndReached`**             | Function    | null          | 上滑加载更多前置函数, 执行next将执行Screen中 **`onEndReached`** 函数 (回调含有 **`next`** 形参)
**`onTabviewChanged`**             | Function    | null          | Tab切换完成回调 (回调含有 **`index`**, **`tabLabel`**, **`isFirst`** 形参)
**`screenScrollThrottle`**             | Number    | 60          | **`Screen`** 横向滑动时节流参数,单位 (毫秒)
**`header`**             | Function / JSX Element / Class Component    | null          | 顶部组件 (若是函数需要返回 Element)
**`stickyHeader`**             | Function / JSX Element / Class Component    | null          | 顶部带吸顶效果组件 (若是函数需要返回 Element)
**`oneTabHidden`**             | Boolean    | false          | 仅一个Tab时将隐藏自身
**`enableCachePage`**             | Boolean    | true          | 是否持久化页面切换后不销毁
**`carouselProps`**             | Object    | {}          | 传递给 Carousel 的剩余属性 < [阅读 Carousel](https://github.com/meliorence/react-native-snap-carousel/blob/master/doc/PROPS_METHODS_AND_GETTERS.md) >
**`sectionListProps`**             | Object    | {}          | 传递给 SectionList 的剩余属性 < [阅读 SectionList](https://reactnative.dev/docs/sectionlist) >
**`toHeaderOnTab`**             | Boolean    | false          | 点击触发已激活的Tab将回到Header(高优先级)
**`toTabsOnTab`**             | Boolean    | false          | 点击触发已激活的Tab将回到Tabs
**`tabsShown`**             | Boolean    | true          | 配置 Tabs 显示隐藏
**`fixedTabs`**             | Boolean    | false          | 在 **`enableCachePage`** 为true的情况下滑动切换Screen设置最小高度保障Header与Tabs不会弹跳
**`fixedHeader`**             | Boolean    | false          | 与Tabs一同渲染，固定顶部Header，不跟随滚动
**`useScroll`**             | Boolean    | false          | Tabs是否支持横向滚动(存在多个类目Tab的情况需要启用，建议 **`tabStyle`** 传入固定宽度)
**`useScrollStyle`**             | Object    | {}          | 为滚动的 **`Tabs`** 设置 **`contentContainerStyle`**，常见为左右两侧添加边距 **`paddingLeft`** **`paddingHorizontal`**
**`fillScreen`**             | Boolean    | true          | 填充整个 Screen
**`title`**             | Function / JSX Element / Class Component    | null          | 动画标题
**`titleArgs`**             | Object    | **`{  style: {}, interpolateOpacity: {}, interpolateHeight: {} }`**          | 标题配置 < [阅读 interpolate](https://reactnative.dev/docs/animations#interpolation) >
**`onScroll`**             | Function    | null          | 滚动事件监听
**`onScroll2Horizontal`**             | Function    | null          | 滚动事件监听(横向)
**`tabsEnableAnimated`**             | Boolean    | false          | 为Tabs启用滑动效果，需要为 **`tabStyle`** 指定 **`width`**
**`tabsEnableAnimatedUnderlineWidth`**             | Number    | 0          | 为Tabs Underline设定固定宽度并添加弹跳动画，需要启用 **`tabsEnableAnimated=true`**.( 建议传入 **`tabStyle.width`** 的三分之一或固定 30px )
**`errorToThrow`**             | Boolean    | false          | **`console.error`** 将会抛出错误 **`throw new Error()`**


## <a name="method"/>Method

``` javascript
  <ScrollableTabView
    ref={rf => (this.scrollableTabView = rf)}
  >
  </ScrollableTabView> 
  this.scrollableTabView.getCurrentRef();
  this.scrollableTabView.toTabView(1);
  this.scrollableTabView.scrollTo(0);
  this.scrollableTabView.clearStacks(()=>alert('done'));
  
```

Name              | Type     | Description
----------------- | -------- | -----------
**`getCurrentRef(index: number.optional)`**            | Function   | 获取当前活动的视图的实例，可传 **`index`** 获取指定实例
**`toTabView(index: number.required / label: string.required)`**            | Function   | 跳到指定 Screen
**`scrollTo(index: number.required)`**            | Function   | 上下滑动至指定位置 (传入 0 默认定位至 tabs / 传入负数则置顶)
**`clearStacks(callback: function.optional)`**            | Function   | 清空栈以及相关状态 (Tabs / Badge / Stacks))

## <a name="StackProperty"/>Stack Property

Name              | Type     | Description
----------------- | -------- | -----------
**`screen`**            | Class Component   | Screen 类组件
**`sticky`**            | Class Component   | 吸顶类组件, 实例内将返回该类组件的上下文
**`tabLabel`**            | String   | Tab 昵称
**`tabLabelRender`**            | Function   | 自定义 Tab渲染函数，优先级高于 **`tabLabel`**
**`badge`**            | Array    | 针对当前 Tab 的徽章，与 **`badges`** 属性互斥，优先级高于最外层属性 **`badges`** < [阅读 Badge Property](#BadgeProperty) >
**`toProps`**            | Object    | **`toProps`** 仅传递给 Screen，不作数据关联

## <a name="BadgeProperty"/>Badge Property

Type     | Description
-------- | -----------
JSX Element   | 基于当前Tab进行渲染的 徽章 / 悬浮 Tips 等


##  <a name="InjectionLifecycle"/>Injection lifecycle to Screen(仅在类组件)

Name              | Type     | Description
----------------- | -------- | -----------
**`onRefresh`**            | Function   | 下拉刷新时触发,形参 **`toggled`** 函数用于切换原生 loading 状态的显隐，若在 loading 中用户切换 tab 将会强制隐藏并重置状态
**`onEndReached`**            | Function   | 上滑加载更多触发


##  <a name="InjectionScreenProps"/>Injection props to Screen

Name              | Type     | Description
----------------- | -------- | -----------
**`refresh()`**            | Function   | 手动触发刷新、同步Screen状态至Sticky
**`scrollTo(index: number.required)`**            | Function   | 上下滑动至指定位置 (传入 0 默认定位至 tabs / 传入负数则置顶)
**`toTabView(index: number.required / label: string.required)`**            | Function   | 跳到指定 Screen
**`layoutHeight.container`**            | Number   | Container 容器总高度
**`layoutHeight.header`**            | Number   | Header 高度
**`layoutHeight.tabs`**            | Number   | Tabs 高度
**`layoutHeight.screen`**            | Number   | 视图 高度
**`initScreen`**            | Function   | (仅在函数组件) 如果是函数组件则必须调用
**`onRefresh`**            | Function   | (仅在函数组件) < [阅读 onRefresh 描述](#InjectionLifecycle) >
**`onEndReached`**            | Function   | (仅在函数组件) < [阅读 onEndReached 描述](#InjectionLifecycle) >

##  <a name="InjectionStickyProps"/>Injection props to Sticky

Name              | Type     | Description
----------------- | -------- | -----------
**`screenContext`**            | Object   | 获取 Screen 上下文

## <a name="KnownIssues"/>Known Issues
- 如果你仅仅是新增 `Stack` 可以 `Push` 即可，但如果需要更新或者删除 `Stack` 请使用 [clearStacks](#Method) 后在添加你所需要的 `Stacks`

## <a name="Snapshot"/>Snapshot

### Android (Sliding Tabs)
<img src="./snapshot/e18k6-3jmxk.gif" />

### iOS (Bounce Tabs)
<img src="./snapshot/e18k6-3jmxk-2.gif" />

### API Example
<img src="./snapshot/qoz8r-klpuc.gif" />
<br />

---

**MIT Licensed**