feat: lazy initialized MaterialTopTabNavigator routes (#9)

Routes in `MaterialTopTabNavigator` are now lazy initialized like in `MaterialBottomTabNavigator`.

A scene visibility is computed from multiple states and props:

To handle the pan between tabs, we check if you're currently swiping between tabs and the prop `lazyOnSwipe` is true (default value) or if the tab have been already loaded, we'll check if this tab is a sibling of the focused tab. Then, we'll display the tab if it's a sibling.

~With the prop `animationEnabled` to true, we shouldn't hide a tab before the transition is done. So we're waiting `COMPLETE_TRANSITION` action to hide it. Also, if the prop `sceneAlwaysVisible` is true (default value), we won't hide scenes between A and D while transitioning.~

If the current tab has not been loaded and must not be visible, we do not render it.

I'll update the docs accordingly to this PR.

![tabs-2](https://user-images.githubusercontent.com/7189823/38261082-3bd30d04-3737-11e8-854e-684430db771f.gif)

<!--
#### Default behavior
Tabs are lazy initialized on swipe or focus and are always visible while transitioning.

![tabs-1](https://user-images.githubusercontent.com/7189823/38260989-060f5808-3737-11e8-87ed-d138fec6022b.gif)

#### Hide tabs between while transitioning

```js
{
  sceneAlwaysVisible: false,
}
```

![tabs-2](https://user-images.githubusercontent.com/7189823/38261082-3bd30d04-3737-11e8-854e-684430db771f.gif)

#### Fallback to only lazy initialized tabs on focus

```js
{
  lazyOnSwipe: false,
}
```

![tabs-3](https://user-images.githubusercontent.com/7189823/38261164-7bcc6018-3737-11e8-8758-de71d28218ae.gif)

-->
This commit is contained in:
Nicolas Charpentier
2018-07-25 20:41:02 -04:00
committed by satyajit.happy
parent 79e1dacb13
commit 18fa1315cf
4 changed files with 143 additions and 17 deletions

View File

@@ -110,7 +110,7 @@ class TabNavigationView extends React.PureComponent<Props, State> {
StyleSheet.absoluteFill,
{ opacity: isFocused ? 1 : 0 },
]}
isFocused={isFocused}
isVisible={isFocused}
>
{renderScene({ route })}
</ResourceSavingScene>

View File

@@ -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,
@@ -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> {
@@ -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,
};
_renderIcon = ({ focused, route, tintColor }) => {
@@ -80,22 +115,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 });
@@ -107,6 +218,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;
@@ -137,6 +250,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={
@@ -148,4 +264,6 @@ class MaterialTabView extends React.PureComponent<Props> {
}
}
polyfill(MaterialTabView);
export default createTabNavigator(MaterialTabView);

View File

@@ -29,7 +29,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];
@@ -145,6 +145,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 }));

View File

@@ -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,
};
@@ -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
@@ -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>