diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml deleted file mode 100644 index e266119..0000000 --- a/.github/actions/setup/action.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Setup -description: Setup Node.js and install dependencies - -runs: - using: composite - steps: - - name: Setup Node.js - uses: actions/setup-node@v3 - with: - node-version-file: .nvmrc - - - name: Cache dependencies - id: yarn-cache - uses: actions/cache@v3 - with: - path: | - **/node_modules - .yarn/install-state.gz - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('**/package.json') }} - restore-keys: | - ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - ${{ runner.os }}-yarn- - - - name: Install dependencies - if: steps.yarn-cache.outputs.cache-hit != 'true' - run: yarn install --immutable - shell: bash diff --git a/.github/workflows/package-publish.yml b/.github/workflows/package-publish.yml new file mode 100644 index 0000000..06006a4 --- /dev/null +++ b/.github/workflows/package-publish.yml @@ -0,0 +1,21 @@ +name: package-publish + +on: workflow_dispatch + +jobs: + package-publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: '18.x' + registry-url: 'https://registry.npmjs.org' + - name: Install Package + run: yarn + - name: Build + run: yarn prepare + - name: Publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npm whoami && npm publish --access public diff --git a/package.json b/package.json index 491c554..99bf7cb 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { - "name": "react-native-tab-page-view", - "version": "0.1.0", + "name": "@onekeyfe/react-native-tab-page-view", + "version": "1.0.0", "description": "React Native Tab Page View", - "main": "src/index", - "module": "src/index", - "types": "src/index", + "main": "lib/commonjs/index", + "module": "lib/module/index", + "types": "lib/typescript/src/index.d.ts", "react-native": "src/index", "source": "src/index", "files": [ @@ -31,8 +31,7 @@ "typecheck": "tsc --noEmit", "lint": "eslint \"**/*.{js,ts,tsx}\"", "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib", - "prepare": "bob build", - "release": "release-it" + "prepare": "bob build" }, "keywords": [ "react-native", diff --git a/src/ContentFlatList/index.tsx b/src/ContentFlatList/index.tsx index 012ee04..38b1fce 100644 --- a/src/ContentFlatList/index.tsx +++ b/src/ContentFlatList/index.tsx @@ -69,6 +69,9 @@ export default class ContentFlatList extends Component { }; private _onScroll = (event: NativeSyntheticEvent) => { + if (event.nativeEvent.layoutMeasurement.width <= 0) { + return; + } this.props.onScroll && this.props.onScroll(event); if (this.isDraging) { return; diff --git a/src/PageContentView.tsx b/src/PageContentView.tsx index 81365cd..62188f3 100644 --- a/src/PageContentView.tsx +++ b/src/PageContentView.tsx @@ -2,7 +2,11 @@ import React, { Component } from 'react'; import { View, Animated, Dimensions, PixelRatio } from 'react-native'; import ContentFlatList from './ContentFlatList'; import type { RefObject } from 'react'; -import type { FlatListProps, ListRenderItemInfo } from 'react-native'; +import type { + FlatListProps, + LayoutChangeEvent, + ListRenderItemInfo, +} from 'react-native'; interface PageContentViewProps extends FlatListProps { shouldSelectedPageAnimation?: boolean; @@ -79,13 +83,13 @@ export default class PageContentView extends Component { } catch (e) {} }; - private _onLayout = ({ - nativeEvent: { - layout: { width }, - }, - }: { - nativeEvent: { layout: { width: number } }; - }) => { + private _onLayout = (event: LayoutChangeEvent) => { + this.props.onLayout?.(event); + const { + nativeEvent: { + layout: { width }, + }, + } = event; const reloadWidth = PixelRatio.roundToNearestPixel(width); if (reloadWidth !== this.state.scrollViewWidth && reloadWidth > 0) { this._scrollViewWidthValue.setValue(reloadWidth); diff --git a/src/PageHeaderCursor.tsx b/src/PageHeaderCursor.tsx index aa11ebc..e2b689d 100644 --- a/src/PageHeaderCursor.tsx +++ b/src/PageHeaderCursor.tsx @@ -1,6 +1,7 @@ import React, { Component } from 'react'; +import type { RefObject } from 'react'; import { View, Animated } from 'react-native'; -import type { LayoutChangeEvent, LayoutRectangle } from 'react-native'; +import type { LayoutRectangle } from 'react-native'; interface PageHeaderCursorProps { data: any[]; @@ -9,42 +10,58 @@ interface PageHeaderCursorProps { renderCursor?: () => React.ReactElement | null; } +interface PageHeaderCursorState { + itemContainerLayoutList: (LayoutRectangle | undefined)[]; +} + export default class PageHeaderCursor extends Component { - override state: { - itemContainerLayoutList: (LayoutRectangle | undefined)[]; - } = { + override state: PageHeaderCursorState = { itemContainerLayoutList: this.props.data.map(() => undefined), }; - public onLayoutItemContainer = ( - event: LayoutChangeEvent, - _: any, - index: number + public reloadItemListContainerLayout = ( + refList: Array>, + scrollRef: RefObject ) => { - const itemContainerLayout = event.nativeEvent.layout; - if (itemContainerLayout == this.state.itemContainerLayoutList[index]) { - return; - } - this.state.itemContainerLayoutList[index] = itemContainerLayout; - let fullLoadItemContainerLayout = true; - this.state.itemContainerLayoutList.forEach((item) => { - if (!item) { - fullLoadItemContainerLayout = false; - } + this.state.itemContainerLayoutList = this.props.data.map(() => undefined); + refList.map((ref, index) => { + ref?.current?.measureLayout( + scrollRef.current as any, + (x: number, y: number, width: number, height: number) => { + if (x + width <= 0) { + return; + } + this.state.itemContainerLayoutList[index] = { x, y, width, height }; + if ( + this.state.itemContainerLayoutList.findIndex((item) => !item) == -1 + ) { + this.setState(this.state); + } + } + ); }); - if (fullLoadItemContainerLayout) { - this.forceUpdate(); + }; + + private _findPercentCursorWidth = () => { + const { width } = this.props.cursorStyle as any; + if (typeof width == 'string') { + return width.match(/(\d+(\.\d+)?)%/)?.[1]; } + return null; }; private _findFixCursorWidth = () => { const { width } = this.props.cursorStyle as any; + if (this._findPercentCursorWidth()) { + return null; + } return width; }; private _reloadPageIndexValue = (isWidth: boolean) => { const fixCursorWidth = this._findFixCursorWidth(); const { left = 0, right = 0 } = this?.props?.cursorStyle as any; + const percentWidth = this._findPercentCursorWidth(); const rangeList = (isIndex: boolean) => { const itemList = [isIndex ? -1 : 0]; itemList.push( @@ -59,7 +76,9 @@ export default class PageHeaderCursor extends Component { : item.x + (item.width - fixCursorWidth) / 2.0; } else { const width = item.width - left - right; - return isWidth ? width : item.x + width / 2.0 + left; + return isWidth + ? (width * Number(percentWidth ?? 100)) / 100 + : item.x + width / 2.0 + left; } } else { return 0; @@ -77,11 +96,15 @@ export default class PageHeaderCursor extends Component { }); }; - override shouldComponentUpdate(nextProps: PageHeaderCursorProps) { - if (nextProps.data !== this.props.data) { - this.setState({ - itemContainerLayoutList: nextProps.data.map(() => false), - }); + override shouldComponentUpdate( + nextProps: PageHeaderCursorProps, + nextState: PageHeaderCursorState + ) { + if ( + nextProps.data !== this.props.data || + nextProps.cursorStyle !== this.props.cursorStyle || + nextState !== this.state + ) { return true; } return false; @@ -110,7 +133,17 @@ export default class PageHeaderCursor extends Component { return ( !item) == -1 + ? 1 + : 0, + }} > {this.props.renderCursor ? ( diff --git a/src/PageHeaderView.tsx b/src/PageHeaderView.tsx index bfa6c22..e700319 100644 --- a/src/PageHeaderView.tsx +++ b/src/PageHeaderView.tsx @@ -30,7 +30,7 @@ interface PageHeaderViewProps extends ScrollViewProps { renderCursor?: () => React.ReactElement | null; onSelectedPageIndex?: (index: number) => void; selectedPageIndexDuration?: number; - shouldSelectedPageIndex?: () => boolean; + shouldSelectedPageIndex?: (pageIndex: number) => boolean; scrollContainerStyle?: object; contentContainerStyle?: object; containerStyle?: object; @@ -43,34 +43,41 @@ export default class PageHeaderView extends Component { private shouldHandlerAnimationValue = true; private itemConfigList = this.props.data.map((_: any) => ({ _animtedEnabledValue: new Animated.Value(1), + _containerRef: React.createRef(), })); - private itemContainerStyle = { - height: '100%', - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - ...this.props.itemContainerStyle, - } as { flex?: number } & object; - private itemTitleNormalStyle: { fontSize: number; color: string } & object = { + private itemContainerStyle = () => + ({ + height: '100%', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + ...this.props.itemContainerStyle, + } as { flex?: number } & object); + private itemTitleNormalStyle: () => { + fontSize: number; + color: string; + } & object = () => ({ fontSize: 16, color: '#333', ...this.props.itemTitleStyle, ...this.props.itemTitleNormalStyle, - }; - private itemTitleSelectedStyle: { fontSize: number; color: string } & object = - { - ...this.itemTitleNormalStyle, - fontSize: 20, - color: '#666', - ...this.props.itemTitleSelectedStyle, - }; - private cursorStyle = { + }); + private itemTitleSelectedStyle: () => { + fontSize: number; + color: string; + } & object = () => ({ + ...this.itemTitleNormalStyle(), + fontSize: 20, + color: '#666', + ...this.props.itemTitleSelectedStyle, + }); + private cursorStyle = () => ({ position: 'absolute', bottom: 0, height: 1 / PixelRatio.get(), backgroundColor: '#999', ...this.props.cursorStyle, - }; + }); private scrollView: RefObject = React.createRef(); private cursor: RefObject = React.createRef(); @@ -95,20 +102,36 @@ export default class PageHeaderView extends Component { private addScrollPageIndexListener = () => { this.scrollPageIndexValue.addListener(({ value }) => { - if (Math.abs(this.nextScrollPageIndex - value) <= 0.01) { + const scrollIsStop = Math.floor(value) === value; + if ( + this.props.shouldSelectedPageIndex && + this.nextScrollPageIndex === -1 && + scrollIsStop && + value !== this.scrollPageIndex && + !this.props.shouldSelectedPageIndex(value) + ) { + this.props?.onSelectedPageIndex?.(this.scrollPageIndex); + this.scrollPageIndex = value; + return; + } + if ( + this.nextScrollPageIndex >= 0 && + Math.abs(this.nextScrollPageIndex - value) <= 0.01 + ) { + this.nextScrollPageIndex = -1; this.itemConfigList.map((item, _index) => { item._animtedEnabledValue.setValue(1); }); } const newPageIndex = Math.round(value); - if (newPageIndex != this.scrollPageIndex) { + if (newPageIndex != this.scrollPageIndex && scrollIsStop) { const itemLayout = this?.cursor?.current?.state ?.itemContainerLayoutList?.[newPageIndex] ?? { x: 0, width: 0 }; this?.scrollView?.current?.scrollTo({ x: itemLayout.x - itemLayout.width, }); + this.scrollPageIndex = newPageIndex; } - this.scrollPageIndex = newPageIndex; }); }; @@ -138,9 +161,9 @@ export default class PageHeaderView extends Component { private _itemTitleProps = (item: any, index: number) => { let fontScale = 1 + - (this.itemTitleSelectedStyle.fontSize - - this.itemTitleNormalStyle.fontSize) / - this.itemTitleNormalStyle.fontSize; + (this.itemTitleSelectedStyle().fontSize - + this.itemTitleNormalStyle().fontSize) / + this.itemTitleNormalStyle().fontSize; if (fontScale === 1) { fontScale = 1.0001; } @@ -155,10 +178,10 @@ export default class PageHeaderView extends Component { Animated.multiply(scale, enabled), Animated.multiply(1, Animated.subtract(1, enabled)) ); - const normalColor = this.itemTitleNormalStyle.color; - const selectedColor = this.itemTitleSelectedStyle.color; + const normalColor = this.itemTitleNormalStyle().color; + const selectedColor = this.itemTitleSelectedStyle().color; return { - ...this.itemTitleNormalStyle, + ...this.itemTitleNormalStyle(), normalColor, selectedColor, selectedScale: fontScale, @@ -166,7 +189,7 @@ export default class PageHeaderView extends Component { ? this.props.titleFromItem(item, index) : item, style: { - ...this.itemTitleNormalStyle, + ...this.itemTitleNormalStyle(), transform: [{ scale }], }, }; @@ -178,7 +201,7 @@ export default class PageHeaderView extends Component { private _itemDidTouch = (_: any, index: number) => { if (this.props.shouldSelectedPageIndex) { - let result = this.props.shouldSelectedPageIndex(); + const result = this.props.shouldSelectedPageIndex(index); if (result === false) { return; } @@ -208,10 +231,8 @@ export default class PageHeaderView extends Component { return ( - this?.cursor?.current?.onLayoutItemContainer(event, item, index) - } + ref={this?.itemConfigList?.[index]?._containerRef} + style={this.itemContainerStyle()} onPress={() => this._itemDidTouch(item, index)} > {content} @@ -226,15 +247,17 @@ export default class PageHeaderView extends Component { data={this.props.data} scrollPageIndexValue={this.scrollPageIndexValue} renderCursor={this.props.renderCursor} - cursorStyle={this.cursorStyle} + cursorStyle={this.cursorStyle()} /> ); }; override shouldComponentUpdate(nextProps: PageHeaderViewProps) { if (nextProps.data !== this.props.data) { + this.nextScrollPageIndex = -1; this.itemConfigList = nextProps.data.map((_: any) => ({ _animtedEnabledValue: new Animated.Value(1), + _containerRef: React.createRef(), })); return true; } @@ -245,9 +268,16 @@ export default class PageHeaderView extends Component { return ( { + this?.cursor?.current?.reloadItemListContainerLayout( + this.itemConfigList.map((item) => item._containerRef), + this.scrollView as any + ); + this?.props?.onContentSizeChange?.(width, height); + }} {...this.props} contentContainerStyle={[ - { width: this.itemContainerStyle?.flex ? '100%' : null }, + { width: this.itemContainerStyle()?.flex ? '100%' : null }, this.props.scrollContainerStyle, ]} ref={this.scrollView} diff --git a/yarn.lock b/yarn.lock index ebbc9a9..f3b448c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2573,6 +2573,37 @@ __metadata: languageName: node linkType: hard +"@onekeyfe/react-native-tab-page-view@workspace:.": + version: 0.0.0-use.local + resolution: "@onekeyfe/react-native-tab-page-view@workspace:." + dependencies: + "@commitlint/config-conventional": ^17.0.2 + "@evilmartians/lefthook": ^1.5.0 + "@react-native/eslint-config": ^0.72.2 + "@release-it/conventional-changelog": ^5.0.0 + "@types/jest": ^28.1.2 + "@types/react": ~17.0.21 + "@types/react-native": 0.70.0 + commitlint: ^17.0.2 + del-cli: ^5.0.0 + eslint: ^8.4.1 + eslint-config-prettier: ^8.5.0 + eslint-plugin-prettier: ^4.0.0 + jest: 29.7.0 + pod-install: ^0.1.0 + prettier: ^2.0.5 + react: 18.2.0 + react-native: 0.72.5 + react-native-builder-bob: ^0.20.0 + release-it: ^15.0.0 + turbo: ^1.10.7 + typescript: ^5.0.2 + peerDependencies: + react: "*" + react-native: "*" + languageName: unknown + linkType: soft + "@pkgjs/parseargs@npm:^0.11.0": version: 0.11.0 resolution: "@pkgjs/parseargs@npm:0.11.0" @@ -10825,37 +10856,6 @@ __metadata: languageName: unknown linkType: soft -"react-native-tab-page-view@workspace:.": - version: 0.0.0-use.local - resolution: "react-native-tab-page-view@workspace:." - dependencies: - "@commitlint/config-conventional": ^17.0.2 - "@evilmartians/lefthook": ^1.5.0 - "@react-native/eslint-config": ^0.72.2 - "@release-it/conventional-changelog": ^5.0.0 - "@types/jest": ^28.1.2 - "@types/react": ~17.0.21 - "@types/react-native": 0.70.0 - commitlint: ^17.0.2 - del-cli: ^5.0.0 - eslint: ^8.4.1 - eslint-config-prettier: ^8.5.0 - eslint-plugin-prettier: ^4.0.0 - jest: 29.7.0 - pod-install: ^0.1.0 - prettier: ^2.0.5 - react: 18.2.0 - react-native: 0.72.5 - react-native-builder-bob: ^0.20.0 - release-it: ^15.0.0 - turbo: ^1.10.7 - typescript: ^5.0.2 - peerDependencies: - react: "*" - react-native: "*" - languageName: unknown - linkType: soft - "react-native@npm:0.72.5": version: 0.72.5 resolution: "react-native@npm:0.72.5"