feat: add animationTypeForReplace option (#297)

Currently, when a screen is replaced the new screen comes into focus with a push animation. However, sometimes you might want to customize how the animation looks like.

For example, when the user logs out, animating out the previous screen like pop feels more natural than doing a push animation with the sign in screen. The PR adds a new `animationTypeForReplace` option to control this. Specifying `animationTypeForReplace: 'pop'` will pop the previous screen, otherwise the new screen will be pushed like before.

Co-authored-by: Michał Osadnik <micosa97@gmail.com>
This commit is contained in:
Satyajit Sahoo
2020-01-27 18:20:53 +01:00
committed by GitHub
parent a6f58677dc
commit 6262f7298b
3 changed files with 55 additions and 17 deletions

View File

@@ -73,6 +73,7 @@ type Props = {
type State = {
isLoading: boolean;
isSignout: boolean;
userToken: undefined | string;
};
@@ -94,17 +95,20 @@ export default function SimpleStackScreen({ navigation }: Props) {
case 'SIGN_IN':
return {
...prevState,
isSignout: false,
userToken: action.token,
};
case 'SIGN_OUT':
return {
...prevState,
isSignout: true,
userToken: undefined,
};
}
},
{
isLoading: true,
isSignout: false,
userToken: undefined,
}
);
@@ -147,7 +151,10 @@ export default function SimpleStackScreen({ navigation }: Props) {
) : state.userToken === undefined ? (
<SimpleStack.Screen
name="SignIn"
options={{ title: 'Sign in' }}
options={{
title: 'Sign in',
animationTypeForReplace: state.isSignout ? 'pop' : 'push',
}}
component={SignInScreen}
/>
) : (

View File

@@ -295,6 +295,11 @@ export type StackNavigationOptions = StackHeaderOptions &
* If you set it to `false`, the screen won't animate when pushing or popping. Defaults to `true`.
*/
animationEnabled?: boolean;
/**
* The type of animation to use when this screen replaces another screen. Defaults to `push`.
* When `pop` is used, the `pop` animation is applied to the screen being replaced.
*/
animationTypeForReplace?: 'push' | 'pop';
/**
* Whether you can use gestures to dismiss this screen. Defaults to `true` on iOS, `false` on Android.
*/

View File

@@ -96,6 +96,12 @@ class StackView extends React.Component<Props, State> {
return descriptor ? descriptor.options.animationEnabled !== false : true;
};
const getAnimationTypeForReplace = (key: string) => {
const descriptor = props.descriptors[key] || state.descriptors[key];
return descriptor.options.animationTypeForReplace ?? 'push';
};
if (
previousFocusedRoute &&
previousFocusedRoute.key !== nextFocusedRoute.key
@@ -125,23 +131,41 @@ class StackView extends React.Component<Props, State> {
if (!routes.find(r => r.key === previousFocusedRoute.key)) {
// The previous focused route isn't present in state, we treat this as a replace
replacingRouteKeys = [
...replacingRouteKeys,
previousFocusedRoute.key,
];
openingRouteKeys = openingRouteKeys.filter(
key => key !== previousFocusedRoute.key
);
closingRouteKeys = closingRouteKeys.filter(
key => key !== previousFocusedRoute.key
);
// Keep the old route in state because it's visible under the new route, and removing it will feel abrupt
// We need to insert it just before the focused one (the route being pushed)
// After the push animation is completed, routes being replaced will be removed completely
routes = routes.slice();
routes.splice(routes.length - 1, 0, previousFocusedRoute);
if (getAnimationTypeForReplace(nextFocusedRoute.key) === 'pop') {
closingRouteKeys = [
...closingRouteKeys,
previousFocusedRoute.key,
];
// By default, new routes have a push animation, so we add it to `openingRouteKeys` before
// But since user configured it to animate the old screen like a pop, we need to add this without animation
// So remove it from `openingRouteKeys` which will remove the animation
openingRouteKeys = openingRouteKeys.filter(
key => key !== nextFocusedRoute.key
);
// Keep the route being removed at the end to animate it out
routes = [...routes, previousFocusedRoute];
} else {
replacingRouteKeys = [
...replacingRouteKeys,
previousFocusedRoute.key,
];
closingRouteKeys = closingRouteKeys.filter(
key => key !== previousFocusedRoute.key
);
// Keep the old route in the state because it's visible under the new route, and removing it will feel abrupt
// We need to insert it just before the focused one (the route being pushed)
// After the push animation is completed, routes being replaced will be removed completely
routes = routes.slice();
routes.splice(routes.length - 1, 0, previousFocusedRoute);
}
}
}
} else if (!routes.find(r => r.key === previousFocusedRoute.key)) {
@@ -151,10 +175,10 @@ class StackView extends React.Component<Props, State> {
isAnimationEnabled(previousFocusedRoute.key) &&
!closingRouteKeys.includes(previousFocusedRoute.key)
) {
// Sometimes a route can be closed before the opening animation finishes
// So we also need to remove it from the opening list
closingRouteKeys = [...closingRouteKeys, previousFocusedRoute.key];
// Sometimes a route can be closed before the opening animation finishes
// So we also need to remove it from the opening list
openingRouteKeys = openingRouteKeys.filter(
key => key !== previousFocusedRoute.key
);
@@ -185,7 +209,9 @@ class StackView extends React.Component<Props, State> {
}
if (!routes.length) {
throw new Error(`There should always be at least one route.`);
throw new Error(
'There should always be at least one route in the navigation state.'
);
}
const descriptors = routes.reduce<StackDescriptorMap>((acc, route) => {