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

来源:互联网 发布:ipsec dh算法 编辑:程序博客网 时间:2024/06/02 03:11

上一节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跟随底部滑动而滑动,小伙伴可以先思考思考哦。

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

阅读全文
0 0