React Native 带你一步一步实现TabNavitator

前言: 入rn坑已经有一段时间了,想想自己的学习过程还是比较艰辛的啊,唉唉~说多了都是泪啊(虽然现在也还是一个菜鸟),趁着年轻奋斗一下~废话不多说了,刚入rn的时候项目中就有用到github上的一个开源项目TabNavitator,真羡慕那些大牛啊~~ 今天写这篇博客的目的呢,也算是对TabNavitator的一个解析吧,就当个人的一个学习笔记了,大牛略过哈~

先上一下TabNavitator在github上的链接地址:

https://github.com/expo/react-native-tab-navigator

然后看一下我们实现出来的最终效果:

这里写图片描述

效果跟react-native-tab-navigator一样,毕竟是照着它写的。

好啦~~ 下面我们来仿照着react-native-tab-navigator来实现一下我们的TabNavitator。

我们使用TabNavitator的时候,一般是这样的:

<TabNavigator>
  <TabNavigator.Item
    selected={this.state.selectedTab === 'home'}
    title="Home"
    renderIcon={() => <Image source={...} />}
    renderSelectedIcon={() => <Image source={...} />}
    badgeText="1"
    onPress={() => this.setState({ selectedTab: 'home' })}>
    {homeView}
  </TabNavigator.Item>
  <TabNavigator.Item
    selected={this.state.selectedTab === 'profile'}
    title="Profile"
    renderIcon={() => <Image source={...} />}
    renderSelectedIcon={() => <Image source={...} />}
    renderBadge={() => <CustomBadgeView />}
    onPress={() => this.setState({ selectedTab: 'profile' })}>
    {profileView}
  </TabNavigator.Item>

TabNavigator为最外面的布局,TabNavigator.Item为每个item,TabNavigator.Item的child为scene页面。

嗯嗯,先说一下思路:

  1. 根据传入的TabNavigator.Item确认TabBar中的内容。
  2. 根据TabNavigator.Item下面的child确定每个页面
  3. 根据传入的selected确认需要显示的view

好啦~我们开动了。

首先定义一个view叫MyTabNavitator,然后继承Component:

/**
 * @author YASIN
 * @version [PABank V01,17/5/18]
 * @date 17/5/18
 * @description MyTabNavitator
 */

import React,{Component}from 'react';
import {
    View,
}from 'react-native';
export default class MyTabNavitator extends Component{
    render(){
        return(
        );
    }
}

定义MyTabNavitator的属性:

static propTypes={  
        ...View.propTypes,
        sceneStyle: View.propTypes.style,//内容页面的样式
    }

我们的MyTabNavitator其实就是一个Container的角色,所以可以继承view的所有属性、并且我们把内容view的样式也通过它传进来。

限制sceneStyle的类型为View.propTypes.style,除了这个属性外,我们还可以为自定义属性设置哪些类型呢?

 static propTypes={
        ...View.propTypes,
        sceneStyle: View.propTypes.style,//内容页面的样式
        title: PropTypes.string //string类型的属性
    }

我们看看PropTypes到底有哪些类型:

var ReactPropTypes = {
  array: createPrimitiveTypeChecker('array'),
  bool: createPrimitiveTypeChecker('boolean'),
  func: createPrimitiveTypeChecker('function'),
  number: createPrimitiveTypeChecker('number'),
  object: createPrimitiveTypeChecker('object'),
  string: createPrimitiveTypeChecker('string'),
  symbol: createPrimitiveTypeChecker('symbol'),

  any: createAnyTypeChecker(),
  arrayOf: createArrayOfTypeChecker,
  element: createElementTypeChecker(),
  instanceOf: createInstanceTypeChecker,
  node: createNodeChecker(),
  objectOf: createObjectOfTypeChecker,
  oneOf: createEnumTypeChecker,
  oneOfType: createUnionTypeChecker,
  shape: createShapeTypeChecker
};

好啦~一些题外话了哈,接下来我们重写构造方法,并且完成一些state的初始化:

   // 构造
    constructor(props) {
        super(props);
        // 初始状态
        this.state = {
            //需要渲染的页面的key值,每个key值对应一个相应的页面
            renderedSceneKeys: this._updateRenderedSceneKeys(props.children)
        };
    }

我们渲染页面的时候,当第一次进入页面的时候,我们只会渲染selected默认为true的那个页面,然后根据点击情况,动态的切换view显示,比如我们有两个页面 (“我的“、“商家“)第一次进来的时候显示“我的“页面,所以此时我们需要 把“我的‘页面对应的key值放入我们定义的renderedSceneKeys中,然后当我们切换到“商家“页面的时候,我们renderedSceneKeys集合中再加入“商家“页面对应的key值。

所以我们的_updateRenderedSceneKeys方法为:

/**
     * 更新需要渲染页面的key集合
     * @param children 所有的页面
     * @param oldSceneKey 老的key集合
     * @returns {*}  新的key集合
     * @private
     */
    _updateRenderedSceneKeys(children, oldSceneKey = Set()):Set {
        //创建一个新的集合
        let newSceneKeys = Set().asMutable();
        //遍历所有的页面
        React.Children.forEach(children, (item, index)=> {
            //页面为null直接返回
            if (item === null) {
                return;
            }
            //生成每个页面对应的key
            let key = this._getSceneKey(item, index);
            //老key集合中是否包含现在的key或者当前页面的selected是否为true
            //也就是把显示过的页面跟需要显示的页面都加入到key集合中
            if (oldSceneKey.has(key) || item.props.selected) {
                newSceneKeys.add(key);
            }
        });
        //返回一个新的集合
        return newSceneKeys.asMutable();
    }

    /**
     * 根据页面生成唯一的key值
     * @param item 每个页面
     * @param index 页面的index
     * @returns {*}  key值
     * @private
     */
    _getSceneKey(item, index):string {
        return `scene-${(item.key !== null) ? item.key : index}`;
    }

我们引入了一个叫“immutable“的第三方库,这也是facebook自己的集合库,很屌,功能很强大。

immutable库的文档地址:
http://facebook.github.io/immutable-js/docs/#/

相信学过java的童鞋看这个应该是没有什么问题的,因为跟java中的集合框架是一样的。

我们需要显示的页面是有了,但是我们的页面是动态改变的(动态改变TabNavitator的属性,知道需要显示的页面),所以我们需要在一个叫componentWillReceiveProps生命周期中做同样的操作,更新下我们的key集合:

/**
     * 当属性值改变的时候,会调用这个方法
     * @param nextProps
     */
    componentWillReceiveProps(nextProps) {
        //拿到之前的页面key集合
        let {renderedSceneKeys}=this.state;
        //重新更新state,更新key集合
        this.setState({
            renderedSceneKeys: this._updateRenderedSceneKeys(
                nextProps.children,
                renderedSceneKeys
            )
        });
    }

再来回顾下react的生命周期:

这里写图片描述

好啦,MyTabNavitator是定义的差不多了,我们再来看一下MyTabNavitator的使用方法:

<TabNavigator>
  <TabNavigator.Item
    selected={this.state.selectedTab === 'home'}
    title="Home"
    renderIcon={() => <Image source={...} />}
    renderSelectedIcon={() => <Image source={...} />}
    badgeText="1"
    onPress={() => this.setState({ selectedTab: 'home' })}>
    {homeView}
  </TabNavigator.Item>
  <TabNavigator.Item
    selected={this.state.selectedTab === 'profile'}
    title="Profile"
    renderIcon={() => <Image source={...} />}
    renderSelectedIcon={() => <Image source={...} />}
    renderBadge={() => <CustomBadgeView />}
    onPress={() => this.setState({ selectedTab: 'profile' })}>
    {profileView}
  </TabNavigator.Item>

我们的MyTabNavitator只是一个container,需要显示的页面跟数据都在TabNavigator.Item中,所以接下来我们需要定义一下TabNavigator.Item:

我们同样创建一个view叫TabNavigatorItem,然后继承component:

/**
 * @author YASIN
 * @version [PABank V01,17/5/17]
 * @date 17/5/17
 * @description TabNavigatorItem
 */
import React,{
    Component,
    PropTypes
}from 'react';
import {
    View
}from 'react-native';
export default class TabNavigatorItem extends Component {
    static propTypes = {
        //tabbar图标正常时候显示的内容
        renderIcon: PropTypes.func,
        //tabbar图标选中时候显示的内容
        renderSelectedIcon: PropTypes.func,
        //tabbar显示的title
        title: PropTypes.string,
        //tabbar显示的title默认样式
        titleStyle: Text.propTypes.style,
        //tabbar显示的title选中后样式
        selectedTitleStyle: Text.propTypes.style,
        //tabbar的样式
        tabStyle: View.propTypes.style,
        //当前page是否被选中
        selected: PropTypes.bool,
        //tab点击的时候
        onPress: PropTypes.func
    }
    render() {
        //获取唯一的子控件
        let child=React.Children.only(this.props.children);
        //根据传入的child重新clone一个新的child(也就是我们传入的scene)
        return React.cloneElement(child,{
            style:[child.props.style,this.props.style]
        });
    }
}

然后我们把TabNavigatorItem放入TabNavitator的item属性中:

/**
 * @author YASIN
 * @version [PABank V01,17/5/18]
 * @date 17/5/18
 * @description MyTabNavitator
 */
import { Set } from 'immutable';
import React,{Component,PropTypes}from 'react';
import {
    View,
}from 'react-native';
import TabNavigatorItem from './TabNavigatorItem';
export default class MyTabNavitator extends Component {
    static propTypes = {
        ...View.propTypes,
        sceneStyle: View.propTypes.style,//内容页面的样式
        title: PropTypes.string //string类型的属性
    }
    // 构造
    constructor(props) {
        super(props);
        // 初始状态
        this.state = {
            //需要渲染的页面的key值,每个key值对应一个相应的页面
            renderedSceneKeys: this._updateRenderedSceneKeys(props.children)
        };
    }
    /**
     * 更新需要渲染页面的key集合
     * @param children 所有的页面
     * @param oldSceneKey 老的key集合
     * @returns {*}  新的key集合
     * @private
     */
    _updateRenderedSceneKeys(children, oldSceneKey = Set()):Set {
        //创建一个新的集合
        let newSceneKeys = Set().asMutable();
        //遍历所有的页面
        React.Children.forEach(children, (item, index)=> {
            //页面为null直接返回
            if (item === null) {
                return;
            }
            //生成每个页面对应的key
            let key = this._getSceneKey(item, index);
            //老key集合中是否包含现在的key或者当前页面的selected是否为true
            //也就是把显示过的页面跟需要显示的页面都加入到key集合中
            if (oldSceneKey.has(key) || item.props.selected) {
                newSceneKeys.add(key);
            }
        });
        //返回一个新的集合
        return newSceneKeys.asMutable();
    }

    /**
     * 根据页面生成唯一的key值
     * @param item 每个页面
     * @param index 页面的index
     * @returns {*}  key值
     * @private
     */
    _getSceneKey(item, index):string {
        return `scene-${(item.key !== null) ? item.key : index}`;
    }

    /**
     * 当属性值改变的时候,会调用这个方法
     * @param nextProps
     */
    componentWillReceiveProps(nextProps) {
        //拿到之前的页面key集合
        let {renderedSceneKeys}=this.state;
        //重新更新state,更新key集合
        this.setState({
            renderedSceneKeys: this._updateRenderedSceneKeys(
                nextProps.children,
                renderedSceneKeys
            )
        });
    }
    render() {
        return (
        );
    }
}
MyTabNavitator.Item=TabNavigatorItem;

好啦,定义好TabNavigatorItem后,我们就要去实现下我们的TabNavitator的render方法了:

render() {
        //获取相应的props内容
        let{style,children,sceneStyle,...props}=this.props;
        //遍历所有的children
        //需要显示的页面跟显示过的页面
        let scenes=[];
        React.Children.forEach(children,(child,index)=>{
            if(child===null){
                return;
            }
            //根据子view获取唯一key
            let key=this._getSceneKey(child,index);
            let {renderedSceneKeys}=this.state;
            //判断该view是不是将要显示的view或者是不是显示过的view
            if(!renderedSceneKeys.has(key)){
                return;
            }
            //当前view为选中状态就设置其样式为sceneContainer
            let scene=null;
            if (child.props.selected) {
                scene = (
                    <View {...child.props} style={styles.sceneContainer} pointerEvents={'auto'} removeClippedSubviews={false}>
                        {child}
                    </View>
                )
            } else {//当前view为未选中状态就设置其样式为hiddenSceneContainer
                scene = (
                    <View {...child.props}style={styles.hiddenSceneContainer} pointerEvents={'none'} removeClippedSubviews={false}>
                        {child}
                    </View>
                )
            }
            if(scene!==null)scenes.push(scene);
        });

        //选出当前需要显示的view
        let currScene = null;
        scenes.forEach((page, index)=> {
            if (page.props.selected) {
                currScene = page;
                return;
            }
        });
        //render
        return (
            <View
                style={styles.container}
            >
                {currScene}
            </View>
        );
    }
}
const styles = StyleSheet.create({
    sceneContainer: {
        position: 'absolute',
        top: 0,
        left: 0,
        bottom: 0,
        right: 0,
        paddingBottom: 49,
        backgroundColor: 'gray'
    },
    hiddenSceneContainer: {
        overflow: 'hidden',
        opacity: 0
    },
    container: {
        flex: 1
    }
});

这里简单解释一下,如果当前view需要显示的话就设置其style为:

  sceneContainer: {
        position: 'absolute',
        top: 0,
        left: 0,
        bottom: 0,
        right: 0,
        paddingBottom: 49,
        backgroundColor: 'gray'
    },

不显示为:

 hiddenSceneContainer: {
        overflow: 'hidden',
        opacity: 0
    },

github上的demo是这样写的: 不管view显示还是不显示,只要显示过的view或者正要显示的view,都放 入container中,只是把它的透明度设置为了0,所以担心会很多view在一起会抢事件,对于没显示的view设置了一个属性为:

   //当前view为选中状态就设置其样式为sceneContainer
            let scene=null;
            if (child.props.selected) {
                scene = (
                    <View {...child.props} style={styles.sceneContainer} pointerEvents={'auto'} removeClippedSubviews={false}>
                        {child}
                    </View>
                )
            } else {//当前view为未选中状态就设置其样式为hiddenSceneContainer
                scene = (
                    <View {...child.props}style={styles.hiddenSceneContainer} pointerEvents={'none'} removeClippedSubviews={false}>
                        {child}
                    </View>
                )
            }

removeClippedSubviews

“当这一选项设置为true的时候,超出屏幕的子视图(同时overflow值为hidden)会从它们原生的父视图中移除。这个属性可以在列表很长的时候提高滚动的性能。默认为false。(0.14版本后默认为true)”

这是一个应用在长列表上极其重要的优化。Android上,overflow值总是hidden的,所以你不必担心没有设置它。而在iOS上,你需要确保在行容器上设置了overflow: hidden。

**pointerEvents
是字符串类型的属性, 可以取值 none,box-none,box-only,auto.**

none 发生在本组件与本组件的子组件上的触摸事件都会交给本组件的父组件处理.
box-none 发生在本组件显示范围内,但不是子组件显示范围内的事件交给本组件,在子组件显示范围内交给子组件处理
box-only 发生在本组件显示范围内的触摸事件将全部由本组件处理,即使触摸事件发生在本组件的子组件显示范围内
auto 视组件的不同而不同,并不是所有的子组件都支持box-none和box-only两个值,使用时最好测试下

好啦!! 写了那么多了,我们来测试一下:

/**
 * @author YASIN
 * @version [PABank V01,17/5/17]
 * @date 17/5/17
 * @description TestComponent
 */

import React,{Component}from 'react';
import {
    View,
    Text,
    StyleSheet,
    Image
}from 'react-native';
import TabNavitator from './MyTabNavitator';
import * as ScreenUtils from '../../Util/ScreenUtil';
export default class TestComponent extends Component{
    // 构造
      constructor(props) {
        super(props);
        // 初始状态
        this.state = {
            selected: '我的'
        };
      }
    render(){
        let self=this;
        return(
            <TabNavitator>
                <TabNavitator.Item
                    selected={self.state.selected==='我的'}
                    title='我的'
                    onPress={self._onItemPress.bind(self,'我的')}
                    renderIcon={() => <Image style={styles.iconStyle} source={{uri:'icon_tabbar_homepage'}} />}
                    renderSelectedIcon={() => <Image style={styles.iconStyle} source={{uri:'icon_tabbar_homepage_selected'}} />}
                    titleStyle={styles.titleStyle}
                    selectedTitleStyle={styles.selectedTitleStyle}
                >
                    <View style={{flex:1,justifyContent:'center',alignItems:'center'}}><Text style={styles.text}>我的</Text></View>
                </TabNavitator.Item>
                <TabNavitator.Item
                    selected={self.state.selected==='购物'}
                    title='购物'
                    onPress={self._onItemPress.bind(self,'购物')}
                    renderIcon={() => <Image style={styles.iconStyle} source={{uri:'icon_tabbar_homepage'}} />}
                    renderSelectedIcon={() => <Image style={styles.iconStyle} source={{uri:'icon_tabbar_homepage_selected'}} />}
                    titleStyle={styles.titleStyle}
                    selectedTitleStyle={styles.selectedTitleStyle}
                >
                    <View style={{flex:1,justifyContent:'center',alignItems:'center'}}><Text style={styles.text}>购物</Text></View>
                </TabNavitator.Item>
                <TabNavitator.Item
                    selected={self.state.selected==='商家'}
                    title='商家'
                    onPress={self._onItemPress.bind(self,'商家')}
                    renderIcon={() => <Image style={styles.iconStyle} source={{uri:'icon_tabbar_homepage'}} />}
                    renderSelectedIcon={() => <Image style={styles.iconStyle} source={{uri:'icon_tabbar_homepage_selected'}} />}
                    titleStyle={styles.titleStyle}
                    selectedTitleStyle={styles.selectedTitleStyle}
                >
                    <View style={{flex:1,justifyContent:'center',alignItems:'center'}}><Text style={styles.text}>商家</Text></View>
                </TabNavitator.Item>
            </TabNavitator>
        );
    };
    _onItemPress(title){
        this.setState({
            selected: title
        });
    }
}
const styles=StyleSheet.create({
    text:{
        color: 'red'
    },
    iconStyle: {
        width: ScreenUtils.scaleSize(40),
        height: ScreenUtils.scaleSize(40),
    },
    selectedTitleStyle: {
        color: 'orange'
    },
    titleStyle: {
        fontSize: ScreenUtils.setSpText(10),
        color: '#333333'
    }
});

我直接拿着标准的TabNavitator测试代码来测试的,可以看到页面中有“我的“、“购物“、“商家“,然后我们默认选中了“我的“,我们运行看一下效果:

这里写图片描述

然后我们把state换成商家:

 constructor(props) {
        super(props);
        // 初始状态
        this.state = {
            selected: '商家'
        };

这里写图片描述

好啦!! 可以看到,我们革命已经进行到一半啦~~~
接下来就只需要添加TabBar,然后添加Tab,然后根据Tab点击回调,最后设置state进行切换页面,

这里对TabNavitator做了一个小小的优化,之前是所有已经显示过的view或者正要显示的view都放在了container中,现在改为了,只有需要显示的view才显示:

 //选出当前需要显示的view
        let currScene = null;
        scenes.forEach((page, index)=> {
            if (page.props.selected) {
                currScene = page;
                return;
            }
        });
        //render
        return (
            <View
                style={styles.container}
            >
                {currScene}
            </View>
        );
    }

之前的代码为:

  //render
        return (
            <View
                style={styles.container}
            >
                {scenes}
            </View>
        );

所以有些小伙伴说TabNavitator切换的时候比较卡,对的,我也发现了,不知道是不是这个原因,还没测试的,真需要优化的话,我觉得每个单独的页面也得做处理了,比如说网络请求放在:

componentDidMount() {

    }

然后就是一些代码上的优化了。 嗯嗯~~~ 这一节先到这里了,有点长啊,欢迎入群~~~~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值