React Native带你实现scrollable-tab-view(五)

上一节React Native带你实现scrollable-tab-view(三)中我们最后实现了我们scrollable-tab-view的效果为:
这里写图片描述

比如有很多页面,我们的tabview需要跟随scrollview的滑动而滑动:

这里写图片描述

我们现在用的还是默认的tabview,比如我们多加一些页面,
这里写图片描述
这样肯定不行,我们需要的是上面的那种效果,所以每个view的flex=1这种模式肯定不适合了,我们需要用一个scrollview去包涵tab,使它也能滑动,所以也算是scrollable-tab-view的一个难点了,好啦~ 我们来一步一步实现一下scrollable-tab-view中的ScrollableTabBar.js。

我们先创建一个view叫ScrollableTabBar.js:

/**
 * @author YASIN
 * @version [React-Native Pactera V01, 2017/9/7]
 * @date 17/2/23
 * @description ScrollableTabBar
 */
import React, {
    Component
} from 'react';
import {
    View,
    Text,
    StyleSheet,
} from 'react-native';
export default class ScrollableTabBar extends Component {
    // 构造
    constructor(props) {
        super(props);
        // 初始状态
        this.state = {};
    }

    render() {
        return (
            <View>
                <Text>ScrollableTabBar</Text>
            </View>
        );
    }
}
const styles = StyleSheet.create({
    container: {}
});

然后替换掉index文件中的DefaultTabBar组件:

  /**
     * 渲染tabview
     * @private
     */
    _renderTabView() {
        let tabParams = {
            tabs: this._children().map((child)=>child.props.tabLabel),
            activeTab: this.state.currentPage,
            scrollValue: this.state.scrollValue,
            containerWidth: this.state.containerWidth,
        };
        return (
            <ScrollableTabBar
                {...tabParams}
                style={[{width: this.state.containerWidth}]}
                onTabClick={(page)=>this.goToPage(page)}
            />
        );
    }

这里写图片描述

然后我们开始实现ScrollableTabBar组件:

我们用一个scrollview去包裹tab,然后其它的东西直接copy一份DefaultTabBar的内容:

/**
 * @author YASIN
 * @version [React-Native Pactera V01, 2017/9/7]
 * @date 17/2/23
 * @description ScrollableTabBar
 */
import React, {
    Component
} from 'react';
import {
    View,
    Text,
    StyleSheet,
    Dimensions,
    TouchableOpacity,
    ScrollView,
    Animated,
} from 'react-native';
const screenW = Dimensions.get('window').width;
const screenH = Dimensions.get('window').height;
export default class ScrollableTabBar extends Component {
    // 构造
    constructor(props) {
        super(props);
        // 初始状态
        this.state = {};
    }

    render() {
        let {containerWidth, tabs, scrollValue}=this.props;
        //给传过来的动画一个插值器
        const left = scrollValue.interpolate({
            inputRange: [0, 1,], outputRange: [0, containerWidth / tabs.length,],
        });
        let tabStyle = {
            width: 50,
            position: 'absolute',
            bottom: 0,
            left,
        };
        return (
            <View style={[styles.container, this.props.style]}>
                <ScrollView
                    automaticallyAdjustContentInsets={false}
                    ref={(scrollView) => {
                        this._scrollView = scrollView;
                    }}
                    horizontal={true}
                    showsHorizontalScrollIndicator={false}
                    showsVerticalScrollIndicator={false}
                    directionalLockEnabled={true}
                    bounces={false}
                    scrollsToTop={false}
                >
                    <View
                        style={styles.tabContainer}
                    >
                        {tabs.map((tab, index)=> {
                            return this._renderTab(tab, index, index === this.props.activeTab);
                        })}
                        <Animated.View
                            style={[styles.tabLineStyle, tabStyle]}
                        />
                    </View>
                </ScrollView>
            </View>
        );
    }

    /**
     * 渲染tab
     * @param name 名字
     * @param page 下标
     * @param isTabActive 是否是选中的tab
     * @private
     */
    _renderTab(name, page, isTabActive) {
        let tabTextStyle = null;
        //如果被选中的style
        if (isTabActive) {
            tabTextStyle = {
                color: 'green'
            };
        } else {
            tabTextStyle = {
                color: 'red'
            };
        }
        let self = this;
        return (
            <TouchableOpacity
                key={name + page}
                style={[styles.tabStyle]}
                onPress={()=>this.props.onTabClick(page)}
            >
                <Text style={[tabTextStyle]}>{name}</Text>
            </TouchableOpacity>
        );
    }
}
const styles = StyleSheet.create({
    container: {
        width: screenW,
        height: 50,
    },
    tabContainer: {
        flexDirection: 'row',
        alignItems: 'center',
    },
    tabLineStyle: {
        height: 1,
        backgroundColor: 'navy',
    },
    tabStyle: {
        height: 49,
        alignItems: 'center',
        justifyContent: 'center',
        paddingHorizontal: 20,
    },
});

然后我们运行代码:
这里写图片描述

可以看到,有点那种感觉了,但是我们目前看到的东西需要解决的就是:
1、底部那个线条长度计算(目前是定死了一个值)

 render() {
       ....
        let tabStyle = {
            width: 50,
            position: 'absolute',
            bottom: 0,
            left,
        };
        ...

我们线条的长度应该等于当前tab的宽度。

2、我们页面滑动的时候,线条指向有问题。

好了~ 我们一个一个的解决,我们把下标线view的left跟宽度用两个单独的动画控制,然后监听内容scrollview的scrollValue动画滑动改变控制left和控制width的动画值:

export default class ScrollableTabBar extends Component {
    // 构造
    constructor(props) {
        super(props);
        // 初始状态
        this.state = {
            _leftTabUnderline: new Animated.Value(0),
            _widthTabUnderline: new Animated.Value(0),
        };
    }

    render() {
        let {containerWidth, tabs, scrollValue}=this.props;
        //给传过来的动画一个插值器
        let tabStyle = {
            width: this.state._widthTabUnderline,
            position: 'absolute',
            bottom: 0,
            left: this.state._leftTabUnderline,
        };
        return (
            <View style={[styles.container, this.props.style]}>
                <ScrollView
                    ....
                >
                    <View
                        style={styles.tabContainer}
                    >
                        ...
                        <Animated.View
                            style={[styles.tabLineStyle, tabStyle]}
                        />
                    </View>
                </ScrollView>
            </View>
        );
    }

然后跟前一节我们实现的DefaultTabBar.js一样,监听scrollValue动画:

componentDidMount() {
        this.props.scrollValue.addListener(this._updateView);
    }
 /**
     * 根据scrollview的滑动改变底部线条的宽度跟left
     * @param value
     * @private
     */
    _updateView = ({value = 0}) => {
        //因为value 的值是[0-1-2-3-4]变换
        const position = Math.floor(value);
        //取小数部分
        const offset = value % 1;
        const tabCount = this.props.tabs.length;
        const lastTabPosition = tabCount - 1;
        //如果没有tab||(有bounce效果)直接return
        if (tabCount === 0 || value < 0 || value > lastTabPosition) {
            return;
        }
        console.log('position==>' + position + ' offset==>' + offset);
    }

我们运行然后从第一页滑动到第二页:
这里写图片描述

可以看到,我们position是从(0-1)而offset为0–>1的一个小数,所以我们:
1、底部线条view的宽度应该为(第position页tab宽度)*(1-offset)+(第positon+1页tab的宽度)*offset。
2、底部线条view的left应该为(第position页tab的x轴)+(第position+1页tab的宽度)*offset。

所以我们接下来要做的就是通过o nLayout方法计算出每个tab的x轴跟宽度然后保存起来。

我们在渲染tabbar的时候提供view o nLayout方法去测量view:

 /**
     * 渲染tab
     * @param name 名字
     * @param page 下标
     * @param isTabActive 是否是选中的tab
     * @private
     */
    _renderTab(name, page, isTabActive) {
        ....
        let self = this;
        return (
            <TouchableOpacity
               .....
                onLayout={(event)=>this._onMeasureTab(page, event)}
            >
                <Text style={[tabTextStyle]}>{name}</Text>
            </TouchableOpacity>
        );
    }

每次测量完毕后去更新tab底部线view的宽度:

/**
     * 测量tabview
     * @param page 页面下标
     * @param event 事件
     * @private
     */
    _onMeasureTab(page, event) {
        let {nativeEvent:{layout:{x, width}}}=event;
        this._tabsMeasurements[page] = {left: x, right: width + x, width: width};
        this._updateView({value: this.props.scrollValue._value})
    }

按照我们前面所说的“1、底部线条view的宽度应该为(第position页tab宽度)*(1-offset)+(第positon+1页tab的宽度)*offset。”所以我们必须要拿到tab的测量值,并且有下一个tab,或者特殊情况就是到最后一个tab了,最后才去做改变width的操作。

 /**
     * 根据scrollview的滑动改变底部线条的宽度跟left
     * @param value
     * @private
     */
    _updateView = ({value = 0}) => {
        //因为value 的值是[0-1-2-3-4]变换
        const position = Math.floor(value);
        //取小数部分
        const offset = value % 1;
        const tabCount = this.props.tabs.length;
        const lastTabPosition = tabCount - 1;
        //如果没有tab||(有bounce效果)直接return
        if (tabCount === 0 || value < 0 || value > lastTabPosition) {
            return;
        }
        if (this._necessarilyMeasurementsCompleted(position, position === tabCount - 1)) {
            this._updateTabLine(position, offset);
        }
    }
  /**
     * 判断是否需要跟新的条件是否初始化
     * @param position
     * @param isLast 是否是最后一个
     * @private
     */
    _necessarilyMeasurementsCompleted(position, isLast) {
        return (
            this._tabsMeasurements[position] &&
            (isLast || this._tabsMeasurements[position + 1])
        );
    }

然后套去我们前面说的公式:

(第position页tab宽度)*(1-offset)+(第positon+1页tab的宽度)*offset
但是有一种特殊情况,就是position+1不存在,所以我们要做好判断:

 /**
     * 更新底部线view的left跟width
     * @param position
     * @param offset
     * @private
     */
    _updateTabLine(position, offset) {
        //当前tab的测量值
        const currMeasure = this._tabsMeasurements[position];
        //position+1的tab的测量值
        const nextMeasure = this._tabsMeasurements[position + 1];
        let width = currMeasure.width * (1 - offset);
        if (nextMeasure) {
            width += nextMeasure.width * offset;
        }
        this.state._widthTabUnderline.setValue(width);
    }

然后我们运行代码:
这里写图片描述

可以看到,我们滑动的时候tab底部线条的宽度再改变,宽度=我们的tab的宽度,好啦!! 我们接着只要使线条view的left跟着scrollview滑动而改变到对应的tab位置就可以了。

我们继续套入公式:

(第position页tab的x轴)+(第position+1页tab的宽度)*offset

/**
     * 更新底部线view的left跟width
     * @param position
     * @param offset
     * @private
     */
    _updateTabLine(position, offset) {
        //当前tab的测量值
        const currMeasure = this._tabsMeasurements[position];
        //position+1的tab的测量值
        const nextMeasure = this._tabsMeasurements[position + 1];
        let width = currMeasure.width * (1 - offset);
        let left= currMeasure.left;
        if (nextMeasure) {
            width += nextMeasure.width * offset;
            left+=nextMeasure.width*offset;
        }
        this.state._leftTabUnderline.setValue(left);
        this.state._widthTabUnderline.setValue(width);
    }

然后运行代码:

这里写图片描述

好啦! 可以看到,我们底部线条view可以跟随了,到这我们还差最后一步,那就是当我们点击tab的时候如果后面的tab被遮住了就得向后移动。

有点晚了,该洗洗睡啦~~

欢迎入群,欢迎交流,大牛勿喷,下一节见!

猜你喜欢

转载自blog.csdn.net/vv_bug/article/details/77887791