redbadger/pride-london-app

View on GitHub
src/components/NavigationTabBar.js

Summary

Maintainability
C
1 day
Test Coverage
// @flow
import React from "react";
import { Animated, StyleSheet, View } from "react-native";
import type { TabScene, _TabBarBottomProps } from "react-navigation";
import SafeAreaView from "react-native-safe-area-view";
import {
  tabBarBgColor,
  tabBarLabelColor,
  tabBarActiveLineColor,
  tabBarActiveLabelColor,
  tabBarBorderColor
} from "../constants/colors";
import Text from "./Text";
import Touchable from "./Touchable";

class NavigationTabBar extends React.PureComponent<_TabBarBottomProps> {
  constructor() {
    super();
    this.tabBarWidth = new Animated.Value(1);
    this.tabLayouts = {};
    this.activeTabX = new Animated.Value(0);
    this.activeTabWidth = new Animated.Value(0);
  }

  componentDidUpdate() {
    this.updateActiveTabLine();
  }

  tabBarLayout: any;
  tabBarWidth: Animated.Value;
  tabLayouts: { [number]: any };
  activeTabX: Animated.Value;
  activeTabWidth: Animated.Value;

  handleTabPress = (key: string) => {
    this.props.jumpTo(key);
  };

  updateActiveTabLine = () => {
    const { navigation } = this.props;
    const currentIndex = navigation.state.index;
    const layout = this.tabLayouts[currentIndex];
    if (!layout || !this.tabBarLayout) {
      return;
    }

    Animated.timing(this.tabBarWidth, {
      toValue: this.tabBarLayout.width,
      duration: 150,
      useNativeDriver: true
    }).start();
    Animated.timing(this.activeTabX, {
      toValue: layout.x,
      duration: 150,
      useNativeDriver: true
    }).start();
    Animated.timing(this.activeTabWidth, {
      toValue: layout.width,
      duration: 150,
      useNativeDriver: true
    }).start();
  };

  renderLabel = (scene: TabScene) => {
    const { focused } = scene;
    const color = focused ? tabBarActiveLabelColor : tabBarLabelColor;
    const label = this.props.getLabelText(scene);

    return (
      <Text type="tabBarItem" numberOfLines={1} style={{ color }}>
        {label}
      </Text>
    );
  };

  renderIcon = (scene: TabScene) => {
    const { route, index, focused } = scene;
    const opacity = focused ? 1 : 0;
    const opacitInv = focused ? 0 : 1;

    // We render the icon twice at the same position on top of each other:
    // active and inactive one, so we can fade between them.
    return (
      <View style={styles.iconContainer}>
        <View style={[styles.icon, { opacity }]}>
          {this.props.renderIcon({
            route,
            index,
            focused: true
          })}
        </View>
        <View style={[styles.icon, { opacity: opacitInv }]}>
          {this.props.renderIcon({
            route,
            index,
            focused: false
          })}
        </View>
      </View>
    );
  };

  render() {
    const { navigation, getTabTestID } = this.props;
    const { routes } = navigation.state;

    const activeTabLineScale = Animated.divide(
      this.activeTabWidth,
      this.tabBarWidth
    );
    const activeTabLineTranslate = Animated.add(
      this.activeTabX,
      Animated.add(
        Animated.divide(this.tabBarWidth, -2),
        Animated.divide(this.activeTabWidth, 2)
      )
    );

    return (
      <SafeAreaView
        style={styles.tabBarWrapper}
        forceInset={{ bottom: "always", top: "never" }}
      >
        <View
          testID="tab-bar"
          onLayout={event => {
            this.tabBarLayout = event.nativeEvent.layout;
            this.updateActiveTabLine();
          }}
          style={styles.tabBar}
        >
          {routes.map((route, index) => {
            const focused = index === navigation.state.index;
            const scene = { route, index, focused };
            return (
              <Touchable
                key={route.key}
                testID={getTabTestID(route.routeName)}
                onLayout={event => {
                  this.tabLayouts[index] = event.nativeEvent.layout;
                  this.updateActiveTabLine();
                }}
                onPress={() => this.handleTabPress(route.key)}
                style={styles.tab}
              >
                {this.renderIcon(scene)}
                {this.renderLabel(scene)}
              </Touchable>
            );
          })}
          <Animated.View
            style={[
              styles.activeTabLine,
              {
                transform: [
                  { translateX: activeTabLineTranslate },
                  { scaleX: activeTabLineScale }
                ]
              }
            ]}
          />
        </View>
      </SafeAreaView>
    );
  }
}

const styles = StyleSheet.create({
  tabBarWrapper: {
    backgroundColor: tabBarBgColor,
    borderTopWidth: StyleSheet.hairlineWidth,
    borderTopColor: tabBarBorderColor,
    paddingHorizontal: 8
  },
  activeTabLine: {
    backgroundColor: tabBarActiveLineColor,
    height: 3,
    position: "absolute",
    left: 0,
    top: 0,
    width: "100%"
  },
  tabBar: {
    flexDirection: "row",
    height: 52,
    width: "100%",
    maxWidth: 440,
    alignSelf: "center"
  },
  tab: {
    alignItems: "center",
    flexDirection: "column",
    minWidth: 48,
    flexGrow: 1,
    marginHorizontal: 4,
    paddingTop: 4,
    paddingBottom: 2
  },
  iconContainer: {
    flex: 1,
    width: "100%"
  },
  icon: {
    position: "absolute",
    alignItems: "center",
    alignSelf: "center",
    top: 0
  }
});

export default NavigationTabBar;