Skip to content

Commit

Permalink
feat: shifting scene animation for BottomNavigator (#3176)
Browse files Browse the repository at this point in the history
Co-authored-by: Andrei Alecu <[email protected]>
  • Loading branch information
andreialecu and andreialecu authored Jul 7, 2022
1 parent c1b1c58 commit 5fcc794
Show file tree
Hide file tree
Showing 6 changed files with 1,700 additions and 45 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 7 additions & 1 deletion docs/pages/10.migration-guide-to-5.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,12 @@ It's worth to mention that default value of prop `shifting` depends on the theme
* <b>3</b> - it's `false`,
* <b>2</b> - it's `true` when there are more than 3 tabs.

Two additional props that control the scene animation were introduced that control the animation of the tabs when `sceneAnimationEnabled` is `true`:
* `sceneAnimationType: "opacity" | "shifting" | undefined` - defines the animation type for the scene. `shifting` enables a new animation where navigating to a scene will shift it horizontally into view. Both `opacity` and `undefined` have the same effect, fading the scene into view.
* `sceneAnimationEasing` allows specifying a custom easing function for the scene animation.

![shiftingAnimation](screenshots/bottom-navigation-shifting.gif)

On a final note, please be aware that `BottomNavigation` with theme version 3 doesn't have a shadow.

## Divider
Expand Down Expand Up @@ -471,4 +477,4 @@ From this place I would like to thank:
- [Olimpia Zurek](https://github.com/OlimpiaZurek) for her contribution and help,
- [Aleksandra Desmurs-Linczewska](https://github.com/p-syche), [Jan Jaworek](https://github.com/jaworek) and [Kewin Wereszczyński](https://github.com/kwereszczynski) for checking and testing changes as well as providing valuable feedback,

and, <i>last but not least</i>, [Satya Sahoo](https://github.com/satya164) for his mentoring during the process.
and, <i>last but not least</i>, [Satya Sahoo](https://github.com/satya164) for his mentoring during the process.
104 changes: 89 additions & 15 deletions example/src/Examples/BottomNavigationExample.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import * as React from 'react';
import { View, Image, Dimensions, StyleSheet, Platform } from 'react-native';
import { BottomNavigation, useTheme } from 'react-native-paper';
import {
View,
Image,
Dimensions,
StyleSheet,
Platform,
Easing,
} from 'react-native';
import { Appbar, BottomNavigation, Menu, useTheme } from 'react-native-paper';
import ScreenWrapper from '../ScreenWrapper';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import type { StackNavigationProp } from '@react-navigation/stack';

type RoutesState = Array<{
key: string;
Expand All @@ -17,6 +25,12 @@ type RoutesState = Array<{

type Route = { route: { key: string } };

type Props = {
navigation: StackNavigationProp<{}>;
};

const MORE_ICON = Platform.OS === 'ios' ? 'dots-horizontal' : 'dots-vertical';

const PhotoGallery = ({ route }: Route) => {
const PHOTOS = Array.from({ length: 24 }).map(
(_, i) => `https://unsplash.it/300/300/?random&__id=${route.key}${i}`
Expand All @@ -33,10 +47,16 @@ const PhotoGallery = ({ route }: Route) => {
);
};

const BottomNavigationExample = () => {
const BottomNavigationExample = ({ navigation }: Props) => {
const { isV3 } = useTheme();
const insets = useSafeAreaInsets();
const [index, setIndex] = React.useState(0);
const [menuVisible, setMenuVisible] = React.useState(false);
const [sceneAnimation, setSceneAnimation] =
React.useState<
React.ComponentProps<typeof BottomNavigation>['sceneAnimationType']
>();

const [routes] = React.useState<RoutesState>([
{
key: 'album',
Expand Down Expand Up @@ -73,19 +93,70 @@ const BottomNavigationExample = () => {
},
]);

React.useLayoutEffect(() => {
navigation.setOptions({
headerShown: false,
});
}, [navigation]);

return (
<BottomNavigation
safeAreaInsets={{ bottom: insets.bottom }}
navigationState={{ index, routes }}
onIndexChange={setIndex}
labelMaxFontSizeMultiplier={2}
renderScene={BottomNavigation.SceneMap({
album: PhotoGallery,
library: PhotoGallery,
favorites: PhotoGallery,
purchased: PhotoGallery,
})}
/>
<View style={styles.screen}>
<Appbar.Header elevated>
<Appbar.BackAction onPress={() => navigation.goBack()} />
<Appbar.Content title="Bottom Navigation" />
<Menu
visible={menuVisible}
onDismiss={() => setMenuVisible(false)}
anchor={
<Appbar.Action
icon={MORE_ICON}
onPress={() => setMenuVisible(true)}
{...(!isV3 && { color: 'white' })}
/>
}
>
<Menu.Item
trailingIcon={sceneAnimation === undefined ? 'check' : undefined}
onPress={() => {
setSceneAnimation(undefined);
setMenuVisible(false);
}}
title="Scene animation: none"
/>
<Menu.Item
trailingIcon={sceneAnimation === 'shifting' ? 'check' : undefined}
onPress={() => {
setSceneAnimation('shifting');
setMenuVisible(false);
}}
title="Scene animation: shifting"
/>
<Menu.Item
trailingIcon={sceneAnimation === 'opacity' ? 'check' : undefined}
onPress={() => {
setSceneAnimation('opacity');
setMenuVisible(false);
}}
title="Scene animation: opacity"
/>
</Menu>
</Appbar.Header>
<BottomNavigation
safeAreaInsets={{ bottom: insets.bottom }}
navigationState={{ index, routes }}
onIndexChange={setIndex}
labelMaxFontSizeMultiplier={2}
renderScene={BottomNavigation.SceneMap({
album: PhotoGallery,
library: PhotoGallery,
favorites: PhotoGallery,
purchased: PhotoGallery,
})}
sceneAnimationEnabled={sceneAnimation !== undefined}
sceneAnimationType={sceneAnimation}
sceneAnimationEasing={Easing.ease}
/>
</View>
);
};

Expand Down Expand Up @@ -126,4 +197,7 @@ const styles = StyleSheet.create({
flex: 1,
resizeMode: 'cover',
},
screen: {
flex: 1,
},
});
62 changes: 59 additions & 3 deletions src/components/BottomNavigation/BottomNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
StyleProp,
Platform,
ViewStyle,
EasingFunction,
} from 'react-native';
import { getBottomSpace } from 'react-native-iphone-x-helper';
import color from 'color';
Expand Down Expand Up @@ -211,6 +212,17 @@ type Props = {
* Specify `sceneAnimationEnabled` as `false` to disable the animation.
*/
sceneAnimationEnabled?: boolean;
/**
* @supported Available in v5.x
* The scene animation effect. Specify `'shifting'` for a different effect.
* By default, 'opacity' will be used.
*/
sceneAnimationType?: 'opacity' | 'shifting';
/**
* @supported Available in v5.x
* The scene animation Easing.
*/
sceneAnimationEasing?: EasingFunction | undefined;
/**
* Whether the bottom navigation bar is hidden when keyboard is shown.
* On Android, this works best when [`windowSoftInputMode`](https://developer.android.com/guide/topics/manifest/activity-element#wsoft) is set to `adjustResize`.
Expand Down Expand Up @@ -354,6 +366,8 @@ const BottomNavigation = ({
style,
theme,
sceneAnimationEnabled = false,
sceneAnimationType = 'opacity',
sceneAnimationEasing,
onTabPress,
onIndexChange,
shifting = theme.isV3 ? false : navigationState.routes.length > 3,
Expand All @@ -380,6 +394,16 @@ const BottomNavigation = ({
)
);

/**
* Active state of individual tab item positions:
* -1 if they're before the active tab, 0 if they're active, 1 if they're after the active tab
*/
const tabsPositionAnims = useAnimatedValueArray(
navigationState.routes.map((_, i) =>
i === navigationState.index ? 0 : i >= navigationState.index ? 1 : -1
)
);

/**
* The top offset for each tab item to position it offscreen.
* Placing items offscreen helps to save memory usage for inactive screens with removeClippedSubviews.
Expand Down Expand Up @@ -458,6 +482,15 @@ const BottomNavigation = ({
toValue: i === index ? 1 : 0,
duration: theme.isV3 || shifting ? 150 * scale : 0,
useNativeDriver: true,
easing: sceneAnimationEasing,
})
),
...navigationState.routes.map((_, i) =>
Animated.timing(tabsPositionAnims[i], {
toValue: i === index ? 0 : i >= index ? 1 : -1,
duration: theme.isV3 || shifting ? 150 * scale : 0,
useNativeDriver: true,
easing: sceneAnimationEasing,
})
),
]).start(({ finished }) => {
Expand Down Expand Up @@ -489,6 +522,8 @@ const BottomNavigation = ({
rippleAnim,
scale,
tabsAnims,
tabsPositionAnims,
sceneAnimationEasing,
theme,
]
);
Expand Down Expand Up @@ -629,7 +664,10 @@ const BottomNavigation = ({
const focused = navigationState.index === index;

const opacity = sceneAnimationEnabled
? tabsAnims[index]
? tabsPositionAnims[index].interpolate({
inputRange: [-1, 0, 1],
outputRange: [0, 1, 0],
})
: focused
? 1
: 0;
Expand All @@ -643,6 +681,16 @@ const BottomNavigation = ({
? 0
: FAR_FAR_AWAY;

const left =
sceneAnimationType === 'shifting'
? tabsPositionAnims[index].interpolate({
inputRange: [-1, 0, 1],
outputRange: [-50, 0, 50],
})
: 0;

const zIndex = focused ? 1 : 0;

return (
<BottomNavigationRouteScreen
key={route.key}
Expand All @@ -653,15 +701,23 @@ const BottomNavigation = ({
}
index={index}
visibility={opacity}
style={[StyleSheet.absoluteFill, { opacity }]}
style={[StyleSheet.absoluteFill, { zIndex }]}
collapsable={false}
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' ? navigationState.index !== index : true
}
>
<Animated.View style={[styles.content, { top }]}>
<Animated.View
style={[
styles.content,
{
opacity: opacity,
transform: [{ translateX: left }, { translateY: top }],
},
]}
>
{renderScene({ route, jumpTo })}
</Animated.View>
</BottomNavigationRouteScreen>
Expand Down
Loading

0 comments on commit 5fcc794

Please sign in to comment.