[置顶] React Native未来导航者:react-navigation 使用详解(进阶篇)
2018-03-06 17:44
941 查看
刚创建的React Native 微信公众号,欢迎微信扫描关注订阅号,每天定期会分享react native 技术文章,移动技术干货,精彩文章技术推送。同时可以扫描我的微信加入react-native技术交流微信群。欢迎各位大牛,React Native技术爱好者加入交流!
测试中发现,在iphone上标题栏的标题为居中状态,而在Android上则是居左对齐。所以需要我们修改源码,进行适配。
【node_modules -- react-navigation -- src -- views -- Header.js】的326行代码处,修改为如下:
(2)去除返回键文字显示:
【node_modules -- react-navigation -- src -- views -- HeaderBackButton.js】的91行代码处,修改为如下即可。
(3)动态设置头部按钮事件:
当我们在头部设置左右按钮时,肯定避免不了要设置按钮的单击事件,但是此时会有一个问题,navigationOptions是被修饰为static类型的,所以我们在按钮的onPress的方法中不能直接通过this来调用Component中的方法。如何解决呢?在官方文档中,作者给出利用设置params的思想来动态设置头部标题。那么我们可以利用这种方式,将单击回调函数以参数的方式传递到params,然后在navigationOption中利用navigation来取出设置到onPress即可:
(4)结合BackHandler处理返回和点击返回键两次退出App效果
点击返回键两次退出App效果的需求屡见不鲜。相信很多人在react-navigation下实现该功能都遇到了很多问题,例如,其他界面不能返回。也就是手机本身返回事件在react-navigation之前拦截了。如何结合react-natigation实现呢?和大家分享两种实现方式:
(1)在注册StackNavigator的界面中,注册BackHandler:
componentWillMount(){
BackHandler.addEventListener('hardwareBackPress', this._onBackAndroid );
}
componentUnWillMount(){
BackHandler.addEventListener('hardwareBackPress', this._onBackAndroid);
}
_onBackAndroid=()=>{
let now = new Date().getTime();
if(now - lastBackPressed < 2500) {
return false;
}
lastBackPressed = now;
ToastAndroid.show('再点击一次退出应用',ToastAndroid.SHORT);
return true;
}
(2)监听react-navigation的Router
react-navigation在Android中默认的界面切换动画是上下。如何实现左右切换呢?很简单的配置即可:import CardStackStyleInterpolator from 'react-navigation/src/views/CardStackStyleInterpolator';然后在StackNavigator的配置下添加如下代码:
当我们快速点击跳转时,会开启多个重复的界面,如何解决呢。其实在官方git中也有提示,解决这个问题需要修改react-navigation源码:
找到src文件夹中的addNavigationHelpers.js文件,替换为如下文本即可:export default function<S: *>(navigation: NavigationProp<S, NavigationAction>) {
// 添加点击判断
let debounce = true;
return {
...navigation,
goBack: (key?: ?string): boolean =>
navigation.dispatch(
NavigationActions.back({
key: key === undefined ? navigation.state.key : key,
}),
),
navigate: (routeName: string,
params?: NavigationParams,
action?: NavigationAction,): boolean => {
if (debounce) {
debounce = false;
navigation.dispatch(
NavigationActions.navigate({
routeName,
params,
action,
}),
);
setTimeout(
() => {
debounce = true;
},
500,
);
return true;
}
return false;
},
/**
* For updating current route params. For example the nav bar title and
* buttons are based on the route params.
* This means `setParams` can be used to update nav bar for example.
*/
setParams: (params: NavigationParams): boolean =>
navigation.dispatch(
NavigationActions.setParams({
params,
key: navigation.state.key,
}),
),
};
}
(7)解决goBack,根据路由名称返回指定界面
react-navigation默认不支持根据路由名返回指定界面,官方只提供了根据Key来做goBack的指定返回。解决这个问题同样需要修改react-navigation源码,在Navigation.goBack条件下添加对路由名的支持。找到/node_modules/react-navigation/src/routers/StackRouter.js, 全局搜索backRoute,将条件判断语句替换为如下代码:if (action.type === NavigationActions.BACK) {
const key = action.key;
let backRouteIndex = null;
if (key) {
const backRoute = null;
if(key.indexOf('id') >= 0) {
backRoute = state.routes.find((route: *) => route.key === action.key);
} else {
backRoute = state.routes.find(route => route.routeName === action.key);
}
backRouteIndex = state.routes.indexOf(backRoute);
}
if (backRouteIndex == null) {
return StateUtils.pop(state);
}
if (backRouteIndex > 0) {
return {
...state,
routes: state.routes.slice(0, backRouteIndex),
index: backRouteIndex - 1,
};
}
}(8)自定义Tabimport React, { Component } from 'react';
import {
AppRegistry,
Platform,
StyleSheet,
Text,
View,
TouchableOpacity,
NativeModules,
ImageBackground,
DeviceEventEmitter
} from 'react-native';
export default class Tab extends Component {
renderItem = (route, index) => {
const {
navigation,
jumpToIndex,
} = this.props;
const focused = index === navigation.state.index;
const color = focused ? this.props.activeTintColor : this.props.inactiveTintColor;
let TabScene = {
focused:focused,
route:route,
tintColor:color
};
if(index==1){
return (<View style={[styles.tabItem,{backgroundColor:'transparent'}]}>
</View>
);
}
return (
<TouchableOpacity
key={route.key}
style={styles.tabItem}
onPress={() => jumpToIndex(index)}
>
<View
style={styles.tabItem}>
{this.props.renderIcon(TabScene)}
<Text style={{ ...styles.tabText,marginTop:SCALE(10),color }}>{this.props.getLabel(TabScene)}</Text>
</View>
</TouchableOpacity>
);
};
render(){
const {navigation,jumpToIndex} = this.props;
const {routes,} = navigation.state;
const focused = 1 === navigation.state.index;
const color = focused ? this.props.activeTintColor : this.props.inactiveTintColor;
let TabScene = {
focused:focused,
route:routes[1],
tintColor:color
};
return (
<View style={{width:WIDTH}}>
<View style={styles.tab}>
{routes && routes.map((route,index) => this.renderItem(route, index))}
</View>
<TouchableOpacity
key={"centerView"}
style={[styles.tabItem,{position:'absolute',bottom:0,left:(WIDTH-SCALE(100))/2,right:WIDTH-SCALE(100),height:SCALE(120)}]}
onPress={() => jumpToIndex(1)}
>
<View
style={styles.tabItem}>
{this.props.renderIcon(TabScene)}
<Text style={{ ...styles.tabText,marginTop:SCALE(10),color }}>{this.props.getLabel(TabScene)}</Text>
</View>
</TouchableOpacity>
</View>
);
}
}
const styles = {
tab:{
width:WIDTH,
backgroundColor:'transparent',
flexDirection:'row',
justifyContent:'space-around',
alignItems:'flex-end'
},
tabItem:{
height:SCALE(80),
width:SCALE(100),
alignItems:'center',
justifyContent:'center'
},
tabText:{
marginTop:SCALE(13),
fontSize:FONT(10),
color:Color.C7b7b7b
},
tabTextChoose:{
color:Color.f3474b
},
tabImage:{
width:SCALE(42),
height:SCALE(42),
},
}
(9)如何在屏幕控件之外的模块获取当前界面及navigation实例
很多情况下,我们都需要处理登录token失效的情况。例如:在当前设备登录后不退出,此时在另一台设备登录,导致第一个设备用户登录状态失效,此时在第一台设备操作网络请求时,需要提醒用户登录失效,跳转登录界面,并重新登录。
这种需求很常见,关于网络请求我们一般会封装为一个HttpUtil。然后在Component中去调用。此时如果需要处理登录失效的跳转逻辑,需要写在HttpUtil,那么在HttpUtil中就没办法获取navigation来做跳转,那么如何解决呢?下面提供一种方案,很实用:
定义一个Component的基类,包含当前显示的Component实例:screen,以及导航函数。import React, {Component} from 'react';
export default class Base extends Component {
static screen;
constructor(props) {
super(props);
Base.screen = this;
}
nav() {
return this.props.navigation;
}
}在其他组件/模块中,我可以调用它来导航到不同的屏幕:
(10)react-navigation高级用法:实现自定义Tab切换效果。
react-navigation 库中提供了实现自定义Router切换的方式,需要用到的组件如下: TabRouter,
createNavigator,
createNavigationContainer1. TabRouter用来自定义路由栈
2. createNavigator用来创建导航组件
3. createNavigationContainer作为导航组件的容器组件
自定义Router切换的流程大致如下:
1. 创建StackNavigator
2. 创建TabRouter
3. 定义导航样式
4. 定义整体路由切换组件
5. 创建Navigator
来看核心代码:// 界面组件
import FirstPage from './scene/FirstPage';
import SecondPage from './scene/SecondPage';
import ThirdPage from './scene/ThirdPage';
import DetailPage from './scene/DetailPage';
(11)定义某个界面的切换动画效果
有时候产品会存在某个界面的切换动画和其他不同,那么如何实现呢?很简单,只需要在StackNavigator中配置参数下声明以下代码:transitionConfig:()=>({
screenInterpolator:
(props)=> {
const { scene } = props
if (scene.route.routeName === 'VIPDetailPage') {
return CardStackStyleInterpolator.forFade
} else {
return CardStackStyleInterpolator.forHorizontal(props)
}
}
})
本篇内容为react-navigation的进阶内容以及高级用法。
(1)适配顶部导航栏标题:测试中发现,在iphone上标题栏的标题为居中状态,而在Android上则是居左对齐。所以需要我们修改源码,进行适配。
【node_modules -- react-navigation -- src -- views -- Header.js】的326行代码处,修改为如下:
title: { bottom: 0, left: TITLE_OFFSET, right: TITLE_OFFSET, top: 0, position: 'absolute', alignItems: 'center', }上面方法通过修改源码的方式其实略有弊端,毕竟扩展性不好。还有另外一种方式就是,在navigationOptions中设置headerTitleStyle的alignSelf为 ' center '即可解决。
(2)去除返回键文字显示:
【node_modules -- react-navigation -- src -- views -- HeaderBackButton.js】的91行代码处,修改为如下即可。
{Platform.OS === 'ios' && title && <Text onLayout={this._onTextLayout} style={[styles.title, { color: tintColor }]} numberOfLines={1} > {backButtonTitle} </Text>}将上述代码删除即可。
(3)动态设置头部按钮事件:
当我们在头部设置左右按钮时,肯定避免不了要设置按钮的单击事件,但是此时会有一个问题,navigationOptions是被修饰为static类型的,所以我们在按钮的onPress的方法中不能直接通过this来调用Component中的方法。如何解决呢?在官方文档中,作者给出利用设置params的思想来动态设置头部标题。那么我们可以利用这种方式,将单击回调函数以参数的方式传递到params,然后在navigationOption中利用navigation来取出设置到onPress即可:
componentDidMount () { /** * 将单击回调函数作为参数传递 */ this.props.navigation.setParams({ switch: () => this.switchView() }); }
/** * 切换视图 */ switchView() { alert('切换') }
static navigationOptions = ({navigation,screenProps}) => ({ headerTitle: '企业服务', headerTitleStyle: CommonStyles.headerTitleStyle, headerRight: ( <NavigatorItem icon={ Images.ic_navigator } onPress={ ()=> navigation.state.params.switch() }/> ), headerStyle: CommonStyles.headerStyle });
componentDidMount () { /** * 将单击回调函数作为参数传递 */ this.props.navigation.setParams({ switch: () => this.switchView() }); }
/** * 切换视图 */ switchView() { alert('切换') }
static navigationOptions = ({navigation,screenProps}) => ({ headerTitle: '企业服务', headerTitleStyle: CommonStyles.headerTitleStyle, headerRight: ( <NavigatorItem icon={ Images.ic_navigator } onPress={ ()=> navigation.state.params.switch() }/> ), headerStyle: CommonStyles.headerStyle });
(4)结合BackHandler处理返回和点击返回键两次退出App效果
点击返回键两次退出App效果的需求屡见不鲜。相信很多人在react-navigation下实现该功能都遇到了很多问题,例如,其他界面不能返回。也就是手机本身返回事件在react-navigation之前拦截了。如何结合react-natigation实现呢?和大家分享两种实现方式:
(1)在注册StackNavigator的界面中,注册BackHandler:
componentWillMount(){
BackHandler.addEventListener('hardwareBackPress', this._onBackAndroid );
}
componentUnWillMount(){
BackHandler.addEventListener('hardwareBackPress', this._onBackAndroid);
}
_onBackAndroid=()=>{
let now = new Date().getTime();
if(now - lastBackPressed < 2500) {
return false;
}
lastBackPressed = now;
ToastAndroid.show('再点击一次退出应用',ToastAndroid.SHORT);
return true;
}
(2)监听react-navigation的Router
/** * 处理安卓返回键 */ const defaultStateAction = AppNavigator.router.getStateForAction; AppNavigator.router.getStateForAction = (action,state) => { if(state && action.type === NavigationActions.BACK && state.routes.length === 1) { if (lastBackPressed + 2000 < Date.now()) { ToastAndroid.show(Constant.hint_exit,ToastAndroid.SHORT); lastBackPressed = Date.now(); const routes = [...state.routes]; return { ...state, ...state.routes, index: routes.length - 1, }; } } return defaultStateAction(action,state); };(5)实现Android中界面跳转左右切换动画
react-navigation在Android中默认的界面切换动画是上下。如何实现左右切换呢?很简单的配置即可:import CardStackStyleInterpolator from 'react-navigation/src/views/CardStackStyleInterpolator';然后在StackNavigator的配置下添加如下代码:
transitionConfig:()=>({ screenInterpolator: CardStackStyleInterpolator.forHorizontal, })(6)解决快速点击多次跳转
当我们快速点击跳转时,会开启多个重复的界面,如何解决呢。其实在官方git中也有提示,解决这个问题需要修改react-navigation源码:
找到src文件夹中的addNavigationHelpers.js文件,替换为如下文本即可:export default function<S: *>(navigation: NavigationProp<S, NavigationAction>) {
// 添加点击判断
let debounce = true;
return {
...navigation,
goBack: (key?: ?string): boolean =>
navigation.dispatch(
NavigationActions.back({
key: key === undefined ? navigation.state.key : key,
}),
),
navigate: (routeName: string,
params?: NavigationParams,
action?: NavigationAction,): boolean => {
if (debounce) {
debounce = false;
navigation.dispatch(
NavigationActions.navigate({
routeName,
params,
action,
}),
);
setTimeout(
() => {
debounce = true;
},
500,
);
return true;
}
return false;
},
/**
* For updating current route params. For example the nav bar title and
* buttons are based on the route params.
* This means `setParams` can be used to update nav bar for example.
*/
setParams: (params: NavigationParams): boolean =>
navigation.dispatch(
NavigationActions.setParams({
params,
key: navigation.state.key,
}),
),
};
}
(7)解决goBack,根据路由名称返回指定界面
react-navigation默认不支持根据路由名返回指定界面,官方只提供了根据Key来做goBack的指定返回。解决这个问题同样需要修改react-navigation源码,在Navigation.goBack条件下添加对路由名的支持。找到/node_modules/react-navigation/src/routers/StackRouter.js, 全局搜索backRoute,将条件判断语句替换为如下代码:if (action.type === NavigationActions.BACK) {
const key = action.key;
let backRouteIndex = null;
if (key) {
const backRoute = null;
if(key.indexOf('id') >= 0) {
backRoute = state.routes.find((route: *) => route.key === action.key);
} else {
backRoute = state.routes.find(route => route.routeName === action.key);
}
backRouteIndex = state.routes.indexOf(backRoute);
}
if (backRouteIndex == null) {
return StateUtils.pop(state);
}
if (backRouteIndex > 0) {
return {
...state,
routes: state.routes.slice(0, backRouteIndex),
index: backRouteIndex - 1,
};
}
}(8)自定义Tabimport React, { Component } from 'react';
import {
AppRegistry,
Platform,
StyleSheet,
Text,
View,
TouchableOpacity,
NativeModules,
ImageBackground,
DeviceEventEmitter
} from 'react-native';
export default class Tab extends Component {
renderItem = (route, index) => {
const {
navigation,
jumpToIndex,
} = this.props;
const focused = index === navigation.state.index;
const color = focused ? this.props.activeTintColor : this.props.inactiveTintColor;
let TabScene = {
focused:focused,
route:route,
tintColor:color
};
if(index==1){
return (<View style={[styles.tabItem,{backgroundColor:'transparent'}]}>
</View>
);
}
return (
<TouchableOpacity
key={route.key}
style={styles.tabItem}
onPress={() => jumpToIndex(index)}
>
<View
style={styles.tabItem}>
{this.props.renderIcon(TabScene)}
<Text style={{ ...styles.tabText,marginTop:SCALE(10),color }}>{this.props.getLabel(TabScene)}</Text>
</View>
</TouchableOpacity>
);
};
render(){
const {navigation,jumpToIndex} = this.props;
const {routes,} = navigation.state;
const focused = 1 === navigation.state.index;
const color = focused ? this.props.activeTintColor : this.props.inactiveTintColor;
let TabScene = {
focused:focused,
route:routes[1],
tintColor:color
};
return (
<View style={{width:WIDTH}}>
<View style={styles.tab}>
{routes && routes.map((route,index) => this.renderItem(route, index))}
</View>
<TouchableOpacity
key={"centerView"}
style={[styles.tabItem,{position:'absolute',bottom:0,left:(WIDTH-SCALE(100))/2,right:WIDTH-SCALE(100),height:SCALE(120)}]}
onPress={() => jumpToIndex(1)}
>
<View
style={styles.tabItem}>
{this.props.renderIcon(TabScene)}
<Text style={{ ...styles.tabText,marginTop:SCALE(10),color }}>{this.props.getLabel(TabScene)}</Text>
</View>
</TouchableOpacity>
</View>
);
}
}
const styles = {
tab:{
width:WIDTH,
backgroundColor:'transparent',
flexDirection:'row',
justifyContent:'space-around',
alignItems:'flex-end'
},
tabItem:{
height:SCALE(80),
width:SCALE(100),
alignItems:'center',
justifyContent:'center'
},
tabText:{
marginTop:SCALE(13),
fontSize:FONT(10),
color:Color.C7b7b7b
},
tabTextChoose:{
color:Color.f3474b
},
tabImage:{
width:SCALE(42),
height:SCALE(42),
},
}
componentDidMount () { /** * 将单击回调函数作为参数传递 */ this.props.navigation.setParams({ switch: () => this.switchView() }); }
/** * 切换视图 */ switchView() { alert('切换') }
static navigationOptions = ({navigation,screenProps}) => ({ headerTitle: '企业服务', headerTitleStyle: CommonStyles.headerTitleStyle, headerRight: ( <NavigatorItem icon={ Images.ic_navigator } onPress={ ()=> navigation.state.params.switch() }/> ), headerStyle: CommonStyles.headerStyle });
(9)如何在屏幕控件之外的模块获取当前界面及navigation实例
很多情况下,我们都需要处理登录token失效的情况。例如:在当前设备登录后不退出,此时在另一台设备登录,导致第一个设备用户登录状态失效,此时在第一台设备操作网络请求时,需要提醒用户登录失效,跳转登录界面,并重新登录。
这种需求很常见,关于网络请求我们一般会封装为一个HttpUtil。然后在Component中去调用。此时如果需要处理登录失效的跳转逻辑,需要写在HttpUtil,那么在HttpUtil中就没办法获取navigation来做跳转,那么如何解决呢?下面提供一种方案,很实用:
定义一个Component的基类,包含当前显示的Component实例:screen,以及导航函数。import React, {Component} from 'react';
export default class Base extends Component {
static screen;
constructor(props) {
super(props);
Base.screen = this;
}
nav() {
return this.props.navigation;
}
}在其他组件/模块中,我可以调用它来导航到不同的屏幕:
Base.screen.nav().navigate(...);这样不管在哪个屏幕上,并且可以随时获取导航对象以在需要时重定向用户。
(10)react-navigation高级用法:实现自定义Tab切换效果。
react-navigation 库中提供了实现自定义Router切换的方式,需要用到的组件如下: TabRouter,
createNavigator,
createNavigationContainer1. TabRouter用来自定义路由栈
2. createNavigator用来创建导航组件
3. createNavigationContainer作为导航组件的容器组件
自定义Router切换的流程大致如下:
1. 创建StackNavigator
2. 创建TabRouter
3. 定义导航样式
4. 定义整体路由切换组件
5. 创建Navigator
来看核心代码:// 界面组件
import FirstPage from './scene/FirstPage';
import SecondPage from './scene/SecondPage';
import ThirdPage from './scene/ThirdPage';
import DetailPage from './scene/DetailPage';
// 引入 react-navigation 核心组件 import { TabRouter, StackNavigator, createNavigator, addNavigationHelpers, createNavigationContainer, } from 'react-navigation';
// 创建 3个 StackNavigator const FirstScreen = StackNavigator( { First: { screen: FirstPage }, Detail: { screen: DetailPage } } ); const SecondScreen = StackNavigator( { Second: { screen: SecondPage } } ); const ThirdScreen = StackNavigator( { Third: { screen: ThirdPage } } );
// 定义 TabRouter const FirstScene = ({ navigation }) => ( <FirstScreen /> ); const SecondScene = ({ navigation }) => ( <SecondScreen /> ); const ThirdScene = ({ navigation }) => ( <ThirdScreen /> ); const CustomTabRouter = TabRouter( { First: { screen: FirstScene, path: 'firstScene' }, Second: { screen: SecondScene, path: 'secondScene' }, Third: { screen: ThirdScene, path: 'thirdScene' }, }, { initialRouteName: 'First' } );
// 定义TabBar const CustomTabBar = ({ navigation, activeRouteName }) => { const { routes } = navigation.state; return ( <View style={ styles.tabContainer }> <ScrollView> { routes.map((route, index)=>( <TouchableOpacity key={ index } onPress={() => navigation.navigate(route.routeName)}> <Text style={[ styles.tabbarText, activeRouteName === route.routeName ? styles.active : styles.inactive ]}> { route.routeName } </Text> </TouchableOpacity> )) } </ScrollView> </View> ) }
// 定义TabView const CustomTabView = ({ router,navigation }) => { const { routes, index } = navigation.state; const activeRouteName = routes[index].routeName; const ActiveScreen = router.getComponentForRouteName(activeRouteName); return( <View style={ styles.container }> <CustomTabBar navigation={ navigation } activeRouteName={ activeRouteName } /> <ActiveScreen navigation={ addNavigationHelpers( { dispath: navigation.dispatch, state: routes[index] } ) } /> </View> ) }
// 创建Navigator const CustomTabs = createNavigationContainer( createNavigator(CustomTabRouter)(CustomTabView) ) export default CustomTabs;
// Style 样式 const styles = StyleSheet.create({ tabContainer: { width: 86, zIndex: 888, flexDirection:'column', alignItems:'center', justifyContent:'center', backgroundColor: '#e7e7e7', borderRightWidth:1, borderColor: '#e0e0e0' }, tabbarText: { fontSize: 18, fontWeight: 'bold', marginTop: 20, marginBottom: 20, color: 'black' }, active: { color: 'red', }, inactive: { color: 'black', }, container: { flexDirection:'row', flex: 1, } });通过上述代码,我们就可以创建出类似于饿了么App中商品分类的模块切换效果。
(11)定义某个界面的切换动画效果
有时候产品会存在某个界面的切换动画和其他不同,那么如何实现呢?很简单,只需要在StackNavigator中配置参数下声明以下代码:transitionConfig:()=>({
screenInterpolator:
(props)=> {
const { scene } = props
if (scene.route.routeName === 'VIPDetailPage') {
return CardStackStyleInterpolator.forFade
} else {
return CardStackStyleInterpolator.forHorizontal(props)
}
}
})
效果图
自定义TabRouter:相关文章推荐
- [置顶] React Native未来导航者:react-navigation 使用详解(基础篇)
- React Native未来导航者:react-navigation 使用详解
- React Native未来导航者:react-navigation 使用详解
- React Native未来导航者:react-navigation 使用详解
- React Native未来导航者:react-navigation 使用详解
- React Native未来导航者:react-navigation 使用详解
- 【转载】React Native未来导航者:react-navigation 使用详解
- react-navigation 使用详解
- react native使用react-navigation跳转后多页面数据传递总结
- react-navigation使用技巧(进阶篇)
- React Native导航器之react-navigation使用
- React Native react-navigation 导航使用详解
- 官方推荐react-navigation的具体使用详解
- [置顶] React-Navigation与Redux整合详解
- react-navigation使用技巧(进阶篇)
- react-navigation 使用详解
- React Native导航器之react-navigation使用
- React Native填坑之旅 -- 使用react-navigation代替Navigator
- React开发-JSX使用与详解