SakuraDon

React-Native StackNavigator和TabNavigator路由嵌套设计
官方文档嵌套模型Screen没有画全,但大概是这样的模型。代码写在app.js中统一管理:const LoginS...
扫描右侧二维码阅读全文
16
2018/12

React-Native StackNavigator和TabNavigator路由嵌套设计

官方文档

嵌套模型

modal.jpg
Screen没有画全,但大概是这样的模型。
代码写在app.js中统一管理:

const LoginStack = createStackNavigator({
    Login: Login,
    Register: Register,
}, {
    initialRouteName: 'Login',
});

const ShopStack = createStackNavigator({
    Shop: Shop,
    ShoppingLog: ShoppingLog
}, {
    initialRouteName: 'Shop',
});


const AssetsStack = createStackNavigator({
    Assets: Assets,
    TransferInLog: TransferInLog,
    TransferOutLog: TransferOutLog,
}, {
    initialRouteName: 'Assets',
});


const MineStack = createStackNavigator({
    Mine: Mine,
    MyPhone: MyPhone,
    PasswordSetting: PasswordSetting,
}, {
    initialRouteName: 'Mine',
});

const MainTab = createBottomTabNavigator({
    ShopStack: {
        screen: ShopStack,
        navigationOptions: {
            tabBarLabel: '商城',
        }
    },
    AssetsStack: {
        screen: AssetsStack,
        navigationOptions: {
            tabBarLabel: '资产',
        }
    },
    MineStack: {
      screen: MineStack,
      navigationOptions: {
            tabBarLabel: '我的',
      }
    }

});

const AppNavigator = createStackNavigator({
    Loading: {
        screen: Loading
    },
    LoginStack: {
        screen: LoginStack
    },
    MainTab: {
        screen: MainTab
    }
}, {
    initialRouteName: 'Loading',
});

const AppContainer = createAppContainer(AppNavigator);

export default class App extends React.Component {
    render() {
        <AppContainer/>
    }
}

react的路由其实是把我们写的页面(screen)放进StackNavigator或是TabNavigator(下面简称StackTab)这种容器中,切换页面时告诉容器,然后容器显示对应的页面,跟传统的网页跳转不大一样,但是跟单页面应用有点相似。
createStackNavigatorcreateBottomTabNavigator两个函数的第一个参数都是声明包含的页面(screen),第二个参数为该容器的配置。

上面的代码只是实现了模型图中的路由,后面还有一大堆需要配置的东西。

头部栏的配置

qq_pic_merged_1546938466971.jpg
qq_pic_merged_1546938480125.jpg
上图所示的为路由中的头部栏。进入Stack非首页后会出现头部栏并带有返回按钮。

隐藏头部栏

每个容器都拥有一个头部栏配置。而我们的容器又是嵌套的,参照上面的图,当我们打开ShopScreen时,我们可能同时看见两个头部栏,分别是ShopStackAppStack的头部栏(Tab没有头部栏,所以不会显示MainTab的头部栏,但是Tab会有底部栏,在下面介绍),所以我们必须在一定场合隐藏一些头部栏。隐藏的方式是在配置中的defaultNavigationOptions加入header: null。这里以AppStack为例,他作为最外层的容器,不需要任何头部栏。

const AppNavigator = createStackNavigator({
    Loading: {
        screen: Loading
    },
    LoginStack: {
        screen: LoginStack
    },
    MainTab: {
        screen: MainTab
    }
}, {
    initialRouteName: 'Loading',
    defaultNavigationOptions: {
        header: null
    }
});

头部栏样式

事实上我们大多数时候都是需要头部栏,我们可以在对应的Stack的defaultNavigationOptions设置对应的样式,然后在对应的页面添加静态方法navigationOptions,用于设置头部栏的标题和左右两边的按钮等。因为我这个项目多个Stack的头部栏样式相同,所以我把样式配置提了出来。
但在配置时要记住当前页面是属于哪一个Stack,使用的哪一个Stack的头部栏。
app.js

const defaultNavigationOptions = () => ({
    headerStyle: {
        backgroundColor: '#1c2d34',
        height: 48,
        shadowOpacity: 0,
        shadowOffset: {
          height: 0,
        },
        shadowRadius: 0,
        elevation: 0,
    },
    headerTintColor: '#00b17b',
    headerTitleStyle: {
        fontSize: 16,
        textAlign: 'center',
        color: '#6792a3',
        fontWeight: 'normal',
        flex: 1,
        textAlign:'center'
    },
    headerRight: <View></View>
});

const MineStack = createStackNavigator({
    Mine: Mine,
    MyPhone: MyPhone,
    PasswordSetting: PasswordSetting,
}, {
    initialRouteName: 'Mine',
    defaultNavigationOptions: defaultNavigationOptions//头部栏样式
});

mine.js

export default class Mine extends React.Component {
    ...
    static navigationOptions = ({navigation}) => ({
        headerTitle: '个人信息',
        headerLeft: <View></View>,
        headerRight: <Text style={{
                        marginRight: 12,
                        textAlign: 'right',
                        fontSize: 16,
                        color: '#e60012'
                    }}>登出</Text>
    });
    ...
}

效果如下
qq_pic_merged_1546952358689.jpg

标题居中

在上面的mine.js中,我在配置中加了一个headerLeft: <View></View>。因为头部栏是由三部分组成的,headerLeftheaderTitleheaderRight。在之前我们也说过,在非首页的页面,头部栏的左边才会出现一个返回按钮,如果是在Stack的首页,那么headerLeft将不存在,导致headerTitle向左偏移,无论如何都难以居中,所以我们在这里添加一个空的View,这样就能正常居中了。

自定义左右按钮

在上面,我在headerRight中写了一个登出的按钮,同样的,我也能在headerLeft中写按钮。如果headerLeft被编辑,那么当前页面默认的返回按钮将不会出现,会被编辑的内容替代。你可以自定义样式,自定义跳转等。
下面的例子中我点击关闭将直接回到登录界面。

...
    static navigationOptions = ({navigation}) => ({
        headerTitle: '激活',
        headerStyle: {
            backgroundColor: '#1C2D34',
            height: 48,
            shadowOpacity: 0,
            shadowOffset: {
              height: 0,
            },
            shadowRadius: 0,
            elevation: 0,
        },
        headerLeft: <Icon name="times" size={24} color="#00b17b" style={{
            marginLeft: 20
        }} onPress={() => {
            const { navigate } = navigation;
            navigate('Login');
        }}></Icon>
    });
...

qq_pic_merged_1546953053207.jpg

底部栏的配置

底部栏也是一个app应该必备的功能,通过点击底部栏图标快速切换对应的页面。
20190108212201.png

底部栏图标

最开始的配置中我们已经编辑了底部栏每个按钮的标题tabBarLabel,当然光有文字是不够的,我们还需要图标,就如上图所示。

const MainTab = createBottomTabNavigator({
    MiningStack: {
        screen: MiningStack,
        navigationOptions: {
            tabBarLabel: '主页',
        }
    },
    ShopStack: {
        screen: ShopStack,
        navigationOptions: {
            tabBarLabel: '商城',
        }
    },
    AssetsStack: {
        screen: AssetsStack,
        navigationOptions: {
            tabBarLabel: '资产',
        }
    },
    MineStack: {
      screen: MineStack,
      navigationOptions: {
            tabBarLabel: '我的',
      }
    }

}, {
    defaultNavigationOptions: ({ navigation }) => ({
        tabBarIcon: ({ focused, horizontal, tintColor }) => {
            const { routeName } = navigation.state;
            let img;
            let imgArr = {
                "Mining": require('./static/image/mining.png'),
                "Mining_hover": require('./static/image/mining_hover.png'),
                "Shop": require('./static/image/shop.png'),
                "Shop_hover": require('./static/image/shop_hover.png'),
                "Assets": require('./static/image/assets.png'),
                "Assets_hover": require('./static/image/assets_hover.png'),
                "Mine": require('./static/image/mine.png'),
                "Mine_hover": require('./static/image/mine_hover.png'),
            };

            if (routeName === 'MiningStack') {
                img = focused ? 'Mining_hover' : 'Mining';
            } else if (routeName === 'ShopStack') {
                img = focused ? 'Shop_hover' : 'Shop';
            } else if (routeName === 'AssetsStack') {
                img = focused ? 'Assets_hover' : 'Assets';
            } else if (routeName === 'MineStack') {
                img = focused ? 'Mine_hover' : 'Mine';
            }

            return <Image style={{
                width: 22,
                height: 22,
            }} source={imgArr[img]} />;

        },
        //隐藏底部栏
        tabBarVisible: navigation.state.index > 0 ? false : true,
    }),
    //底部栏样式
    tabBarOptions: {
        activeTintColor: '#00b07a',
        inactiveTintColor: '#6792a3',
        style: {
            backgroundColor: '#1c2d34',
            height: 48,
            paddingTop: 8,
            paddingBottom: 4
        },
    },
});

这里,我们把四个按钮的图标统一在app.js中处理了,当然也可以在对应页面的navigationOptions里配置tabBarIcon

底部栏样式

上面代码也注释出来了,tabBarOptions即为Tab的样式配置。

隐藏底部栏

底部栏通常只在底部栏上对应的页面中显示,如果是在其他页面,例如PasswordSettingScreen,如果不加任何配置,那么底部栏会一直存在。所以我们要让底部栏在不需要的时候不显示出来。
上面代码同样注释出来了。在defaultNavigationOptions里配置tabBarVisible

tabBarVisible: navigation.state.index > 0 ? false : true,

为什么要这样设置呢?
还记得我们的MainTab包含有多个Stack,而MainTab里显示的页面其实是Stack里的页面,所以这里的navigation其实是对应的Stack的。navigation.state.index > 0代表着当前Stack显示的是非首页,就达到了我们想要的目的。

返回键的处理

以下内容仅针对android,因为ios没有返回键233

默认的情况下,在非Stack首页按下返回键会自动回到上一页(等同头部栏的返回键),在Stack首页按下返回键会退出app。在Tab里,按下返回键会返回到第一个页面(index=0),因为我们的MainTab嵌套在AppStack里,所以在MainTab的首页里再按下返回键就会跳转到LoadingScreen
当然上面的情况是我们不想要的,所以我们需要加以处理。另外我们在这里在这里再添加一个'再按一次退出应用'功能。
先上代码。

import {
    ToastAndroid,
    BackHandler,
} from 'react-native';

let isExit = true;
let lastBackPressed = null;
export default class App extends React.Component {
    constructor(props) {
        super(props);
    }
 
    componentDidMount() {
        BackHandler.addEventListener('hardwareBackPress', this.onBackAndroid);
    }
 
    componentWillUnmount() {
        BackHandler.removeEventListener('hardwareBackPress', this.onBackAndroid);
        lastBackPressed = null;
    }
 
    onBackAndroid() {
        if (isExit) { // 根界面
            if (lastBackPressed && lastBackPressed + 2000 >= Date.now()) {
                BackHandler.exitApp();
                return true;
            }
            lastBackPressed = Date.now();
            ToastAndroid.show('再按一次退出应用', ToastAndroid.SHORT);
            return true;
        }
        return false
    }

    render() {
      return <AppContainer onNavigationStateChange={(prevNav, nav, action) => {
                routes = nav.routes;
                curretRoutes = routes[routes.length - 1];
                if (curretRoutes.routeName == 'MainTab') {
                    if (curretRoutes.routes[curretRoutes.index].index == undefined || curretRoutes.routes[curretRoutes.index].index == 0) {
                        isExit = true;
                    } else {
                        isExit = false;
                    }
                } else {
                    isExit = false;
                }

                if (curretRoutes.routeName == 'LoginStack') {
                    if (curretRoutes.index == 0){
                        isExit = true;
                    } else {
                        isExit = false;
                    }
                }
            }}/>

    }
}

我们在componentDidMountcomponentWillUnmount里分别添加和移除hardwareBackPress的事件,即监听返回键,按下返回键时触发onBackAndroid()。我们又在AppContainer里修改onNavigationStateChange方法,即路由发生改变时执行的方法。
这里要注意的是,当我们按下返回键时,路由即将改变,所以这里会先触发onNavigationStateChange,然后再触发onBackAndroid。所以我们用isExit变量控制onBackAndroid之后的操作。监听函数返回false,则会执行默认行为,返回上一页或者退出应用,返回true则不会执行这些行为。
LoginStack很好理解,curretRoutes.index == 0即在LoginStack首页时执行'再按一次退出应用'的功能,不在首页执行默认功能。
对于MainTabcurretRoutes.routes[curretRoutes.index]代表着Tab当前选中页,因为MainTab里嵌套的是Stack,所以这里就是对应StackcurretRoutes.routes[curretRoutes.index].index == 0即对应Stack在首页,curretRoutes.routes[curretRoutes.index].index == undefined即选中的不是Stack而是普通的Screen

在任意js文件里跳转路由

这个项目中,接口都是分离到api文件夹下的,单独的js文件,如果这时需要跳转路由当然也是可以做到的。
首先需要一个创建一个NavigationService.js文件

// NavigationService.js

import { NavigationActions } from 'react-navigation';

let _navigator;

function setTopLevelNavigator(navigatorRef) {
  _navigator = navigatorRef;
}

function navigate(routeName, params) {
  _navigator.dispatch(
    NavigationActions.navigate({
      routeName,
      params,
    })
  );
}

// add other navigation functions that you need and export them

export default {
  navigate,
  setTopLevelNavigator,
};

在app.js里引入NavigationService.js,并在AppContainer上加上ref参数

import NavigationService from './utils/NavigationService';

export default class App extends React.Component {
    ...
    render() {
      return <AppContainer onNavigationStateChange={(prevNav, nav, action) => {
                ...
            }} ref={navigatorRef => {
                NavigationService.setTopLevelNavigator(navigatorRef);
            }}/>
    }
    ...
}

在需要的页面引入NavigationService.js,调用navigate即可

import NavigationService from './NavigationService';

...
NavigationService.navigate('Login');
...

切换动画

跟样式一样,切换动画对于各个Stack都通用,所以我也把其提了出来。

import {
    Easing,
    Animated
} from 'react-native';

const defaultTransitionConfig = () => ({
    transitionSpec: {
        duration: 500,
        easing: Easing.out(Easing.poly(4)),
        timing: Animated.timing,
    },
    screenInterpolator: sceneProps => {
        const { layout, position, scene } = sceneProps;
        const { index } = scene;

        const height = layout.initHeight;
        const translateY = position.interpolate({
            inputRange: [index - 1, index, index + 1],
            outputRange: [height, 0, 0],
        });

        const opacity = position.interpolate({
            inputRange: [index - 1, index - 0.99, index],
            outputRange: [0, 1, 1],
        });
        return { opacity, transform: [{ translateY }] };
    },
});

const MineStack = createStackNavigator({
    Mine: Mine,
    MyPhone: MyPhone,
    PasswordSetting: PasswordSetting,
}, {
    initialRouteName: 'Mine',
    defaultNavigationOptions: defaultNavigationOptions,
    transitionConfig: defaultTransitionConfig,//切换动画配置
});
最終更新:2019 年 03 月 14 日 08 : 37 PM
あなたが私の記事があなたにとって有用であると感じるならば、それを感謝してください。