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

Commit

Permalink
feat: lazy initialized MaterialTopTabNavigator routes (#9)
Browse files Browse the repository at this point in the history
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)

-->
  • Loading branch information
charpeni authored and satya164 committed Aug 18, 2019
1 parent 79e1dac commit 18fa131
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,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 packages/bottom-tabs/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,
};

_renderIcon = ({ focused, route, tintColor }) => {
Expand Down Expand Up @@ -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 });
Expand All @@ -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;

Expand Down Expand Up @@ -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={
Expand All @@ -148,4 +264,6 @@ class MaterialTabView extends React.PureComponent<Props> {
}
}

polyfill(MaterialTabView);

export default createTabNavigator(MaterialTabView);
10 changes: 9 additions & 1 deletion packages/bottom-tabs/src/utils/createTabNavigator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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 }));

Expand Down
10 changes: 5 additions & 5 deletions packages/bottom-tabs/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

0 comments on commit 18fa131

Please sign in to comment.