react-component / m-tabs

React Mobile Tabs Component (web & react-native)
http://react-component.github.io/m-tabs/
143 stars 47 forks source link

关于defaultTabBar长度的可扩展性问题 #23

Open Jemair opened 6 years ago

Jemair commented 6 years ago

今天在开发的时候遇到这样一个问题
设计稿中tabBarItem每个长度为页面的1/6 当不足6个时不自动填满宽度而是将右侧留白 效果如下 image

但是在defaultTabBar中 是使用js动态添加行内样式到组件上 导致标签样式难以覆盖 同时使用!important强行覆盖之后 由于underline宽度和位置也是写死的 导致underline长度和位置都无法与tab标签对齐

期望增加tabWidth属性 可以自定义设置tab宽度 同时使用offsetLeft动态计算underline位置 可以增强组件的扩展性 自定义tabBar代码如下 (当tab为垂直时使用offsetTop)

export default class TabBar extends PureComponent {
  render() {
    return (
      <div className={s.tabBar}>
        {this.renderTabs()}
        {this.renderUnderline()}
      </div>
    )
  }

  renderTabs = () => {
    const { tabs, activeTab, goToTab } = this.props
    return tabs.map((i, index) =>
      <div
        key={i.key}
        ref={el => { this[index] = el }}
        onClick={() => { goToTab(index) }}
        // .tab { width: 16% }
        className={`${s.tab} ${activeTab === index ? s.active : ''}`}>
        {i.title}
      </div>
    )
  }

  renderUnderline = () => {
    const { activeTab } = this.props
    const offsetLeft = (this[activeTab] && this[activeTab].offsetLeft) || 0
    const offset = {
      transform: `translateX(${offsetLeft}px)`,
      WebkitTransform: `translateX(${offsetLeft}px)`,
    }
    // .underline 宽度默认与.tab一致 但通过覆盖样式也可以自定义宽度
    return <div className={s.underline} style={offset} />
  }
}
nicetu commented 6 years ago

自定义defaultTabBar如下:

import {
    default as RN,
    Animated,
    Dimensions,
    Platform,
    ScrollView,
    Text,
    TouchableOpacity,
    View,
} from 'react-native';
import { TabBarPropsType } from 'rmc-tabs/lib/PropsType';
import { Models } from 'rmc-tabs/lib/Models';
import defaultStyles from 'rmc-tabs/lib/Styles.native';
import React from 'react';

const WINDOW_WIDTH = Dimensions.get('window').width;

export interface PropsType extends TabBarPropsType {
    scrollValue?: any;
    styles?: typeof defaultStyles;
    tabStyle?: RN.ViewStyle;
    tabsContainerStyle?: RN.ViewStyle;
    /** default: false */
    dynamicTabUnderlineWidth?: boolean;
    keyboardShouldPersistTaps?: boolean;
    tabBarUnderLineWidth:number;
}

export interface StateType {
    _leftTabUnderline: Animated.Value;
    _widthTabUnderline: Animated.Value;
    _containerWidth: number;
    _tabContainerWidth: number;
}
export class DefaultTabBar extends React.PureComponent<PropsType, StateType> {
    static defaultProps = {
        animated: true,
        tabs: [],
        goToTab: () => { },
        activeTab: 0,
        page: 5,
        tabBarUnderlineStyle: {},
        tabBarBackgroundColor: '#fff',
        tabBarActiveTextColor: '',
        tabBarInactiveTextColor: '',
        tabBarTextStyle: {},
        dynamicTabUnderlineWidth: false,
        styles: defaultStyles,
        tabBarUnderLineWidth:50,
    } as PropsType;

    _tabsMeasurements: any[] = [];
    _tabContainerMeasurements: any;
    _containerMeasurements: any;
    _scrollView: any;

    constructor(props: PropsType) {
        super(props);
        this.state = {
            _leftTabUnderline: new Animated.Value(0),
            _widthTabUnderline: new Animated.Value(0),
            _containerWidth: this.props.tabBarUnderLineWidth,
            _tabContainerWidth: WINDOW_WIDTH,
        };
    }

    componentDidMount() {
        this.props.scrollValue && this.props.scrollValue.addListener(this.updateView);
    }

    updateView = (offset: any) => {
        const position = Math.floor(offset.value);
        const pageOffset = offset.value % 1;
        const tabCount = this.props.tabs.length;
        const lastTabPosition = tabCount - 1;

        if (tabCount === 0 || offset.value < 0 || offset.value > lastTabPosition) {
            return;
        }

        if (this.necessarilyMeasurementsCompleted(position, position === lastTabPosition)) {
            this.updateTabPanel(position, pageOffset);
            this.updateTabUnderline(position, pageOffset, tabCount);
        }
    }

    necessarilyMeasurementsCompleted(position: number, isLastTab: boolean) {
        return this._tabsMeasurements[position] &&
            (isLastTab || this._tabsMeasurements[position + 1]) &&
            this._tabContainerMeasurements &&
            this._containerMeasurements;
    }

    updateTabPanel(position: number, pageOffset: number) {
        const containerWidth = this._containerMeasurements.width;
        const tabWidth = this._tabsMeasurements[position].width;
        const nextTabMeasurements = this._tabsMeasurements[position + 1];
        const nextTabWidth = nextTabMeasurements && nextTabMeasurements.width || 0;
        const tabOffset = this._tabsMeasurements[position].left;
        const absolutePageOffset = pageOffset * tabWidth;
        let newScrollX = tabOffset + absolutePageOffset;

        newScrollX -= (containerWidth - (1 - pageOffset) * tabWidth - pageOffset * nextTabWidth) / 2;
        newScrollX = newScrollX >= 0 ? newScrollX : 0;

        if (Platform.OS === 'android') {
            this._scrollView.scrollTo({ x: newScrollX, y: 0, animated: false, });
        } else {
            const rightBoundScroll = this._tabContainerMeasurements.width - (this._containerMeasurements.width);
            newScrollX = newScrollX > rightBoundScroll ? rightBoundScroll : newScrollX;
            this._scrollView.scrollTo({ x: newScrollX, y: 0, animated: false, });
        }
    }

    updateTabUnderline(position: number, pageOffset: number, tabCount: number) {
        const { dynamicTabUnderlineWidth } = this.props;

        if (0 <= position && 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 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);
            }
        }
    }

    onPress = (index: number) => {
        const { goToTab, onTabClick, tabs } = this.props;
        onTabClick && onTabClick(tabs[index], index);
        goToTab && goToTab(index);
    }

    renderTab(tab: Models.TabData, index: number, width: number, onLayoutHandler: any) {
        const {
            tabBarActiveTextColor: activeTextColor,
            tabBarInactiveTextColor: inactiveTextColor,
            tabBarTextStyle: textStyle,
            activeTab, renderTab,
            styles = defaultStyles
        } = this.props;
        const isTabActive = activeTab === index;
        const textColor = isTabActive ?
            (activeTextColor || styles.TabBar.activeTextColor) :
            (inactiveTextColor || styles.TabBar.inactiveTextColor);

        return <TouchableOpacity
            activeOpacity={1}
            key={`${tab.title}_${index}`}
            accessible={true}
            accessibilityTraits="button"
            onPress={() => this.onPress(index)}
            onLayout={onLayoutHandler}
        >
            <View style={{
                ...styles.TabBar.tab,
                ...this.props.tabStyle,
                width,
            }}>
                {
                    renderTab ? renderTab(tab) :
                        <Text style={{
                            color: textColor,
                            ...styles.TabBar.textStyle,
                            ...textStyle
                        }}>
                            {tab.title}
                        </Text>
                }
            </View>
        </TouchableOpacity>;
    }

    measureTab = (page: number, event: any) => {
        const { x, width, height, } = event.nativeEvent.layout;
        this._tabsMeasurements[page] = { left: x, right: x + width, width, height };
        this.updateView({ value: this.props.scrollValue._value });
    }

    render() {
        const {
            tabs, page = 1,
            tabBarUnderlineStyle,
            tabBarBackgroundColor,
            styles = defaultStyles,
            tabsContainerStyle,
            renderUnderline,
            keyboardShouldPersistTaps,
        } = this.props;

        const tabUnderlineStyle = {
            position: 'absolute',
            bottom: 0,
            ...styles.TabBar.underline,
            ...tabBarUnderlineStyle,
        };

        const tabWidth = this.state._containerWidth / Math.min(page, tabs.length);

        const dynamicTabUnderline = {
            left: this.state._leftTabUnderline,
            width: this.state._widthTabUnderline,
            marginLeft:(tabWidth - this.props.tabBarUnderLineWidth)/2
        };

        const underlineProps = {
            style: {
                ...tabUnderlineStyle,
                ...dynamicTabUnderline,
            }
        };

        return <View
            style={{
                ...styles.TabBar.container,
                backgroundColor: tabBarBackgroundColor,
            }}
            onLayout={this.onContainerLayout}
        >
            <ScrollView
                ref={(scrollView: any) => { this._scrollView = scrollView; }}
                horizontal={true}
                showsHorizontalScrollIndicator={false}
                showsVerticalScrollIndicator={false}
                directionalLockEnabled={true}
                bounces={false}
                scrollsToTop={false}
                scrollEnabled={tabs.length > page}
                keyboardShouldPersistTaps={keyboardShouldPersistTaps}
            >
                <View
                    style={{
                        ...styles.TabBar.tabs,
                        ...tabsContainerStyle,
                        backgroundColor: tabBarBackgroundColor,
                    }}
                    onLayout={this.onTabContainerLayout}
                >
                    {
                        tabs.map((name, index) => {
                            let tab = { title: name } as Models.TabData;
                            if (tabs.length - 1 >= index) {
                                tab = tabs[index];
                            }
                            return this.renderTab(tab, index, tabWidth, this.measureTab.bind(this, index));
                        })
                    }
                    {
                        renderUnderline ? renderUnderline(underlineProps) :
                            <Animated.View {...underlineProps} />
                    }
                </View>
            </ScrollView>
        </View>;
    }

    onTabContainerLayout = (e: RN.LayoutChangeEvent) => {
        this._tabContainerMeasurements = e.nativeEvent.layout;
        let width = this._tabContainerMeasurements.width;
        if (width < WINDOW_WIDTH) {
            width = WINDOW_WIDTH;
        }
        this.setState({ _tabContainerWidth: width, });
        if (!this.props.dynamicTabUnderlineWidth) {
            this.state._widthTabUnderline.setValue(this.props.tabBarUnderLineWidth);
        }
        this.updateView({ value: this.props.scrollValue._value, });
    }

    onContainerLayout = (e: RN.LayoutChangeEvent) => {
        this._containerMeasurements = e.nativeEvent.layout;
        this.setState({ _containerWidth: this._containerMeasurements.width, });
        this.updateView({ value: this.props.scrollValue._value, });
    }
}

引用如下:

<Tabs               
       renderTabBar={(props) => <BaseTabBar {...props} tabBarUnderLineWidth={this.props.tabBarUnderLineWidth?this.props.tabBarUnderLineWidth:54}/>}
        ></Tabs>