From ea2ae3efc1fa597d307ae1b0cbe2df15caf5546f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E5=9D=A4?= Date: Wed, 23 Feb 2022 16:46:13 +0800 Subject: [PATCH] feat: refactor Tabs (#1218) --- components/carousel/index.tsx | 19 ++-- components/tabs/DefaultTabBar.tsx | 151 ++++++++++++++-------------- components/tabs/Tabs.tsx | 159 ++++++++++++------------------ components/tabs/demo/basic.tsx | 8 +- 4 files changed, 155 insertions(+), 182 deletions(-) diff --git a/components/carousel/index.tsx b/components/carousel/index.tsx index 40623c2f..3cd22ed6 100644 --- a/components/carousel/index.tsx +++ b/components/carousel/index.tsx @@ -118,7 +118,7 @@ class Carousel extends React.PureComponent { height: 0, isScrolling: false, selectedIndex: index, - afterSelectedIndex: 0, + afterSelectedIndex: -1, offset: { x: 0, y: 0 }, } } @@ -357,8 +357,9 @@ class Carousel extends React.PureComponent { /** * go to index * @param index + * @param animated */ - public goTo(index: number) { + public goTo(index: number, animated?: boolean) { const { width, height } = this.state const count = this.props.infinite ? this.count - 1 : this.count @@ -370,11 +371,15 @@ class Carousel extends React.PureComponent { ) } - this.scrollview?.current?.scrollTo( - this.props.vertical - ? { x: 0, y: (index + (this.props.infinite ? 1 : 0)) * height } - : { x: (index + (this.props.infinite ? 1 : 0)) * width, y: 0 }, - ) + this.scrollview?.current?.scrollTo({ + x: this.props.vertical + ? 0 + : (index + (this.props.infinite ? 1 : 0)) * width, + y: this.props.vertical + ? (index + (this.props.infinite ? 1 : 0)) * height + : 0, + animated, + }) } render() { diff --git a/components/tabs/DefaultTabBar.tsx b/components/tabs/DefaultTabBar.tsx index 2fcfe9c0..3e9267a7 100644 --- a/components/tabs/DefaultTabBar.tsx +++ b/components/tabs/DefaultTabBar.tsx @@ -23,8 +23,6 @@ export interface PropsType scrollValue?: any tabStyle?: ViewStyle tabsContainerStyle?: ViewStyle - /** default: false */ - dynamicTabUnderlineWidth?: boolean keyboardShouldPersistTaps?: boolean } @@ -46,13 +44,13 @@ export class DefaultTabBar extends React.PureComponent { tabBarActiveTextColor: '', tabBarInactiveTextColor: '', tabBarTextStyle: {}, - dynamicTabUnderlineWidth: false, } _tabsMeasurements: any[] = [] _tabContainerMeasurements: any _containerMeasurements: any _scrollView: ScrollView + _newLineLeft: number constructor(props: PropsType) { super(props) @@ -115,40 +113,36 @@ export class DefaultTabBar extends React.PureComponent { newScrollX = newScrollX >= 0 ? newScrollX : 0 if (Platform.OS === 'android') { - this._scrollView.scrollTo({ x: newScrollX, y: 0, animated: false }) + this._scrollView?.scrollTo({ x: newScrollX, y: 0 }) } else { const rightBoundScroll = this._tabContainerMeasurements.width - this._containerMeasurements.width newScrollX = newScrollX > rightBoundScroll ? rightBoundScroll : newScrollX - this._scrollView.scrollTo({ x: newScrollX, y: 0, animated: false }) + this._scrollView?.scrollTo({ x: newScrollX, y: 0 }) } } updateTabUnderline(position: number, pageOffset: number, tabCount: number) { - const { dynamicTabUnderlineWidth } = this.props - if (position >= 0 && position <= tabCount - 1) { - if (dynamicTabUnderlineWidth) { - const nowLeft = this._tabsMeasurements[position].left - const nowRight = this._tabsMeasurements[position].right - const nextTabLeft = this._tabsMeasurements[position + 1].left - const nextTabRight = this._tabsMeasurements[position + 1].right + const nowLeft = this._tabsMeasurements[position].left + const nowRight = this._tabsMeasurements[position].right + const nextTabLeft = this._tabsMeasurements[position + 1]?.left || 0 + const nextTabRight = this._tabsMeasurements[position + 1]?.right || 0 - const newLineLeft = - pageOffset * nextTabLeft + (1 - pageOffset) * nowLeft - const newLineRight = - pageOffset * nextTabRight + (1 - pageOffset) * nowRight + const newLineLeft = pageOffset * nextTabLeft + (1 - pageOffset) * nowLeft + const newLineRight = + pageOffset * nextTabRight + (1 - pageOffset) * nowRight - this.state._leftTabUnderline.setValue(newLineLeft) - this.state._widthTabUnderline.setValue(newLineRight - newLineLeft) - } else { - const nowLeft = (position * this.state._tabContainerWidth) / tabCount - const nextTabLeft = - ((position + 1) * this.state._tabContainerWidth) / tabCount - const newLineLeft = - pageOffset * nextTabLeft + (1 - pageOffset) * nowLeft - this.state._leftTabUnderline.setValue(newLineLeft) - } + if (this._newLineLeft === newLineLeft) return + this._newLineLeft = newLineLeft + Animated.timing(this.state._leftTabUnderline, { + toValue: newLineLeft, + useNativeDriver: false, + }).start() + Animated.timing(this.state._widthTabUnderline, { + toValue: newLineRight - newLineLeft, + useNativeDriver: false, + }).start() } } @@ -191,8 +185,8 @@ export class DefaultTabBar extends React.PureComponent { {renderTab ? ( renderTab(tab) @@ -221,45 +215,69 @@ export class DefaultTabBar extends React.PureComponent { this.updateView({ value: this.props.scrollValue._value }) } + getTabs = (styles: TabBarStyle, theme: Theme) => { + const { tabs, page = 0 } = this.props + return tabs.map((name, index) => { + let tab = { title: name } as TabData + if (tabs.length - 1 >= index) { + tab = tabs[index] + } + const tabWidth = this.state._containerWidth / Math.min(page, tabs.length) + + return this.renderTab( + tab, + index, + tabWidth, + this.measureTab.bind(this, index), + styles, + theme, + ) + }) + } + + getUnderLine = (styles: TabBarStyle) => { + const { tabBarUnderlineStyle, renderUnderline } = this.props + + const tabUnderlineStyle = { + position: 'absolute', + bottom: 0, + ...StyleSheet.flatten(styles.underline), + ...StyleSheet.flatten(tabBarUnderlineStyle), + } + + const dynamicTabUnderline = { + left: this.state._leftTabUnderline, + width: this.state._widthTabUnderline, + } + const underlineProps = { + style: { + ...dynamicTabUnderline, + ...tabUnderlineStyle, + }, + } + return renderUnderline ? ( + renderUnderline(underlineProps.style) + ) : ( + //@ts-ignore + + ) + } + render() { const { tabs, page = 0, - tabBarUnderlineStyle, tabBarBackgroundColor, tabsContainerStyle, - renderUnderline, keyboardShouldPersistTaps, } = this.props return ( {(styles, theme) => { - const tabUnderlineStyle = { - position: 'absolute', - bottom: 0, - ...StyleSheet.flatten(styles.underline), - ...StyleSheet.flatten(tabBarUnderlineStyle), - } - - const dynamicTabUnderline = { - left: this.state._leftTabUnderline, - width: this.state._widthTabUnderline, - } - - const tabWidth = - this.state._containerWidth / Math.min(page, tabs.length) - const underlineProps = { - style: { - ...dynamicTabUnderline, - ...tabUnderlineStyle, - }, - } - return ( { - {tabs.map((name, index) => { - let tab = { title: name } as TabData - if (tabs.length - 1 >= index) { - tab = tabs[index] - } - return this.renderTab( - tab, - index, - tabWidth, - this.measureTab.bind(this, index), - styles, - theme, - ) - })} - {renderUnderline ? ( - renderUnderline(underlineProps.style) - ) : ( - - )} + {this.getTabs(styles, theme)} + {this.getUnderLine(styles)} @@ -324,9 +322,6 @@ export class DefaultTabBar extends React.PureComponent { // width = WINDOW_WIDTH; // } this.setState({ _tabContainerWidth: width }) - if (!this.props.dynamicTabUnderlineWidth) { - this.state._widthTabUnderline.setValue(width / this.props.tabs.length) - } this.updateView({ value: this.props.scrollValue._value }) } diff --git a/components/tabs/Tabs.tsx b/components/tabs/Tabs.tsx index 0e0370af..6d27f7a7 100644 --- a/components/tabs/Tabs.tsx +++ b/components/tabs/Tabs.tsx @@ -1,12 +1,6 @@ import React from 'react' -import { - Animated, - Dimensions, - LayoutChangeEvent, - NativeScrollEvent, - NativeSyntheticEvent, -} from 'react-native' -import ViewPager from 'react-native-pager-view' +import { Animated, Dimensions, LayoutChangeEvent } from 'react-native' +import Carousel from '../carousel/index' import { WithTheme, WithThemeStyles } from '../style' import View from '../view' import { DefaultTabBar } from './DefaultTabBar' @@ -18,6 +12,8 @@ export interface StateType { scrollX: Animated.Value scrollValue: Animated.Value containerWidth: number + containerHeight: number + selectedIndex: number } let instanceId: number = 0 @@ -38,15 +34,12 @@ export class Tabs extends React.PureComponent { } static DefaultTabBar = DefaultTabBar - viewPager: ViewPager | null + carousel: Carousel | null protected instanceId: number protected prevCurrentTab: number protected tabCache: { [index: number]: React.ReactNode } = {} - /** compatible for different between react and preact in `setState`. */ - private nextCurrentTab: number - constructor(props: PropsType) { super(props) @@ -57,8 +50,9 @@ export class Tabs extends React.PureComponent { scrollX: new Animated.Value(pageIndex * width), scrollValue: new Animated.Value(pageIndex), containerWidth: width, + containerHeight: 0, + selectedIndex: 0, } - this.nextCurrentTab = this.state.currentTab this.instanceId = instanceId++ } @@ -72,7 +66,7 @@ export class Tabs extends React.PureComponent { renderContent = (getSubElements = this.getSubElements()) => { const { tabs, usePaged, destroyInactiveTab } = this.props - const { currentTab = 0, containerWidth = 0 } = this.state + const { containerHeight = 0, containerWidth = 0, currentTab } = this.state const content = tabs.map((tab, index) => { const key = tab.key || `tab_${index}` @@ -86,52 +80,47 @@ export class Tabs extends React.PureComponent { return ( + style={[ + { width: containerWidth }, + containerHeight ? { height: containerHeight } : { flex: 1 }, + ]}> {this.tabCache[index]} ) }) + return ( - { - this.state.scrollX.setValue( - e.nativeEvent.position * this.state.containerWidth, - ) + pagination={() => null} + selectedIndex={currentTab} + afterChange={(index: number) => { + this.setState({ currentTab: index }, () => { + this.state.scrollX.setValue(index * this.state.containerWidth) + }) }} - style={{ flex: 1 }} - onPageSelected={(e) => { - const index = e.nativeEvent.position - this.setState( - { - currentTab: index, - }, - () => { - // tslint:disable-next-line:no-unused-expression - this.props.onChange && this.props.onChange(tabs[index], index) - }, - ) - this.nextCurrentTab = index + scrollEnabled={this.props.swipeable || usePaged} + style={{ + height: containerHeight, + width: containerWidth, }} - ref={(ref) => (this.viewPager = ref)}> + ref={(ref) => (this.carousel = ref)}> {content} - + ) } - onMomentumScrollEnd = (e: NativeSyntheticEvent) => { - const offsetX = e.nativeEvent.contentOffset.x - const page = this.getOffsetIndex(offsetX, this.state.containerWidth) - if (this.state.currentTab !== page) { - this.goToTab(page) - } + // 在ScrollView下会拿不到正确高度 + // 所以先展示第一个拿到高度后再以第一个高度为基准渲染整个Carousel + handleLayout1 = (e: LayoutChangeEvent) => { + const { height } = e.nativeEvent.layout + requestAnimationFrame(() => { + this.setState({ containerHeight: height }) + }) } - handleLayout = (e: LayoutChangeEvent) => { + handleLayout2 = (e: LayoutChangeEvent) => { const { width } = e.nativeEvent.layout requestAnimationFrame(() => { this.scrollTo(this.state.currentTab, false) @@ -142,22 +131,19 @@ export class Tabs extends React.PureComponent { } scrollTo = (index: number, animated = true) => { - if (this.viewPager) { - if (animated) { - this.viewPager.setPage(index) - } else { - this.viewPager.setPageWithoutAnimation(index) - } - return + if (this.carousel) { + this.carousel.goTo(index, animated) } } render() { - const { tabBarPosition, noRenderContent, keyboardShouldPersistTaps } = - this.props - const { scrollX, scrollValue, containerWidth } = this.state - // let overlayTabs = (this.props.tabBarPosition === 'overlayTop' || this.props.tabBarPosition === 'overlayBottom'); - const overlayTabs = false + const { + children, + tabBarPosition, + noRenderContent, + keyboardShouldPersistTaps, + } = this.props + const { scrollX, scrollValue, containerWidth, containerHeight } = this.state const tabBarProps = { ...this.getTabBarBaseProps(), @@ -168,14 +154,6 @@ export class Tabs extends React.PureComponent { containerWidth: containerWidth, } - if (overlayTabs) { - // tabBarProps.style = { - // position: 'absolute', - // left: 0, - // right: 0, - // [this.props.tabBarPosition === 'overlayTop' ? 'top' : 'bottom']: 0, - // }; - } return ( {(styles) => { @@ -192,10 +170,24 @@ export class Tabs extends React.PureComponent { !noRenderContent && this.renderContent(), ] + if (containerHeight === 0 && Array.isArray(children)) { + return ( + + {React.Children.toArray(children)[0]} + + ) + } + return ( + onLayout={this.handleLayout2}> {tabBarPosition === 'top' ? content : content.reverse()} ) @@ -235,7 +227,7 @@ export class Tabs extends React.PureComponent { UNSAFE_componentWillReceiveProps(nextProps: PropsType) { if (this.props.page !== nextProps.page && nextProps.page !== undefined) { - this.goToTab(this.getTabIndex(nextProps), true) + this.goToTab(this.getTabIndex(nextProps)) } } @@ -261,31 +253,11 @@ export class Tabs extends React.PureComponent { } } - goToTab(index: number, force = false, newState: any = {}) { - if (!force && this.nextCurrentTab === index) { - return false + goToTab(index: number) { + if (this.carousel) { + this.carousel.goTo(index) } - this.nextCurrentTab = index - const { tabs, onChange } = this.props as PropsType - if (index >= 0 && index < tabs.length) { - if (!force) { - // tslint:disable-next-line:no-unused-expression - onChange && onChange(tabs[index], index) - } - this.setState( - { - currentTab: index, - ...newState, - }, - () => { - requestAnimationFrame(() => { - this.scrollTo(this.state.currentTab, this.props.animated) - }) - }, - ) - } - - return true + this.state.scrollX.setValue(index * this.state.containerWidth) } tabClickGoToTab(index: number) { @@ -326,7 +298,6 @@ export class Tabs extends React.PureComponent { } } - // tslint:disable-next-line:no-shadowed-variable renderTabBar(tabBarProps: any, DefaultTabBar: React.ComponentClass) { const { renderTabBar } = this.props if (renderTabBar === false) { @@ -342,7 +313,7 @@ export class Tabs extends React.PureComponent { const { children } = this.props const subElements: { [key: string]: React.ReactNode } = {} - return (defaultPrefix: string = '$i$-', allPrefix: string = '$ALL$') => { + return (defaultPrefix: string = '$i$-') => { if (Array.isArray(children)) { children.forEach((child: any, index) => { if (child.key) { @@ -350,8 +321,6 @@ export class Tabs extends React.PureComponent { } subElements[`${defaultPrefix}${index}`] = child }) - } else if (children) { - subElements[allPrefix] = children } return subElements } diff --git a/components/tabs/demo/basic.tsx b/components/tabs/demo/basic.tsx index e4f81ed6..7f54b78f 100644 --- a/components/tabs/demo/basic.tsx +++ b/components/tabs/demo/basic.tsx @@ -20,7 +20,11 @@ const renderContent = (tab: any, index: any) => { ) }) - return {content} + return ( + + {content} + + ) } export default class BasicTabsExample extends React.Component { @@ -109,7 +113,7 @@ export default class BasicTabsExample extends React.Component { - {renderContent} + {tabs2.map((tab, index) => renderContent(tab, index))}