React Native未来导航者:react-navigation 使用详解(进阶篇)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u013718120/article/details/79461446

刚创建的React Native 微信公众号,欢迎微信扫描关注订阅号,每天定期会分享react native 技术文章,移动技术干货,精彩文章技术推送。同时可以扫描我的微信加入react-native技术交流微信群。欢迎各位大牛,React Native技术爱好者加入交流!

本篇内容为react-navigation的进阶内容以及高级用法。基础篇请看:

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 === NavigationActions.POP
   ) {
        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)自定义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:              

 

猜你喜欢

转载自blog.csdn.net/u013718120/article/details/79461446