feat: initial commit

This commit is contained in:
Satyajit Sahoo
2018-03-17 18:37:54 +01:00
committed by satyajit.happy
parent 7f0486a2af
commit 89934b93c0
10 changed files with 1033 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
{
"presets": ["react-native"]
}

View File

@@ -0,0 +1,14 @@
/* @flow */
/* eslint-disable import/no-commonjs */
module.exports = {
get createBottomTabNavigator() {
return require('./navigators/createBottomTabNavigator').default;
},
get createMaterialTopTabNavigator() {
return require('./navigators/createMaterialTopTabNavigator').default;
},
get createMaterialBottomTabNavigator() {
return require('./navigators/createMaterialBottomTabNavigator').default;
},
};

View File

@@ -0,0 +1,129 @@
/* @flow */
import * as React from 'react';
import { View, StyleSheet } from 'react-native';
import createTabNavigator, {
type InjectedProps,
} from '../utils/createTabNavigator';
import TabBarBottom, { type TabBarOptions } from '../views/TabBarBottom';
type Props = InjectedProps & {
tabBarComponent?: React.ComponentType<*>,
tabBarOptions?: TabBarOptions,
};
type State = {
loaded: number[],
};
class TabNavigationView extends React.PureComponent<Props, State> {
state = {
loaded: [this.props.navigation.state.index],
};
componentWillReceiveProps(nextProps) {
if (
nextProps.navigation.state.index !== this.props.navigation.state.index
) {
const { index } = nextProps.navigation.state;
this.setState(state => ({
loaded: state.loaded.includes(index)
? state.loaded
: [...state.loaded, index],
}));
}
}
_getLabel = ({ route, focused, tintColor }) => {
const label = this.props.getLabelText({ route });
if (typeof label === 'function') {
return label({ focused, tintColor });
}
return label;
};
_renderTabBar = () => {
const {
tabBarComponent: TabBarComponent = TabBarBottom,
tabBarOptions,
navigation,
onIndexChange,
screenProps,
getLabelText,
renderIcon,
onTabPress,
} = this.props;
const { descriptors } = this.props;
const { state } = this.props.navigation;
const route = state.routes[state.index];
const descriptor = descriptors[route.key];
const options = descriptor.options;
if (options.tabBarVisible === false) {
return null;
}
return (
<TabBarComponent
{...tabBarOptions}
navigation={navigation}
jumpToIndex={onIndexChange}
screenProps={screenProps}
onTabPress={onTabPress}
getLabelText={getLabelText}
renderIcon={renderIcon}
/>
);
};
render() {
const { navigation, renderScene } = this.props;
const { routes } = navigation.state;
const { loaded } = this.state;
return (
<View style={styles.container}>
<View style={styles.pages}>
{routes.map((route, index) => {
if (!loaded.includes(index)) {
// Don't render a screen if we've never navigated to it
return null;
}
const isFocused = navigation.state.index === index;
return (
<View
key={route.key}
pointerEvents={isFocused ? 'auto' : 'none'}
style={[
StyleSheet.absoluteFill,
{ opacity: isFocused ? 1 : 0 },
]}
>
{renderScene({ route })}
</View>
);
})}
</View>
{this._renderTabBar()}
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
overflow: 'hidden',
},
pages: {
flex: 1,
},
});
export default createTabNavigator(TabNavigationView);

View File

@@ -0,0 +1,40 @@
/* @flow */
import * as React from 'react';
import { BottomNavigation } from 'react-native-paper';
import createTabNavigator, {
type InjectedProps,
} from '../utils/createTabNavigator';
type Props = InjectedProps & {
activeTintColor?: string,
};
class BottomNavigationView extends React.Component<Props> {
_getColor = ({ route }) => {
const { descriptors } = this.props;
const descriptor = descriptors[route.key];
const options = descriptor.options;
return options.tabBarColor;
};
render() {
// eslint-disable-next-line no-unused-vars
const { activeTintColor, navigation, descriptors, ...rest } = this.props;
return (
<BottomNavigation
{...rest}
navigationState={navigation.state}
getColor={this._getColor}
theme={
/* $FlowFixMe */
activeTintColor ? { colors: { primary: activeTintColor } } : null
}
/>
);
}
}
export default createTabNavigator(BottomNavigationView);

View File

@@ -0,0 +1,169 @@
/* @flow */
import * as React from 'react';
import { Platform } from 'react-native';
import { TabViewAnimated, TabViewPagerPan } from 'react-native-tab-view';
import createTabNavigator, {
type InjectedProps,
} from '../utils/createTabNavigator';
import TabBarTop, { type TabBarOptions } from '../views/TabBarTop';
type Props = InjectedProps & {
animationEnabled?: boolean,
swipeEnabled?: boolean,
tabBarPosition?: 'top' | 'bottom',
tabBarComponent?: React.ComponentType<*>,
tabBarOptions?: TabBarOptions,
};
class TabView extends React.PureComponent<Props> {
static defaultProps = {
// fix for https://github.com/react-native-community/react-native-tab-view/issues/312
initialLayout: Platform.select({
android: { width: 1, height: 0 },
}),
};
_getLabel = ({ route, tintColor, focused }) => {
const { descriptors } = this.props;
const descriptor = descriptors[route.key];
const options = descriptor.options;
if (options.tabBarLabel) {
return typeof options.tabBarLabel === 'function'
? options.tabBarLabel({ tintColor, focused })
: options.tabBarLabel;
}
if (typeof options.title === 'string') {
return options.title;
}
return route.routeName;
};
_getOnPress = (previousScene, { route }) => {
const { descriptors } = this.props;
const descriptor = descriptors[route.key];
const options = descriptor.options;
return options.tabBarOnPress;
};
_getTestIDProps = ({ route, focused }) => {
const { descriptors } = this.props;
const descriptor = descriptors[route.key];
const options = descriptor.options;
return typeof options.tabBarTestIDProps === 'function'
? options.tabBarTestIDProps({ focused })
: options.tabBarTestIDProps;
};
_renderIcon = ({ focused, route, tintColor }) => {
const { descriptors } = this.props;
const descriptor = descriptors[route.key];
const options = descriptor.options;
if (options.tabBarIcon) {
return typeof options.tabBarIcon === 'function'
? options.tabBarIcon({ tintColor, focused })
: options.tabBarIcon;
}
return null;
};
_renderTabBar = props => {
const { state } = this.props.navigation;
const route = state.routes[state.index];
const { descriptors } = this.props;
const descriptor = descriptors[route.key];
const options = descriptor.options;
const tabBarVisible =
options.tabBarVisible == null ? true : options.tabBarVisible;
const {
tabBarComponent: TabBarComponent = TabBarTop,
tabBarPosition,
tabBarOptions,
} = this.props;
if (TabBarComponent === null || !tabBarVisible) {
return null;
}
return (
<TabBarComponent
{...tabBarOptions}
{...props}
tabBarPosition={tabBarPosition}
screenProps={this.props.screenProps}
navigation={this.props.navigation}
getLabelText={this.props.getLabelText}
getTestIDProps={this._getTestIDProps}
renderIcon={this._renderIcon}
/>
);
};
_renderPanPager = props => <TabViewPagerPan {...props} />;
render() {
const {
navigation,
tabBarPosition,
animationEnabled,
renderScene,
...rest
} = this.props;
let renderHeader;
let renderFooter;
let renderPager;
const { state } = this.props.navigation;
const route = state.routes[state.index];
const { descriptors } = this.props;
const descriptor = descriptors[route.key];
const options = descriptor.options;
let swipeEnabled =
options.swipeEnabled == null
? this.props.swipeEnabled
: options.swipeEnabled;
if (typeof swipeEnabled === 'function') {
swipeEnabled = swipeEnabled(state);
}
if (tabBarPosition === 'bottom') {
renderFooter = this._renderTabBar;
} else {
renderHeader = this._renderTabBar;
}
if (animationEnabled === false && swipeEnabled === false) {
renderPager = this._renderPanPager;
}
return (
<TabViewAnimated
{...rest}
navigationState={navigation.state}
animationEnabled={animationEnabled}
swipeEnabled={swipeEnabled}
renderScene={
/* $FlowFixMe */
renderScene
}
renderPager={renderPager}
renderHeader={renderHeader}
renderFooter={renderFooter}
/>
);
}
}
export default createTabNavigator(TabView);

View File

@@ -0,0 +1,133 @@
/* @flow */
import * as React from 'react';
import {
TabRouter,
NavigationActions,
createNavigator,
createNavigationContainer,
} from 'react-navigation';
import SceneView from 'react-navigation/src/views/SceneView';
export type InjectedProps = {
getLabelText: (props: { route: any }) => any,
renderIcon: (props: {
route: any,
focused: boolean,
tintColor: string,
}) => React.Node,
renderScene: (props: { route: any }) => ?React.Node,
onIndexChange: (index: number) => any,
onTabPress: (props: { route: any }) => mixed,
navigation: any,
descriptors: any,
screenProps?: any,
};
export default function createTabNavigator(TabView: React.ComponentType<*>) {
class NavigationView extends React.Component<*> {
_renderScene = ({ route }) => {
const { screenProps, descriptors } = this.props;
const descriptor = descriptors[route.key];
const TabComponent = descriptor.getComponent();
return (
<SceneView
screenProps={screenProps}
navigation={descriptor.navigation}
component={TabComponent}
/>
);
};
_renderIcon = ({ route, focused = true, tintColor }) => {
const { descriptors } = this.props;
const descriptor = descriptors[route.key];
const options = descriptor.options;
if (options.tabBarIcon) {
return typeof options.tabBarIcon === 'function'
? options.tabBarIcon({ focused, tintColor })
: options.tabBarIcon;
}
return null;
};
_getLabelText = ({ route }) => {
const { descriptors } = this.props;
const descriptor = descriptors[route.key];
const options = descriptor.options;
if (options.tabBarLabel) {
return options.tabBarLabel;
}
if (typeof options.title === 'string') {
return options.title;
}
return route.routeName;
};
_handleOnTabPress = ({ route }) => {
const { descriptors } = this.props;
const descriptor = descriptors[route.key];
const { navigation, options } = descriptor;
if (options.tabBarOnPress) {
options.tabBarOnPress({
navigation,
});
} else {
const isFocused =
this.props.navigation.state.index ===
this.props.navigation.state.routes.indexOf(route);
if (isFocused) {
if (route.hasOwnProperty('index') && route.index > 0) {
navigation.dispatch(NavigationActions.popToTop({ key: route.key }));
} else {
// TODO: do something to scroll to top
}
}
}
};
_handleIndexChange = index => {
const { navigation } = this.props;
navigation.navigate(navigation.state.routes[index].routeName);
};
render() {
const { descriptors, navigation, screenProps } = this.props;
const { state } = navigation;
const route = state.routes[state.index];
const descriptor = descriptors[route.key];
const options = {
...this.props.navigationConfig,
...descriptor.options,
};
return (
<TabView
{...options}
getLabelText={this._getLabelText}
renderIcon={this._renderIcon}
renderScene={this._renderScene}
onIndexChange={this._handleIndexChange}
onTabPress={this._handleOnTabPress}
navigation={navigation}
descriptors={descriptors}
screenProps={screenProps}
/>
);
}
}
return (routes, config = {}) => {
const router = TabRouter(routes, config);
const navigator = createNavigator(NavigationView, router, config);
return createNavigationContainer(navigator);
};
}

View File

@@ -0,0 +1,37 @@
import * as React from 'react';
import { Dimensions } from 'react-native';
import hoistNonReactStatic from 'hoist-non-react-statics';
export const isOrientationLandscape = ({ width, height }) => width > height;
export default function withDimensions(WrappedComponent) {
const { width, height } = Dimensions.get('window');
class EnhancedComponent extends React.Component {
static displayName = `withDimensions(${WrappedComponent.displayName})`;
state = {
dimensions: { width, height },
isLandscape: isOrientationLandscape({ width, height }),
};
componentDidMount() {
Dimensions.addEventListener('change', this.handleOrientationChange);
}
componentWillUnmount() {
Dimensions.removeEventListener('change', this.handleOrientationChange);
}
handleOrientationChange = ({ window }) => {
const isLandscape = isOrientationLandscape(window);
this.setState({ isLandscape });
};
render() {
return <WrappedComponent {...this.props} {...this.state} />;
}
}
return hoistNonReactStatic(EnhancedComponent, WrappedComponent);
}

View File

@@ -0,0 +1,286 @@
/* @flow */
import React from 'react';
import {
Animated,
TouchableWithoutFeedback,
StyleSheet,
View,
Platform,
} from 'react-native';
import SafeAreaView from 'react-native-safe-area-view';
import TabBarIcon from './TabBarIcon';
import withDimensions from '../utils/withDimensions';
export type TabBarOptions = {
activeTintColor?: string,
inactiveTintColor?: string,
activeBackgroundColor?: string,
inactiveBackgroundColor?: string,
allowFontScaling: boolean,
showLabel: boolean,
showIcon: boolean,
labelStyle: any,
tabStyle: any,
adaptive?: boolean,
style: any,
};
type Props = TabBarOptions & {
navigation: any,
descriptors: any,
jumpToIndex: any,
onTabPress: any,
getLabelText: ({ route: any }) => any,
renderIcon: any,
dimensions: { width: number, height: number },
isLandscape: boolean,
};
const majorVersion = parseInt(Platform.Version, 10);
const isIos = Platform.OS === 'ios';
const isIOS11 = majorVersion >= 11 && isIos;
const DEFAULT_MAX_TAB_ITEM_WIDTH = 125;
class TabBarBottom extends React.Component<Props> {
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,
allowFontScaling: true,
adaptive: isIOS11,
};
_renderLabel = ({ route, focused }) => {
const {
activeTintColor,
inactiveTintColor,
labelStyle,
showLabel,
showIcon,
allowFontScaling,
} = this.props;
if (showLabel === false) {
return null;
}
const label = this.props.getLabelText({ route });
const tintColor = focused ? activeTintColor : inactiveTintColor;
if (typeof label === 'string') {
return (
<Animated.Text
numberOfLines={1}
style={[
styles.label,
{ color: tintColor },
showIcon && this._shouldUseHorizontalLabels()
? styles.labelBeside
: styles.labelBeneath,
styles.labelBeneath,
labelStyle,
]}
allowFontScaling={allowFontScaling}
>
{label}
</Animated.Text>
);
}
if (typeof label === 'function') {
return label({ route, focused, tintColor });
}
return label;
};
_renderIcon = ({ route, focused }) => {
const {
navigation,
activeTintColor,
inactiveTintColor,
renderIcon,
showIcon,
showLabel,
} = this.props;
if (showIcon === false) {
return null;
}
const horizontal = this._shouldUseHorizontalLabels();
const activeOpacity = focused ? 1 : 0;
const inactiveOpacity = focused ? 0 : 1;
return (
<TabBarIcon
route={route}
navigation={navigation}
activeOpacity={activeOpacity}
inactiveOpacity={inactiveOpacity}
activeTintColor={activeTintColor}
inactiveTintColor={inactiveTintColor}
renderIcon={renderIcon}
style={[
styles.iconWithExplicitHeight,
styles.iconWithLabel,
showLabel === false && !horizontal && styles.iconWithoutLabel,
showLabel !== false && !horizontal && styles.iconWithLabel,
]}
/>
);
};
_shouldUseHorizontalLabels = () => {
const { routes } = this.props.navigation.state;
const { isLandscape, dimensions, adaptive, tabStyle } = this.props;
if (!adaptive) {
return false;
}
if (Platform.isPad) {
let maxTabItemWidth = DEFAULT_MAX_TAB_ITEM_WIDTH;
const flattenedStyle = StyleSheet.flatten(tabStyle);
if (flattenedStyle) {
if (typeof flattenedStyle.width === 'number') {
maxTabItemWidth = flattenedStyle.width;
} else if (typeof flattenedStyle.maxWidth === 'number') {
maxTabItemWidth = flattenedStyle.maxWidth;
}
}
return routes.length * maxTabItemWidth <= dimensions.width;
} else {
return isLandscape;
}
};
render() {
const {
navigation,
activeBackgroundColor,
inactiveBackgroundColor,
onTabPress,
jumpToIndex,
style,
tabStyle,
} = this.props;
const { routes } = navigation.state;
const tabBarStyle = [
styles.tabBar,
!Platform.isPad ? styles.tabBarCompact : styles.tabBarRegular,
styles.tabBarRegular,
style,
];
return (
<Animated.View style={tabBarStyle}>
<SafeAreaView
style={styles.container}
forceInset={{ bottom: 'always', top: 'never' }}
>
{routes.map((route, index) => {
const focused = index === navigation.state.index;
const scene = { route, focused };
const backgroundColor = focused
? activeBackgroundColor
: inactiveBackgroundColor;
return (
<TouchableWithoutFeedback
key={route.key}
onPress={() => {
jumpToIndex(index);
onTabPress({ route });
}}
>
<View
style={[
styles.tab,
{ backgroundColor },
this._shouldUseHorizontalLabels()
? styles.tabLandscape
: styles.tabPortrait,
styles.tabPortrait,
tabStyle,
]}
>
{this._renderIcon(scene)}
{this._renderLabel(scene)}
</View>
</TouchableWithoutFeedback>
);
})}
</SafeAreaView>
</Animated.View>
);
}
}
const DEFAULT_HEIGHT = 49;
const COMPACT_HEIGHT = 29;
const styles = StyleSheet.create({
tabBar: {
backgroundColor: '#F7F7F7', // Default background color in iOS 10
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: 'rgba(0, 0, 0, .3)',
},
container: {
flex: 1,
flexDirection: 'row',
},
tabBarCompact: {
height: COMPACT_HEIGHT,
},
tabBarRegular: {
height: DEFAULT_HEIGHT,
},
tab: {
flex: 1,
alignItems: isIos ? 'center' : 'stretch',
},
tabPortrait: {
justifyContent: 'flex-end',
flexDirection: 'column',
},
tabLandscape: {
justifyContent: 'center',
flexDirection: 'row',
},
iconWithoutLabel: {
flex: 1,
},
iconWithLabel: {
flex: 1,
},
iconWithExplicitHeight: {
height: Platform.isPad ? DEFAULT_HEIGHT : COMPACT_HEIGHT,
},
label: {
textAlign: 'center',
backgroundColor: 'transparent',
},
labelBeneath: {
fontSize: 10,
marginBottom: 1.5,
},
labelBeside: {
fontSize: 13,
marginLeft: 20,
},
});
export default withDimensions(TabBarBottom);

View File

@@ -0,0 +1,63 @@
/* @flow */
import React from 'react';
import { Animated, View, StyleSheet } from 'react-native';
type Props = {
route: any,
activeOpacity: any,
inactiveOpacity: any,
activeTintColor: any,
inactiveTintColor: any,
renderIcon: any,
style: any,
};
export default class TabBarIcon extends React.Component<Props> {
render() {
const {
route,
activeOpacity,
inactiveOpacity,
activeTintColor,
inactiveTintColor,
renderIcon,
style,
} = this.props;
// 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={style}>
<Animated.View style={[styles.icon, { opacity: activeOpacity }]}>
{renderIcon({
route,
focused: true,
tintColor: activeTintColor,
})}
</Animated.View>
<Animated.View style={[styles.icon, { opacity: inactiveOpacity }]}>
{renderIcon({
route,
focused: false,
tintColor: inactiveTintColor,
})}
</Animated.View>
</View>
);
}
}
const styles = StyleSheet.create({
icon: {
// We render the icon twice at the same position on top of each other:
// active and inactive one, so we can fade between them:
// Cover the whole iconContainer:
position: 'absolute',
alignSelf: 'center',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
width: '100%',
},
});

View File

@@ -0,0 +1,159 @@
/* @flow */
import * as React from 'react';
import { Animated, StyleSheet } from 'react-native';
import { TabBar } from 'react-native-tab-view';
import TabBarIcon from './TabBarIcon';
export type TabBarOptions = {
activeTintColor?: string,
inactiveTintColor?: string,
showLabel?: boolean,
showIcon?: boolean,
upperCaseLabel?: boolean,
labelStyle?: any,
iconStyle?: any,
allowFontScaling?: boolean,
};
type Props = TabBarOptions & {
position: Animated.Value,
offsetX: Animated.Value,
panX: Animated.Value,
layout: any,
navigation: any,
renderIcon: (props: {
route: any,
focused: boolean,
tintColor: string,
}) => React.Node,
getLabelText: (props: { route: any }) => any,
useNativeDriver?: boolean,
jumpToIndex: (index: number) => any,
};
export default class TabBarTop extends React.PureComponent<Props> {
static defaultProps = {
activeTintColor: '#fff',
inactiveTintColor: '#fff',
showIcon: false,
showLabel: true,
upperCaseLabel: true,
allowFontScaling: true,
};
_renderLabel = ({ route, index, focused }) => {
const {
position,
navigation,
activeTintColor,
inactiveTintColor,
showLabel,
upperCaseLabel,
labelStyle,
allowFontScaling,
} = this.props;
if (showLabel === false) {
return null;
}
const { routes } = navigation.state;
// Prepend '-1', so there are always at least 2 items in inputRange
const inputRange = [-1, ...routes.map((x, i) => i)];
const outputRange = inputRange.map(
inputIndex => (inputIndex === index ? activeTintColor : inactiveTintColor)
);
const color = position.interpolate({
inputRange,
outputRange: outputRange,
});
const tintColor = focused ? activeTintColor : inactiveTintColor;
const label = this.props.getLabelText({ route });
if (typeof label === 'string') {
return (
<Animated.Text
style={[styles.label, { color }, labelStyle]}
allowFontScaling={allowFontScaling}
>
{upperCaseLabel ? label.toUpperCase() : label}
</Animated.Text>
);
}
if (typeof label === 'function') {
return label({ focused, tintColor });
}
return label;
};
_renderIcon = ({ route, index }) => {
const {
position,
navigation,
activeTintColor,
inactiveTintColor,
renderIcon,
showIcon,
iconStyle,
} = this.props;
if (showIcon === false) {
return null;
}
// Prepend '-1', so there are always at least 2 items in inputRange
const inputRange = [-1, ...navigation.state.routes.map((x, i) => i)];
const activeOpacity = position.interpolate({
inputRange,
outputRange: inputRange.map(i => (i === index ? 1 : 0)),
});
const inactiveOpacity = position.interpolate({
inputRange,
outputRange: inputRange.map(i => (i === index ? 0 : 1)),
});
return (
<TabBarIcon
route={route}
navigation={navigation}
activeOpacity={activeOpacity}
inactiveOpacity={inactiveOpacity}
activeTintColor={activeTintColor}
inactiveTintColor={inactiveTintColor}
renderIcon={renderIcon}
style={[styles.icon, iconStyle]}
/>
);
};
render() {
/* eslint-disable no-unused-vars */
const { navigation, renderIcon, getLabelText, ...rest } = this.props;
return (
/* $FlowFixMe */
<TabBar
{...rest}
navigationState={navigation.state}
renderIcon={this._renderIcon}
renderLabel={this._renderLabel}
/>
);
}
}
const styles = StyleSheet.create({
icon: {
height: 24,
width: 24,
},
label: {
textAlign: 'center',
fontSize: 13,
margin: 8,
backgroundColor: 'transparent',
},
});