Skip to content
This repository has been archived by the owner on Feb 25, 2020. It is now read-only.

Lazy initialized MaterialTopTabNavigator routes #9

Merged
merged 5 commits into from
Jul 26, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/navigators/createBottomTabNavigator.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ class TabNavigationView extends React.PureComponent<Props, State> {
StyleSheet.absoluteFill,
{ opacity: isFocused ? 1 : 0 },
]}
isFocused={isFocused}
isVisible={isFocused}
>
{renderScene({ route })}
</ResourceSavingScene>
Expand Down
138 changes: 128 additions & 10 deletions src/navigators/createMaterialTopTabNavigator.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/* @flow */

import * as React from 'react';
import { Platform } from 'react-native';
import { View, Platform } from 'react-native';
import { polyfill } from 'react-lifecycles-compat';
import { TabView, PagerPan } from 'react-native-tab-view';
import createTabNavigator, {
type InjectedProps,
Expand All @@ -13,10 +14,19 @@ import ResourceSavingScene from '../views/ResourceSavingScene';

type Props = InjectedProps & {
animationEnabled?: boolean,
lazy?: boolean,
optimizationsEnabled?: boolean,
swipeEnabled?: boolean,
tabBarPosition?: 'top' | 'bottom',
tabBarComponent?: React.ComponentType<*>,
tabBarOptions?: TabBarOptions,
tabBarPosition?: 'top' | 'bottom',
};

type State = {
index: number,
isSwiping: boolean,
loaded: Array<number>,
transitioningFromIndex: ?number,
};

class MaterialTabView extends React.PureComponent<Props> {
Expand All @@ -25,6 +35,31 @@ class MaterialTabView extends React.PureComponent<Props> {
initialLayout: Platform.select({
android: { width: 1, height: 0 },
}),
animationEnabled: true,
lazy: false,
optimizationsEnabled: false,
};

static getDerivedStateFromProps(nextProps, prevState) {
const { index } = nextProps.navigation.state;

if (prevState.index === index) {
return null;
}

return {
loaded: prevState.loaded.includes(index)
? prevState.loaded
: [...prevState.loaded, index],
index,
};
}

state = {
index: 0,
isSwiping: false,
loaded: [this.props.navigation.state.index],
transitioningFromIndex: null,
};

_getLabel = ({ route, tintColor, focused }) => {
Expand Down Expand Up @@ -98,22 +133,98 @@ class MaterialTabView extends React.PureComponent<Props> {

_renderPanPager = props => <PagerPan {...props} />;

_handleAnimationEnd = () => {
const { lazy } = this.props;

if (lazy) {
this.setState({
transitioningFromIndex: null,
isSwiping: false,
});
}
};

_handleSwipeStart = () => {
const { navigation, lazy } = this.props;

if (lazy) {
this.setState({
isSwiping: true,
loaded: [
...new Set([
...this.state.loaded,
Math.max(navigation.state.index - 1, 0),
Math.min(
navigation.state.index + 1,
navigation.state.routes.length - 1
),
]),
],
});
}
};

_handleIndexChange = index => {
const { animationEnabled, navigation, onIndexChange, lazy } = this.props;

if (lazy && animationEnabled) {
this.setState({
transitioningFromIndex: navigation.state.index || 0,
});
}

onIndexChange(index);
};

_mustBeVisible = ({ index, focused }) => {
const { animationEnabled, navigation } = this.props;
const { isSwiping, transitioningFromIndex } = this.state;

if (isSwiping) {
const isSibling =
navigation.state.index === index - 1 ||
navigation.state.index === index + 1;

if (isSibling) {
return true;
}
}

// The previous tab should remain visible while transitioning
if (animationEnabled && transitioningFromIndex === index) {
return true;
}

return focused;
};

_renderScene = ({ route }) => {
const {
renderScene,
animationEnabled,
swipeEnabled,
descriptors,
lazy,
optimizationsEnabled,
} = this.props;

if (animationEnabled === false && swipeEnabled === false) {
if (lazy) {
const { loaded } = this.state;
const { routes } = this.props.navigation.state;
const index = routes.findIndex(({ key }) => key === route.key);
const { navigation } = descriptors[route.key];

return (
<ResourceSavingScene isFocused={navigation.isFocused()}>
{renderScene({ route })}
</ResourceSavingScene>
);
const mustBeVisible = this._mustBeVisible({ index, focused: navigation.isFocused()});

if (!loaded.includes(index) && !mustBeVisible) {
return <View />;
}

if (optimizationsEnabled) {
return (
<ResourceSavingScene isVisible={mustBeVisible}>
{renderScene({ route })}
</ResourceSavingScene>
);
}
}

return renderScene({ route });
Expand All @@ -125,6 +236,8 @@ class MaterialTabView extends React.PureComponent<Props> {
animationEnabled,
// eslint-disable-next-line no-unused-vars
renderScene,
// eslint-disable-next-line no-unused-vars
onIndexChange,
...rest
} = this.props;

Expand Down Expand Up @@ -155,6 +268,9 @@ class MaterialTabView extends React.PureComponent<Props> {
navigationState={navigation.state}
animationEnabled={animationEnabled}
swipeEnabled={swipeEnabled}
onAnimationEnd={this._handleAnimationEnd}
onIndexChange={this._handleIndexChange}
onSwipeStart={this._handleSwipeStart}
renderPager={renderPager}
renderTabBar={this._renderTabBar}
renderScene={
Expand All @@ -166,4 +282,6 @@ class MaterialTabView extends React.PureComponent<Props> {
}
}

polyfill(MaterialTabView);

export default createTabNavigator(MaterialTabView);
10 changes: 9 additions & 1 deletion src/utils/createTabNavigator.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export type InjectedProps = {
};

export default function createTabNavigator(TabView: React.ComponentType<*>) {
class NavigationView extends React.Component<*> {
class NavigationView extends React.Component<*, State> {
_renderScene = ({ route }) => {
const { screenProps, descriptors } = this.props;
const descriptor = descriptors[route.key];
Expand Down Expand Up @@ -132,6 +132,14 @@ export default function createTabNavigator(TabView: React.ComponentType<*>) {
this._jumpTo(this.props.navigation.state.routes[index].routeName);
};

_handleSwipeStart = () => {
this.setState({ isSwiping: true });
};

_handleSwipeEnd = () => {
this.setState({ isSwiping: false });
};

_jumpTo = routeName =>
this.props.navigation.dispatch(NavigationActions.navigate({ routeName }));

Expand Down
10 changes: 5 additions & 5 deletions src/views/ResourceSavingScene.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as React from 'react';
import { Platform, StyleSheet, View } from 'react-native';

type Props = {
isFocused: boolean,
isVisible: boolean,
children: React.Node,
style?: any,
};
Expand All @@ -13,7 +13,7 @@ const FAR_FAR_AWAY = 3000; // this should be big enough to move the whole view o

export default class ResourceSavingScene extends React.Component<Props> {
render() {
const { isFocused, children, style, ...rest } = this.props;
const { isVisible, children, style, ...rest } = this.props;

return (
<View
Expand All @@ -22,12 +22,12 @@ export default class ResourceSavingScene extends React.Component<Props> {
removeClippedSubviews={
// On iOS, set removeClippedSubviews to true only when not focused
// This is an workaround for a bug where the clipped view never re-appears
Platform.OS === 'ios' ? !isFocused : true
Platform.OS === 'ios' ? !isVisible : true
}
pointerEvents={isFocused ? 'auto' : 'none'}
pointerEvents={isVisible ? 'auto' : 'none'}
{...rest}
>
<View style={isFocused ? styles.attached : styles.detached}>
<View style={isVisible ? styles.attached : styles.detached}>
{children}
</View>
</View>
Expand Down