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

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

还记得我们上一节最后留下的问题吗?比如我们有很多个页面,然后一出来就加载那么多页面的话,再牛掰的手机都扛不住,我们想做的是:
1、第一次加载页面的时候,假设有三个页面,第一次就只加载第一个页面,(用户也可以选择预加载出第二个和第三个页面)。
2、每次滑动的时候,滑动到某个页面,只渲染加载过的页面跟需要预加载的页面,其它的页面先用一个空白页面替代。

哈哈~ 说了那么多文字性的东西,小伙伴是否已经疲惫了呢?下面我们直接撸代码了。

我们把每一个加载过的和需要加载的页面用一个对应的集合sceneKeys对应起来:

export default class ScrollableTab extends Component {
    static propTypes = {
        prerenderingSiblingsNumber: PropTypes.number,//预加载的页面
    }
    static defaultProps = {
        prerenderingSiblingsNumber: 0,//不需要预加载
    }
    // 构造
    constructor(props) {
        super(props);
        // 初始状态
        this.state = {
            containerWidth: screenW,
            currentPage: 0,//当前页面
            scrollXAnim: new Animated.Value(0),
            scrollValue: new Animated.Value(0),
            sceneKeys: this._newSceneKeys({currentPage: 0}),
        };
    }
    ......
      /**
     * 生成需要渲染的页面跟渲染过的页面的集合
     * @param previousKeys 之前的集合
     * @param currentPage 当前页面
     * @param children 子控件
     * @private
     */
    _newSceneKeys({previousKeys = [], currentPage = 0, children = this.props.children,}) {
        let newKeys = [];
        this._children().forEach((child, index)=> {
            const key = this._makeSceneKey(child, index);
            //页面是否渲染过||是否需要预加载
            if (this._keyExists(previousKeys, key) || this._shouldSceneRender(index, currentPage)) {
                newKeys.push(key);
            }
        });
        return newKeys;
    }

    /**
     * 生成唯一key
     * @param child 子控件
     * @param index 下标
     * @private
     */
    _makeSceneKey(child, index) {
        return (child.props.tabLabel + '_' + index);
    }

    /**
     * 判断key是否存在
     * @param previousKeys key集合
     * @param key 当前key
     * @private
     */
    _keyExists(previousKeys, key) {
        return (previousKeys.find((sceneKey)=>sceneKey === key));
    }

    /**
     * 是否需要预加载
     * @private
     */
    _shouldSceneRender(index, currentPage) {
        const siblingsNumber = this.props.prerenderingSiblingsNumber;
        //比如当前页面为1,预加载1个,也就是我们需要显示0、1、2三个页面,所[-1<x<3]
        return (index < (currentPage + siblingsNumber + 1) && index > (currentPage - siblingsNumber - 1));
    }

然后我们渲染子控件的时候,在我们集合中的控件我们就渲染出来,不在集合中的控件我们直接用一个空view替代(因为我们需要滑动操作)。

 /**
     * 渲染主体内容
     * @private
     */
    _renderScrollableContent() {
        return (
            <Animated.ScrollView
                ref={(ref) => {
                    this._scrollView = ref;
                }}
                ....
            >
                {this._renderContentView()}
            </Animated.ScrollView>
        );
    }
 /**
     * 渲染子view
     * @private
     */
    _renderContentView() {
        let scenes = [];
        this._children().forEach((child, index)=> {
            const sceneKey = this._makeSceneKey(child, index);
            let scene = null;
            if (this._keyExists(this.state.sceneKeys, sceneKey)) {
                scene = (child);
            } else {
                scene = (<View tabLabel={child.tabLabel}/>);
            }
            scenes.push(
                <View
                    key={child.key}
                    style={{width: this.state.containerWidth}}
                >
                    {scene}
                </View>
            );
        });
        return scenes;
    }

然后我们在滑动开始和结束的时候,需要重新更新下ke yScenes,也就是重新计算我们需要渲染哪些页面:

 /**
     * scrollview开始跟结束滑动回调
     * @param e
     * @private
     */
    _onMomentumScrollBeginAndEnd = (e) => {
        let offsetX = e.nativeEvent.contentOffset.x;
        let page = Math.round(offsetX / this.state.containerWidth);
        if (this.state.currentPage !== page) {
            this._updateKeyScenes(page);
        }
    }

    /**
     * 更新sceneskey和当前页面
     * @param nextPage
     * @private
     */
    _updateKeyScenes(nextPage) {
        let sceneKeys = this._newSceneKeys({previousKeys: this.state.sceneKeys, currentPage: nextPage})
        this.setState({
            currentPage: nextPage,
            sceneKeys: sceneKeys,
        });

好啦~~ 我们走一遍代码:

这里写图片描述

可以看到,我们页面刚渲染的时候然后滑动到第二页的时候会闪烁一下,那是因为我们加载第一个页面的时候,第二页并没有加载,而是在滑动到第二页的时候加载完毕的,所以当我们再滑回到第一第二页的时候,页面里面出来了,(这也是我们一开始说的“滑到哪加载到哪,页面加载完毕后就不需要重新加载了”)那么问题来了,说好的预加载呢? 哈哈~~ 有的!! 我们把prerenderingSiblingsNumber改为1,也就是说一进来就加载第一页跟第二页,我们再看看效果:

这里写图片描述

可以看到,当我们滑动到第二页的时候页面立马出来了,然后第二页滑动到第三页的时候页面也是立马出来了。

好啦~~ 当我们页面滑动完毕后我们有改变我们的ke y Scenes集合,但是我们点击tabview的时候是不是也得改变下我们的ke y Scenes集合呢? 我们没改变,所以我们点击一下tab切换一下:

这里写图片描述

可以看到,我们点击第一页跟第二页都有页面,然后点到第三页就空白了,这是为什么呢?? 哈哈~ 因为我们前面有设置一个预加载,所以第二页跟第一页一开始就渲染出来了,但是当我们切换到第三页的时候我们的ke yScenes还是之前的集合,所以出来的还是两个页面,所以我们在点击tab回调的地方也得重新计算下ke yScenes集合:

/**
     * 渲染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 (
            <DefaultTabBar
                {...tabParams}
                style={[{width: this.state.containerWidth}]}
                onTabClick={(page)=>this.goToPage(page)}
            />
        );
    }
 /**
     * 滑动到指定位置
     * @param pageNum page下标
     * @param scrollAnimation 是否需要动画
     */
    goToPage(pageNum, scrollAnimation = true) {
        if (this._scrollView && this._scrollView._component && this._scrollView._component.scrollTo) {
            this._scrollView._component.scrollTo({x: pageNum * this.state.containerWidth, scrollAnimation});
            this._updateKeyScenes(pageNum);
        }
    }

然后我们把预加载去掉:

 static propTypes = {
        prerenderingSiblingsNumber: PropTypes.number,//预加载的页面
    }
    static defaultProps = {
        prerenderingSiblingsNumber: 0,//不需要预加载
    }

我们再次运行代码:
这里写图片描述

可以看到,我们完美的实现了我们的需求!
最后附上本节代码:
DefaultTabBar.js:

/**
 * @author YASIN
 * @version [React-Native Pactera V01, 2017/9/5]
 * @date 17/2/23
 * @description DefaultTabBar
 */
import React, {
    Component, PropTypes,
} from 'react';
import {
    View,
    Text,
    StyleSheet,
    TouchableOpacity,
    Dimensions,
    Animated,
} from 'react-native';
const screenW = Dimensions.get('window').width;
const screenH = Dimensions.get('window').height;
export default class DefaultTabBar extends Component {
    static propTypes = {
        tabs: PropTypes.array,
        activeTab: PropTypes.number,//当前选中的tab
        style: View.propTypes.style,
        onTabClick: PropTypes.func,
        containerWidth: PropTypes.number,
    }
    // 构造
    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: containerWidth / tabs.length,
            position: 'absolute',
            bottom: 0,
            left,
        }
        return (
            <View style={[styles.container, this.props.style]}>
                {this.props.tabs.map((name, page) => {
                    const isTabActive = this.props.activeTab === page;
                    return this._renderTab(name, page, isTabActive);
                })}
                <Animated.View
                    style={[styles.tabLineStyle, tabStyle]}
                />
            </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,
        flexDirection: 'row',
        alignItems: 'center',
        height: 50,
    },
    tabStyle: {
        flex: 1,
        alignItems: 'center',
        justifyContent: 'center',
    },
    tabLineStyle: {
        height: 2,
        backgroundColor: 'navy',
    }
});

ScrollableTab.js:

/**
 * @author YASIN
 * @version [React-Native Pactera V01, 2017/9/5]
 * @date 2017/9/5
 * @description index
 */
import React, {
    Component, PropTypes,
} from 'react';
import {
    View,
    Text,
    StyleSheet,
    ScrollView,
    Dimensions,
    TouchableOpacity,
    Animated,
} from 'react-native';
const screenW = Dimensions.get('window').width;
const screenH = Dimensions.get('window').height;
import DefaultTabBar from './DefaultTabBar';
export default class ScrollableTab extends Component {
    static propTypes = {
        prerenderingSiblingsNumber: PropTypes.number,//预加载的页面
    }
    static defaultProps = {
        prerenderingSiblingsNumber: 0,//不需要预加载
    }
    // 构造
    constructor(props) {
        super(props);
        // 初始状态
        this.state = {
            containerWidth: screenW,
            currentPage: 0,//当前页面
            scrollXAnim: new Animated.Value(0),
            scrollValue: new Animated.Value(0),
            sceneKeys: this._newSceneKeys({currentPage: 0}),
        };
    }

    render() {
        return (
            <View
                style={styles.container}
                onLayout={this._onLayout}
            >
                {/*渲染tabview*/}
                {this._renderTabView()}
                {/*渲染主体内容*/}
                {this._renderScrollableContent()}
            </View>
        );
    }

    componentDidMount() {
        //设置scroll动画监听
        this.state.scrollXAnim.addListener(({value})=> {
            let offset = value / this.state.containerWidth;
            this.state.scrollValue.setValue(offset);
        });
    }

    componentWillUnMount() {
        //移除动画监听
        this.state.scrollXAnim.removeAllListeners();
        this.state.scrollValue.removeAllListeners();
    }

    /**
     * 渲染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 (
            <DefaultTabBar
                {...tabParams}
                style={[{width: this.state.containerWidth}]}
                onTabClick={(page)=>this.goToPage(page)}
            />
        );
    }

    /**
     * 渲染主体内容
     * @private
     */
    _renderScrollableContent() {
        return (
            <Animated.ScrollView
                ref={(ref) => {
                    this._scrollView = ref;
                }}
                style={{width: this.state.containerWidth}}
                pagingEnabled={true}
                horizontal={true}
                onMomentumScrollBegin={this._onMomentumScrollBeginAndEnd}
                onMomentumScrollEnd={this._onMomentumScrollBeginAndEnd}
                scrollEventThrottle={15}
                onScroll={Animated.event([{
                    nativeEvent: {contentOffset: {x: this.state.scrollXAnim}}
                }], {
                    useNativeDriver: true,
                })}
                bounces={false}
                scrollsToTop={false}
            >
                {this._renderContentView()}
            </Animated.ScrollView>
        );
    }

    /**
     * 渲染子view
     * @private
     */
    _renderContentView() {
        let scenes = [];
        this._children().forEach((child, index)=> {
            const sceneKey = this._makeSceneKey(child, index);
            let scene = null;
            if (this._keyExists(this.state.sceneKeys, sceneKey)) {
                scene = (child);
            } else {
                scene = (<View tabLabel={child.tabLabel}/>);
            }
            scenes.push(
                <View
                    key={child.key}
                    style={{width: this.state.containerWidth}}
                >
                    {scene}
                </View>
            );
        });
        return scenes;
    }

    /**
     * 获取子控件数组集合
     * @param children
     * @returns {*}
     * @private
     */
    _children(children = this.props.children) {
        return React.Children.map(children, (child)=>child);
    }

    /**
     * 获取控件宽度
     * @param e
     * @private
     */
    _onLayout = (e)=> {
        let {width}=e.nativeEvent.layout;
        if (this.state.containerWidth !== width) {
            this.setState({
                containerWidth: width,
            });
        }
    }

    /**
     * scrollview开始跟结束滑动回调
     * @param e
     * @private
     */
    _onMomentumScrollBeginAndEnd = (e) => {
        let offsetX = e.nativeEvent.contentOffset.x;
        let page = Math.round(offsetX / this.state.containerWidth);
        if (this.state.currentPage !== page) {
            this._updateKeyScenes(page);
        }
    }

    /**
     * 更新sceneskey和当前页面
     * @param nextPage
     * @private
     */
    _updateKeyScenes(nextPage) {
        let sceneKeys = this._newSceneKeys({previousKeys: this.state.sceneKeys, currentPage: nextPage})
        this.setState({
            currentPage: nextPage,
            sceneKeys: sceneKeys,
        });
    }

    /**
     * 滑动到指定位置
     * @param pageNum page下标
     * @param scrollAnimation 是否需要动画
     */
    goToPage(pageNum, scrollAnimation = true) {
        if (this._scrollView && this._scrollView._component && this._scrollView._component.scrollTo) {
            this._scrollView._component.scrollTo({x: pageNum * this.state.containerWidth, scrollAnimation});
            this._updateKeyScenes(pageNum);
        }
    }

    /**
     * 生成需要渲染的页面跟渲染过的页面的集合
     * @param previousKeys 之前的集合
     * @param currentPage 当前页面
     * @param children 子控件
     * @private
     */
    _newSceneKeys({previousKeys = [], currentPage = 0, children = this.props.children,}) {
        let newKeys = [];
        this._children().forEach((child, index)=> {
            const key = this._makeSceneKey(child, index);
            //页面是否渲染过||是否需要预加载
            if (this._keyExists(previousKeys, key) || this._shouldSceneRender(index, currentPage)) {
                newKeys.push(key);
            }
        });
        return newKeys;
    }

    /**
     * 生成唯一key
     * @param child 子控件
     * @param index 下标
     * @private
     */
    _makeSceneKey(child, index) {
        return (child.props.tabLabel + '_' + index);
    }

    /**
     * 判断key是否存在
     * @param previousKeys key集合
     * @param key 当前key
     * @private
     */
    _keyExists(previousKeys, key) {
        return (previousKeys.find((sceneKey)=>sceneKey === key));
    }

    /**
     * 是否需要预加载
     * @private
     */
    _shouldSceneRender(index, currentPage) {
        const siblingsNumber = this.props.prerenderingSiblingsNumber;
        //比如当前页面为1,预加载1个,也就是我们需要显示0、1、2三个页面,所[-1<x<3]
        return (index < (currentPage + siblingsNumber + 1) && index > (currentPage - siblingsNumber - 1));
    }
}
const styles = StyleSheet.create({
    container: {
        width: screenW,
        flex: 1,
        marginTop: 22,
    },
});

下一节我们去实现一下ScrollableTabBar.js,也就是说上面的tabview需要映射底部的scrollview滑动,让tabview跟随底部滑动而滑动,小伙伴可以先思考思考哦。

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

猜你喜欢

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