React Native未來導航者:react-navigation 使用詳解(進階篇)
剛建立的React Native 微信公眾號,歡迎微信掃描關注訂閱號,每天定期會分享react native 技術文章,移動技術乾貨,精彩文章技術推送。同時可以掃描我的微信加入react-native技術交流微信群。歡迎各位大牛,React Native技術愛好者加入交流!
本篇內容為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 ||
action.type === StackActions.POP
) {
const { key, n, immediate } = action;
let backRouteIndex = state.index;
if (action.type === StackActions.POP && n != null) {
// determine the index to go back *from*. In this case, n=1 means to go
// back from state.index, as if it were a normal "BACK" action
backRouteIndex = Math.max(1, state.index - n + 1);
} else 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,
isTransitioning: immediate !== true,
};
}
}
(8)自定義Tab
import 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,
createNavigationContainer
1. 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: