react-navigation 使用錦囊

sun168發表於2019-03-04

前言

這裡是針對日常使用react-navigation中的遇到的一些問題對其進行解決而總結出的小技巧。

TabNavigator 和 StackNavigator

簡單瞭解一下 StackNavigator

react-navigation 使用錦囊
如其名就是一個棧,遵循先進後出的原則,每開啟一個screen,screen就會在頁面最頂層的位置。

簡單瞭解一下TabNavigator

react-navigation 使用錦囊

在初始化TabNavigator的時候就會將TabNavigator上的所有screen都進行初始化。通過左右滑動/點選底部的TabBar對應的icon專案進行切換。

而一個TabNavigator也可以作為一個screen放入到StackNavigator中。

在瞭解了其簡單使用原理後,進入到日常可能會遇到的一些問題。

TabNavigator的子screen缺少一些額外的鉤子

由於TabNavigator會在第一次載入的時候例項化其子screen,所以其所以子screen的componentDidMount() 會隨著TabNavigator例項的時候執行。 於是就有了這樣的需要,子screen在螢幕上的時候才向伺服器請求資料,或者是更新資料等邏輯。 目前react-navigation並沒有提供相關的鉤子去幫助我們(見#51),所以我們就有需要去使用一些高階元件為我們的screen新增一些hook。 如https://github.com/pmachowski/react-navigation-is-focused-hoc 這裡提供一個在整合redux後自己實現的一個方案

import React from 'react'
import { connect } from 'react-redux'

import PropTypes from 'prop-types'



function _getCurrentRouteName(navigationState) {
  if (!navigationState) return null
  const route = navigationState.routes[navigationState.index]
  if (route.routes) return _getCurrentRouteName(route)
  return route.routeName
}

/**
 * 給當前screen傳遞isFocused以判斷是否在為當前路由
 * @param {Component} WrappedComponent 
 * @param {string} screenName 
 */
export default function withNavigationFocus(WrappedComponent, screenName) {
  class InnerComponent extends React.Component {
    static propTypes = {
      nav: PropTypes.object,
    }

    static navigationOptions = (props) => {
      if (typeof WrappedComponent.navigationOptions === 'function') {
        return WrappedComponent.navigationOptions(props)
      }
      return { ...WrappedComponent.navigationOptions }
    }

    constructor(props) {
      super(props)
      this.state = {
        isFocused: true,
      }
    }

    componentDidMount() {

    }

    componentWillReceiveProps(nextProps) {
      if (nextProps && nextProps.nav) {
        this._handleNavigationChange(_getCurrentRouteName(nextProps.nav))
      }
    }

    componentWillUnmount() {

    }

    _handleNavigationChange = (routeName) => {
      // update state only when isFocused changes
      if (this.state.isFocused !== (screenName === routeName)) {
        this.setState({
          isFocused: screenName === routeName,
        })
      }
    }

    render() {
      return <WrappedComponent isFocused={this.state.isFocused} {...this.props} />
    }
  }
  return connect(mapStateToProps)(InnerComponent)
}

/*將react-navigation整合到redux中*/
const mapStateToProps = (state) => ({
  nav: state.nav,
})

複製程式碼

通過丟擲引玉可以為相關的其他鉤子實現提供思路

實現一個自定義的tabbar

為什麼會有這個需求,因為設計師總會各種新(sao)的想(cao)法(zuo),例如issues上看到的這個圖

react-navigation 使用錦囊

這裡之前實踐的例子

import React, { Component } from 'react'
import {
  View,
  TouchableOpacity,
  Text,
  StyleSheet,
  Dimensions,
  Image,
} from 'react-native'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'

const { width } = Dimensions.get('window')

function _getCurrentRouteName(navigationState) {
  if (!navigationState) return null
  const route = navigationState.routes[navigationState.index]
  if (route.routes) return _getCurrentRouteName(route)
  return route.routeName
}

const extraRoutes = [{
  routeName: 'InsuranceTimeline',
  defaultIcon: require('../../assets/tabbar-icon/verify-icon/ic_circle_n.png'),
  selectIcon: require('../../assets/tabbar-icon/verify-icon/ic_circle_s.png'),
  title: 'screen2',
}, {
  routeName: 'InsuranceLesson',
  defaultIcon: require('../../assets/tabbar-icon/verify-icon/ic_umbrella_n.png'),
  selectIcon: require('../../assets/tabbar-icon/verify-icon/ic_umbrella_s.png'),
  title: 'sreen2',
}]

class TabBar extends Component {
  static defaultProps = {
    activeTintColor: '#3478f6', // Default active tint color in iOS 10
    activeBackgroundColor: 'transparent',
    inactiveTintColor: '#929292', // Default inactive tint color in iOS 10
    inactiveBackgroundColor: 'transparent',
    showLabel: true,
    showIcon: true,
  }

  static propTypes = {
    activeTintColor: PropTypes.string,
    inactiveTintColor: PropTypes.string,
    navigation: PropTypes.object,
    showPromoteModal: PropTypes.func,
    nav: PropTypes.object.isRequired,
  }

  renderExtraTabBtns = (props) => {
    const { navigation } = this.props
    const {
      activeTintColor,
      inactiveTintColor,
    } = this.props
    const imageType = isActive ? 'selectedIcon' : 'defaultIcon'
    const color = isActive ? activeTintColor : inactiveTintColor
    const isActive = _getCurrentRouteName(navigation.state) == props.name
    return <TouchableOpacity
      onPress={() => {
        navigation.navigate(props.routeName)
      }}
      style={styles.tab}
      key={props.routeName}
    >
      <Image
        source={props[imageType]}
        style={styles.icon}
      />
      <Text style={{ color, fontSize: 10 }}>{props.title}</Text>
    </TouchableOpacity>
  }

  render() {
    let navigation = this.props.navigation
    let images = [
      {
        default: require('../../assets/tabbar-icon/verify-icon/ic_home_n.png'),
        selected: require('../../assets/tabbar-icon/verify-icon/ic_home_s.png'),
      },
      {
        default: require('../../assets/tabbar-icon/verify-icon/ic_mine_n.png'),
        selected: require('../../assets/tabbar-icon/verify-icon/ic_mine_s.png'),
      },
    ]

    let titles = [
      'screen1',
      'screen2',
    ]
    const { routes, index } = navigation.state
    const {
      activeTintColor,
      inactiveTintColor,
    } = this.props

    const tabBtns = routes.map((route, idx) => {
      const color = (index === idx) ? activeTintColor : inactiveTintColor
      const isActive = index === idx
      const imageType = isActive ? 'selected' : 'default'
      return (
        <TouchableOpacity
          onPress={() => {
            navigation.navigate(route.routeName)
          }}
          style={styles.tab}
          key={route.routeName}
        >
          <Image
            source={images[idx][imageType]}
            style={styles.icon}
          />
          <Text style={{ color, fontSize: 10 }}>{titles[idx]}</Text>
        </TouchableOpacity>
      )
    })

    const extraBtns = extraRoutes.map(route => (
      this.renderExtraTabBtns(route)
    ))

    return (
      <View style={styles.tabContainer}>
        {
          [...tabBtns.slice(0, 1),
            ...extraBtns
            , ...tabBtns.slice(1)]
        }
      </View>
    )
  }
}


const styles = StyleSheet.create({
  tabContainer: {
    borderTopWidth: 1,
    borderTopColor: '#e6e6e6',
    position: 'relative',
    flexDirection: 'row',
    width,
    backgroundColor: '#fff',
    // borderTopColor: theme.primaryColor
  },
  tab: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    height: 55,
  },
  icon: {
    width: 30,
    height: 30,
  },
})

const mapStateToProps = (state) => ({
  nav: state.nav,
})

export default connect(mapStateToProps)(TabBar)


複製程式碼

stackNavigator登入後通過狀態重新整理screen

由於登入後一般是將登入頁reset即將上面的screen直接出棧,而下面的screen是直接呈現在頁面上面,而在單一資料流,有時候沒有觸發其重新整理的資料,此時就通過加一個高階函式

import React, { Component } from 'react'
import { ScrollView, RefreshControl } from 'react-native'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'

/**
 * 
 * @param {Component} WrappedComponent 需要套的高階元件
 * @param {Array}  extraKeys 需要更新資料的額外key
 * 連線了redux中的user
 * 通過受控元件的fetchData更新資料
 * (可以傳入extraKeys)
 */
const AuthComponent = (WrappedComponent, extraKeys = [], scroll = false) => {
  class InnerComponent extends Component {
    static navigationOptions = (props) => {
      if (typeof WrappedComponent.navigationOptions === 'function') {
        return WrappedComponent.navigationOptions(props)
      }
      return { ...WrappedComponent.navigationOptions }
    }
    static propTypes = {
      fetchData: PropTypes.func,
      user: PropTypes.object,
    }

    constructor(props) {
      super(props)
      this.state = {
        refreshing: false,
      }
    }

    /**
     * 執行獲取資料(子控制元件通過這個方法重新整理資料)
     */
    fetchData = (nextProps) => {
      if (this.wrappedComponent.fetchData) {
        //傳遞下一次props
        this.wrappedComponent.fetchData(nextProps)
      }
    }

    componentDidMount = () => {
      this.fetchData()
    }

    componentWillReceiveProps(nextProps) {
      if (nextProps.user.isLogin !== this.props.user.isLogin) {
        if (nextProps) {
          this.fetchData(nextProps)
        } else {
          this.fetchData()
        }

      }

      for (let i = 0; i < extraKeys.length; i++) {
        let key = extraKeys[i]
        if (this.props.hasOwnProperty(key)) {
          //只支援淺比較
          if (nextProps[key] !== this.props[key]) {
            this.fetchData(nextProps)
            break
          }
        }
      }
    }

    /**
     * 比對前後屬性
     */
    compare = (now, next) => {
      if (Array.isArray(now) && Array.isArray(next)) {
        if (now.length !== next.length) {
          return false
        }
        return now.every((element, index) => {
          return now[index] === next[index]
        })
      }
    }

    _onRefresh = () => {

    }

    render() {
      if (scroll) {
        return <ScrollView
          refreshControl={
            <RefreshControl
              refreshing={this.state.refreshing}
              onRefresh={this._onRefresh}
            />
          }
        >
          <WrappedComponent
            ref={(wrappedComponent) => this.wrappedComponent = wrappedComponent}
            {...this.props}
          />
        </ScrollView>
      }
      return <WrappedComponent
        ref={(wrappedComponent) => this.wrappedComponent = wrappedComponent}
        {...this.props}
      />

    }
  }
  return connect(mapStateToProps)(InnerComponent)
}


const mapStateToProps = (state) => ({
  user: state.user,
})

export default AuthComponent

複製程式碼

重置 stack狀態

在上面提到了重置stack狀態

    const resetAction = NavigationActions.reset({
      index: 1,
      actions: [
        NavigationActions.navigate({ routeName: 'screen1' }),//重置後的第一層screen
        NavigationActions.navigate({ routeName: 'screen2' }),//重置後的第二層screen(此時是棧頂,即為當前螢幕顯示頁面)
      ],
    })
 this.props.navigation.dispatch(resetAction)
複製程式碼

替換screen

      this.props.dispatch({
        key: 'NearMeMap',
        type: 'ReplaceCurrentScreen',
        routeName: routeName,
      })
複製程式碼

相關文章